feat(print): add print module
Some checks failed
Test / build (map[name:nightly]) (push) Failing after 39s
Test / build (map[name:stable]) (push) Failing after 35s

This commit is contained in:
Paul Campbell 2024-12-07 10:49:42 +00:00
parent 436ad890d8
commit 8f71d1107f
5 changed files with 529 additions and 3 deletions

34
examples/printer.rs Normal file
View file

@ -0,0 +1,34 @@
use kxio::{kxprintln, print::Printer};
// Example using kxprintln macro
fn greet(printer: &Printer, name: &str) {
kxprintln!(printer, "Hello, {}!", name);
}
fn main() {
// Production code example
let printer = Printer::standard();
kxprintln!(printer, "Macro says: Hello, {}!", "Carol");
greet(&printer, "Carol");
}
#[cfg(test)]
mod test {
use super::*;
use kxio::print::TestPrint;
#[test]
fn test_printer() {
// Test code
let printer = Printer::test();
// Get reference to TestPrinter so we can make assertions
let test_print = printer.as_test().unwrap();
greet(&printer, "Bob");
assert_eq!(test_print.output(), "Hello, Bob!\n");
test_print.clear();
greet(&printer, "Dave");
assert_eq!(test_print.output(), "Hello, Dave!\n");
}
}

View file

@ -1,14 +1,15 @@
//! # kxio
//!
//! `kxio` is a Rust library that provides injectable `FileSystem` and `Network`
//! `kxio` is a Rust library that provides injectable `FileSystem`, `Network` and `Printer`
//! resources to enhance the testability of your code. By abstracting system-level
//! interactions, `kxio` enables easier mocking and testing of code that relies on
//! file system and network operations.
//! file system, network and print operations.
//!
//! ## Features
//!
//! - Filesystem Abstraction
//! - Network Abstraction
//! - Print Abstraction
//! - Enhanced Testability
//!
//! ## Filesystem
@ -30,6 +31,11 @@
//! comprehensive documentation and usage examples, please refer to
//! <https://docs.rs/kxio/latest/kxio/net/>
//!
//! ## Printer
//!
//! No, not a hardware printer, but console output via the family of `println` macros from the
//! Standard Library.
//!
//! ## Getting Started
//!
//! Add `kxio` to your `Cargo.toml`:
@ -42,7 +48,7 @@
//! ## Usage
//!
//! See the example [get.rs](https://git.kemitix.net/kemitix/kxio/src/branch/main/examples/get.rs) for an annotated example on how to use the `kxio` library.
//! It covers both the `net` and `fs` modules.
//! It covers the `net`, `fs` and `print` modules.
//!
//! ## Development
//!
@ -65,6 +71,7 @@
pub mod fs;
pub mod net;
pub mod print;
mod result;
pub use result::{Error, Result};

74
src/print/macros.rs Normal file
View file

@ -0,0 +1,74 @@
#[macro_export]
macro_rules! kxprintln {
($printer:expr, $($arg:tt)*) => {{
$crate::print::Print::println_fmt(&$printer.clone(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! kxprint {
($printer:expr, $($arg:tt)*) => {{
$crate::print::Print::print_fmt(&$printer.clone(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! kxeprintln {
($printer:expr, $($arg:tt)*) => {{
$crate::print::Print::eprintln_fmt(&$printer.clone(), format_args!($($arg)*))
}};
}
#[macro_export]
macro_rules! kxeprint {
($printer:expr, $($arg:tt)*) => {{
$crate::print::Print::eprint_fmt(&$printer.clone(), format_args!($($arg)*))
}};
}
#[cfg(test)]
mod tests {
use super::super::TestPrint;
#[test]
fn test_macro_with_ref_and_owned() {
// Test with reference
let printer = TestPrint::new();
kxprintln!(&printer, "ref test");
assert_eq!(printer.output(), "ref test\n");
printer.clear();
// Test with owned value
kxprintln!(printer, "owned test");
assert_eq!(printer.output(), "owned test\n");
}
#[test]
fn test_kxprint_macro() {
let printer = TestPrint::new();
kxprint!(printer, "Hello {}", "world");
assert_eq!(printer.output(), "Hello world");
}
#[test]
fn test_kxprintln_macro() {
let printer = TestPrint::new();
kxprintln!(printer, "Hello {}", "world");
assert_eq!(printer.output(), "Hello world\n");
}
#[test]
fn test_kxeprint_macro() {
let printer = TestPrint::new();
kxeprint!(printer, "Error: {}", "file not found");
assert_eq!(printer.stderr(), "Error: file not found");
}
#[test]
fn test_kxeprintln_macro() {
let printer = TestPrint::new();
kxeprintln!(printer, "Error: {}", "permission denied");
assert_eq!(printer.stderr(), "Error: permission denied\n");
}
}

343
src/print/mod.rs Normal file
View file

@ -0,0 +1,343 @@
//! Provides an injectable interface for standard print operations.
//!
//! This module also includes macros for println and print operations that
//! are similar to the standard library but work with the Print trait.
//!
//! This module offers a trait-based abstraction over printing operations,
//! allowing for dependency injection and easier testing of code that performs
//! printing operations.
//!
//! # Examples
//!
//! ```
//! use kxio::print::{Print, StandardPrint};
//!
//! fn print_hello(printer: &impl Print) {
//! printer.println("Hello, World!");
//! }
//!
//! let printer = StandardPrint;
//! print_hello(&printer);
//! ```
mod macros;
mod printer;
pub use printer::Printer;
use std::fmt::Arguments;
/// Standard implementation of the Print trait that uses the std::print! and std::println! macros.
pub fn standard() -> Printer {
Printer::standard()
}
/// A no-op implementation of the Print trait that discards all output.
pub fn null() -> Printer {
Printer::null()
}
/// A test implementation that captures output in a String.
pub fn test() -> Printer {
Printer::test()
}
/// Trait defining print operations that can be performed by implementors.
pub trait Print: Clone {
/// Prints a formatted line to the standard output.
///
/// # Arguments
///
/// * `args` - Format arguments to print
///
/// # Examples
///
/// ```
/// use kxio::print::{Print, StandardPrint};
///
/// let printer = StandardPrint;
/// printer.print_fmt(format_args!("Hello"));
/// ```
fn print_fmt(&self, args: Arguments<'_>);
/// Prints a formatted line followed by a newline to the standard output.
///
/// # Arguments
///
/// * `args` - Format arguments to print
///
/// # Examples
///
/// ```
/// use kxio::print::{Print, StandardPrint};
///
/// let printer = StandardPrint;
/// printer.println_fmt(format_args!("Hello"));
/// ```
fn println_fmt(&self, args: Arguments<'_>);
/// Prints a string slice to the standard output.
///
/// # Arguments
///
/// * `s` - The string slice to print
///
/// # Examples
///
/// ```
/// use kxio::print::{Print, StandardPrint};
///
/// let printer = StandardPrint;
/// printer.print("Hello");
/// ```
fn print(&self, s: &str) {
self.print_fmt(format_args!("{}", s));
}
/// Prints a string slice followed by a newline to the standard output.
///
/// # Arguments
///
/// * `s` - The string slice to print
///
/// # Examples
///
/// ```
/// use kxio::print::{Print, StandardPrint};
///
/// let printer = StandardPrint;
/// printer.println("Hello");
/// ```
fn println(&self, s: &str) {
self.println_fmt(format_args!("{}", s));
}
/// Prints a string slice to the standard error.
///
/// # Arguments
///
/// * `s` - The string slice to print to stderr
///
/// # Examples
///
/// ```
/// use kxio::print::{Print, StandardPrint};
///
/// let printer = StandardPrint;
/// printer.eprint("Error");
/// ```
fn eprint(&self, s: &str) {
self.eprint_fmt(format_args!("{}", s));
}
/// Prints a string slice followed by a newline to the standard error.
///
/// # Arguments
///
/// * `s` - The string slice to print to stderr
///
/// # Examples
///
/// ```
/// use kxio::print::{Print, StandardPrint};
///
/// let printer = StandardPrint;
/// printer.eprintln("Error");
/// ```
fn eprintln(&self, s: &str) {
self.eprintln_fmt(format_args!("{}", s));
}
/// Prints to stderr with a format string.
///
/// This method is the base method for printing to stderr. The other stderr printing
/// methods are implemented in terms of this one.
///
/// # Arguments
///
/// * `args` - The format arguments to print
fn eprint_fmt(&self, args: Arguments<'_>);
/// Prints to stderr with a format string, followed by a newline.
///
/// This method is the base method for printing to stderr with a newline. The other stderr printing
/// methods are implemented in terms of this one.
///
/// # Arguments
///
/// * `args` - The format arguments to print
fn eprintln_fmt(&self, args: Arguments<'_>);
}
/// Standard implementation of the Print trait that uses the std::print! and std::println! macros.
#[derive(Clone, Debug, Default, Copy)]
pub struct StandardPrint;
impl Print for StandardPrint {
fn print_fmt(&self, args: Arguments<'_>) {
std::print!("{}", args);
}
fn println_fmt(&self, args: Arguments<'_>) {
std::println!("{}", args);
}
fn eprint_fmt(&self, args: Arguments<'_>) {
std::eprint!("{}", args);
}
fn eprintln_fmt(&self, args: Arguments<'_>) {
std::eprintln!("{}", args);
}
}
/// A no-op implementation of the Print trait that discards all output.
#[derive(Clone, Debug, Default, Copy)]
pub struct NullPrint;
impl Print for NullPrint {
fn print_fmt(&self, _args: Arguments<'_>) {}
fn println_fmt(&self, _args: Arguments<'_>) {}
fn eprint_fmt(&self, _args: Arguments<'_>) {}
fn eprintln_fmt(&self, _args: Arguments<'_>) {}
}
/// A test implementation that captures output in a String.
#[derive(Clone, Debug, Default)]
pub struct TestPrint {
stdout: std::sync::Arc<std::sync::Mutex<String>>,
stderr: std::sync::Arc<std::sync::Mutex<String>>,
}
impl TestPrint {
/// Creates a new TestPrint instance.
pub fn new() -> Self {
Self {
stdout: std::sync::Arc::new(std::sync::Mutex::new(String::new())),
stderr: std::sync::Arc::new(std::sync::Mutex::new(String::new())),
}
}
/// Returns the captured stdout output as a String.
pub fn output(&self) -> String {
self.stdout.lock().unwrap().clone()
}
/// Returns the captured stderr output as a String.
pub fn stderr(&self) -> String {
self.stderr.lock().unwrap().clone()
}
/// Clears both the captured stdout and stderr output.
pub fn clear(&self) {
self.stdout.lock().unwrap().clear();
self.stderr.lock().unwrap().clear();
}
}
impl Print for TestPrint {
fn print_fmt(&self, args: Arguments<'_>) {
(&self).print_fmt(args)
}
fn println_fmt(&self, args: Arguments<'_>) {
(&self).println_fmt(args)
}
fn eprint_fmt(&self, args: Arguments<'_>) {
(&self).eprint_fmt(args)
}
fn eprintln_fmt(&self, args: Arguments<'_>) {
(&self).eprintln_fmt(args)
}
}
impl Print for &TestPrint {
fn print_fmt(&self, args: Arguments<'_>) {
let mut output = self.stdout.lock().unwrap();
output.push_str(&format!("{}", args));
}
fn println_fmt(&self, args: Arguments<'_>) {
let mut output = self.stdout.lock().unwrap();
output.push_str(&format!("{}\n", args));
}
fn eprint_fmt(&self, args: Arguments<'_>) {
let mut output = self.stderr.lock().unwrap();
output.push_str(&format!("{}", args));
}
fn eprintln_fmt(&self, args: Arguments<'_>) {
let mut output = self.stderr.lock().unwrap();
output.push_str(&format!("{}\n", args));
}
}
#[cfg(test)]
mod tests {
use crate::{kxprint, kxprintln};
use super::*;
#[test]
fn test_standard_print() {
let printer = Printer::standard();
// Note: This will actually print to stdout
printer.println("This is a test");
kxprintln!(printer, "This is a {} test", "macro");
}
#[test]
fn test_null_print() {
let printer = Printer::null();
printer.println("This should not appear anywhere");
kxprintln!(printer, "This should also, {}", "not appear anywhere");
}
#[test]
fn test_test_print() {
let printer = Printer::test();
// we are interleaving printing, with assertions, so just extraxt the TestPrinter reference now
let test_print = printer.as_test().unwrap();
// Test stdout functions
printer.print("Hello");
kxprint!(printer, " ");
printer.println("World");
assert_eq!(test_print.output(), "Hello World\n");
// Test stderr functions
printer.eprint("Error: ");
printer.eprintln("something went wrong");
assert_eq!(test_print.stderr(), "Error: something went wrong\n");
// Test clear function clears both buffers
test_print.clear();
assert_eq!(test_print.output(), "");
assert_eq!(test_print.stderr(), "");
// Verify separate stdout/stderr streams
printer.println("info: running task");
printer.eprintln("error: task failed");
assert_eq!(test_print.output(), "info: running task\n");
assert_eq!(test_print.stderr(), "error: task failed\n");
test_print.clear();
printer.println("New message");
kxprintln!(printer, "second line");
assert_eq!(test_print.output(), "New message\nsecond line\n");
}
#[test]
fn test_print_in_function() {
fn print_message(printer: &Printer) {
printer.println("Test message");
kxprintln!(printer, "{} test message", "Second");
}
let printer = Printer::test();
let test_print = printer.as_test().unwrap();
print_message(&printer);
assert_eq!(test_print.output(), "Test message\nSecond test message\n");
}
}

68
src/print/printer.rs Normal file
View file

@ -0,0 +1,68 @@
use super::{NullPrint, Print, StandardPrint, TestPrint};
/// A wrapper struct that can contain any implementation of the Print trait
#[derive(Clone)]
pub enum Printer {
Standard(StandardPrint),
Null(NullPrint),
Test(TestPrint),
}
impl Printer {
/// Creates a new Printer wrapping a StandardPrint implementation
pub fn standard() -> Self {
Self::Standard(StandardPrint)
}
/// Creates a new Printer wrapping a NullPrint implementation
pub fn null() -> Self {
Self::Null(NullPrint)
}
/// Creates a new Printer wrapping a TestPrint implementation
pub fn test() -> Self {
Self::Test(TestPrint::new())
}
/// Returns a reference to the wrapped TestPrint implementation if this Printer contains one
pub fn as_test(&self) -> Option<&TestPrint> {
match self {
Self::Test(test_print) => Some(test_print),
_ => None,
}
}
}
impl Print for Printer {
fn print_fmt(&self, args: std::fmt::Arguments<'_>) {
match self {
Self::Standard(p) => p.print_fmt(args),
Self::Null(p) => p.print_fmt(args),
Self::Test(p) => p.print_fmt(args),
}
}
fn println_fmt(&self, args: std::fmt::Arguments<'_>) {
match self {
Self::Standard(p) => p.println_fmt(args),
Self::Null(p) => p.println_fmt(args),
Self::Test(p) => p.println_fmt(args),
}
}
fn eprint_fmt(&self, args: std::fmt::Arguments<'_>) {
match self {
Self::Standard(p) => p.eprint_fmt(args),
Self::Null(p) => p.eprint_fmt(args),
Self::Test(p) => p.eprint_fmt(args),
}
}
fn eprintln_fmt(&self, args: std::fmt::Arguments<'_>) {
match self {
Self::Standard(p) => p.eprintln_fmt(args),
Self::Null(p) => p.eprintln_fmt(args),
Self::Test(p) => p.eprintln_fmt(args),
}
}
}