diff --git a/Cargo.lock b/Cargo.lock index 232a617a..f5523412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,3 +9,48 @@ version = "0.1.0" [[package]] name = "mw_log_fmt" version = "0.0.1" + +[[package]] +name = "mw_log_fmt_macro" +version = "0.0.1" +dependencies = [ + "mw_log_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" diff --git a/Cargo.toml b/Cargo.toml index 22653f0d..fcbc35e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,13 @@ resolver = "2" # Split to default members without tests and examples. # Used when executing cargo from project root. -default-members = ["src/containers", "src/log/mw_log_fmt"] +default-members = [ + "src/containers", + "src/log/mw_log_fmt", + "src/log/mw_log_fmt_macro", +] # Include tests and examples as a member for IDE support and Bazel builds. -members = ["src/containers", "src/log/mw_log_fmt"] +members = ["src/containers", "src/log/mw_log_fmt", "src/log/mw_log_fmt_macro"] [workspace.package] @@ -16,6 +20,7 @@ authors = ["S-CORE Contributors"] [workspace.dependencies] mw_log_fmt = { path = "src/log/mw_log_fmt" } +mw_log_fmt_macro = { path = "src/log/mw_log_fmt_macro" } [workspace.lints.clippy] diff --git a/docs/baselibs_rust/log/architecture/_assets/interface.puml b/docs/baselibs_rust/log/architecture/_assets/interface.puml index a797f53c..f82a9682 100644 --- a/docs/baselibs_rust/log/architecture/_assets/interface.puml +++ b/docs/baselibs_rust/log/architecture/_assets/interface.puml @@ -37,9 +37,8 @@ package log <> { +trace!(...) : () } - class mw_log_macro <> { + class mw_log_fmt_macro <> { +mw_log_format_args!(format_string: &str, args...) : Arguments<'_> - +mw_log_format_args_nl!(format_string: &str, args...) : Arguments<'_> } package mw_log_fmt { @@ -51,13 +50,12 @@ package log <> { +write(output: &mut dyn ScoreWrite, args: Arguments<'_>) : Result +score_write!(format_string: &str, args...) : Result - +score_writeln!(format_string: &str, args...) : Result } } mw_log -- Level mw_log -- LevelFilter - mw_log -- mw_log_macro + mw_log -- mw_log_fmt_macro mw_log -right- mw_log_fmt } diff --git a/docs/baselibs_rust/log/architecture/_assets/static_view.puml b/docs/baselibs_rust/log/architecture/_assets/static_view.puml index b3218e61..ada34047 100644 --- a/docs/baselibs_rust/log/architecture/_assets/static_view.puml +++ b/docs/baselibs_rust/log/architecture/_assets/static_view.puml @@ -5,10 +5,10 @@ package "log" <> { component "mw_log_fmt" - component "mw_log_macro" + component "mw_log_fmt_macro" mw_log ..> mw_log_fmt : use - mw_log ..> mw_log_macro : use + mw_log ..> mw_log_fmt_macro : use } component "mw_log_subscriber" <> diff --git a/docs/baselibs_rust/log/detailed_design/_assets/class_diagram.puml b/docs/baselibs_rust/log/detailed_design/_assets/class_diagram.puml index cf35dd2c..328fad4e 100644 --- a/docs/baselibs_rust/log/detailed_design/_assets/class_diagram.puml +++ b/docs/baselibs_rust/log/detailed_design/_assets/class_diagram.puml @@ -195,7 +195,6 @@ package "mw_log_fmt crate" { +write(output: &mut dyn ScoreWrite, args: Arguments<'_>) : Result +score_write!(format_string: &str, args...) : Result - +score_writeln!(format_string: &str, args...) : Result } note top Provided macros can be used for implementing custom formatting traits. @@ -217,10 +216,9 @@ package "mw_log_fmt crate" { mw_log_fmt -- Arguments } -package "mw_log_macro crate" { - +class mw_log_macro <> { +package "mw_log_fmt_macro crate" { + +class mw_log_fmt_macro <> { +mw_log_format_args!(format_string: &str, args...) : Arguments<'_> - +mw_log_format_args_nl!(format_string: &str, args...) : Arguments<'_> } note top @@ -262,7 +260,7 @@ package "mw_log_subscriber crate" { } "mw_log crate" -[hidden]down-> "mw_log_fmt crate" -"mw_log crate" -[hidden]up-> "mw_log_macro crate" +"mw_log crate" -[hidden]up-> "mw_log_fmt_macro crate" "mw_log crate" -[hidden]down------> "mw_log_subscriber crate" "mw_log_fmt crate" -[hidden]down------> "mw_log_subscriber crate" diff --git a/docs/baselibs_rust/log/detailed_design/_assets/log_op.puml b/docs/baselibs_rust/log/detailed_design/_assets/log_op.puml index 80fcb378..f6d6ea69 100644 --- a/docs/baselibs_rust/log/detailed_design/_assets/log_op.puml +++ b/docs/baselibs_rust/log/detailed_design/_assets/log_op.puml @@ -7,7 +7,7 @@ participant "mw_log <>" as mw_log end box box #LightGreen -participant "mw_log_macro <>" as mw_log_macro +participant "mw_log_fmt_macro <>" as mw_log_fmt_macro end box box #LightPink @@ -36,8 +36,8 @@ alt log-level-check-failed mw_log --> actor else log-level-check-passed - mw_log -> mw_log_macro : mw_log_format_args!() - mw_log_macro --> mw_log + mw_log -> mw_log_fmt_macro : mw_log_format_args!() + mw_log_fmt_macro --> mw_log mw_log -> logger : log() diff --git a/docs/baselibs_rust/log/detailed_design/index.rst b/docs/baselibs_rust/log/detailed_design/index.rst index 593f7012..f02b6964 100644 --- a/docs/baselibs_rust/log/detailed_design/index.rst +++ b/docs/baselibs_rust/log/detailed_design/index.rst @@ -36,7 +36,9 @@ Log component consists of three units: - `mw_log` - modelled after `log` Rust library. - `mw_log_fmt` - replacement for `core::fmt` provided by Rust core library. -- `mw_log_macro` - replacement for `format_args` macro provided by Rust compiler. +- `mw_log_fmt_macro` - replacement for macros provided by Rust compiler: + - `mw_log_format_args!` - replacement for `format_args!` + - `ScoreDebug` - replacement for `Debug` Most common approach in Rust is that formatting always results in a string. This means that the `log` library always receives a pre-formatted string. diff --git a/src/log/mw_log_fmt/macros.rs b/src/log/mw_log_fmt/macros.rs index 0f324e62..70deec45 100644 --- a/src/log/mw_log_fmt/macros.rs +++ b/src/log/mw_log_fmt/macros.rs @@ -22,17 +22,3 @@ macro_rules! score_write { $crate::write($dst, mw_log::__private_api::format_args!($($arg)*)) }; } - -/// Writes data using provided writer, with a newline appended. -/// -/// For more information, see [`score_write!`]. -#[macro_export] -macro_rules! score_writeln { - ($dst:expr $(,)?) => { - $crate::score_write!($dst, "\n") - }; - ($dst:expr, $($arg:tt)*) => { - // TODO: `mw_log::__private_api` will become available in future PRs. - $crate::write($dst, mw_log::__private_api::format_args_nl!($($arg)*)) - }; -} diff --git a/src/log/mw_log_fmt_macro/BUILD b/src/log/mw_log_fmt_macro/BUILD new file mode 100644 index 00000000..f55f03eb --- /dev/null +++ b/src/log/mw_log_fmt_macro/BUILD @@ -0,0 +1,42 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_rust//rust:defs.bzl", "rust_proc_macro", "rust_test") + +rust_proc_macro( + name = "mw_log_fmt_macro", + srcs = glob(["*.rs"]), + # TODO: expose required interface through `mw_log` and make it private again. + # visibility = ["//visibility:private"], + visibility = ["//visibility:public"], + deps = [ + "//src/log/mw_log_fmt", + "@score_crates//:proc_macro2", + "@score_crates//:quote", + "@score_crates//:syn", + ], +) + +rust_test( + name = "tests", + srcs = glob(["tests/**/*.rs"]), + proc_macro_deps = [ + "//src/log/mw_log_fmt_macro", + ], + tags = [ + "unit_tests", + "ut", + ], + deps = [ + "//src/log/mw_log_fmt", + ], +) diff --git a/src/log/mw_log_fmt_macro/Cargo.toml b/src/log/mw_log_fmt_macro/Cargo.toml new file mode 100644 index 00000000..c52e4359 --- /dev/null +++ b/src/log/mw_log_fmt_macro/Cargo.toml @@ -0,0 +1,32 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +[package] +name = "mw_log_fmt_macro" +version.workspace = true +authors.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +proc-macro = true +path = "lib.rs" + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" +mw_log_fmt.workspace = true + +[lints] +workspace = true diff --git a/src/log/mw_log_fmt_macro/format_args.rs b/src/log/mw_log_fmt_macro/format_args.rs new file mode 100644 index 00000000..4a53d7c8 --- /dev/null +++ b/src/log/mw_log_fmt_macro/format_args.rs @@ -0,0 +1,597 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use mw_log_fmt::{Alignment, DebugAsHex, DisplayHint, FormatSpec, Sign}; +use quote::{quote, ToTokens}; +use syn::punctuated::{IntoIter, Punctuated}; +use syn::token::Comma; +use syn::{parse_macro_input, Error, Expr, ExprLit, Lit}; + +/// Parse error containing reason. +/// - Functions with access to tokens should return `syn::Error` +/// - Other functions should return `ParseError` containing explanation. +struct ParseError(pub String); + +enum Argument { + Position, + Index(usize), + Name(String), +} + +/// Parse left side of the placeholder (`{*arg*:spec}`). +fn parse_argument(s: &str) -> Result { + let arg = if s.is_empty() { + Argument::Position + } else if let Ok(v) = s.parse::() { + Argument::Index(v) + } else { + Argument::Name(s.to_string()) + }; + Ok(arg) +} + +/// Get alignment based on provided character. +fn get_alignment(c: &char) -> Result { + match c { + '<' => Ok(Alignment::Left), + '>' => Ok(Alignment::Right), + '^' => Ok(Alignment::Center), + _ => Err(ParseError(format!("unknown alignment character provided: {c}"))), + } +} + +/// Get sign based on provided character. +fn get_sign(c: &char) -> Result { + match c { + '+' => Ok(Sign::Plus), + '-' => Ok(Sign::Minus), + _ => Err(ParseError(format!("unknown sign character provided: {c}"))), + } +} + +/// Parse right side of the placeholder `{arg:*spec*}`. +fn parse_spec(s: &str) -> Result { + let mut chars = s.chars().peekable(); + + // Parse fill and alignment ([[fill]align]). + let mut fill = ' '; + let mut align = None; + { + if let (Some(a), Some(b)) = (chars.next(), chars.peek()) { + const ALIGN_CHARS: [char; 3] = ['<', '^', '>']; + // `[[fill]align]` + if ALIGN_CHARS.contains(b) { + fill = a; + align = Some(get_alignment(b)?); + chars.next(); + } + // `[align]` + else if ALIGN_CHARS.contains(&a) { + align = Some(get_alignment(&a)?); + } + } + + // `align` not set (`[]`) - reset `chars` position. + if align.is_none() { + chars = s.chars().peekable(); + } + } + + // Parse sign ([sign]). + let mut sign = None; + { + if let Some(c) = chars.peek() { + const SIGN_CHARS: [char; 2] = ['+', '-']; + if SIGN_CHARS.contains(c) { + sign = Some(get_sign(c)?); + } + } + + if sign.is_some() { + chars.next(); + } + } + + // Parse alternate (['#']). + let mut alternate = false; + { + // "if let" and "if" can't be chained before Rust 2024 edition. + if let Some(c) = chars.peek() { + if *c == '#' { + alternate = true; + chars.next(); + } + } + } + + // Parse zero pad (['0']). + let mut zero_pad = false; + { + if let Some(c) = chars.peek() { + if *c == '0' { + zero_pad = true; + chars.next(); + } + } + } + + // Parse width ([width]). + let mut width: Option = None; + { + let mut width_str = String::new(); + while let Some(c) = chars.peek() { + if c.is_ascii_digit() { + width_str.push(*c); + chars.next(); + } else { + break; + } + } + if !width_str.is_empty() { + width = match width_str.parse() { + Ok(v) => Some(v), + Err(_) => return Err(ParseError("unable to parse width".to_string())), + }; + } + } + + // Parse precision (['.' precision]). + let mut precision: Option = None; + { + if let Some(c) = chars.peek() { + if *c == '.' { + chars.next(); + + let mut precision_str = String::new(); + while let Some(c) = chars.peek() { + if c.is_ascii_digit() { + precision_str.push(*c); + chars.next(); + } else { + break; + } + } + if !precision_str.is_empty() { + precision = match precision_str.parse() { + Ok(v) => Some(v), + Err(_) => return Err(ParseError("unable to parse precision".to_string())), + }; + } + } + } + } + + // Parse display hint ([type]). + // Macro and format lib display hints are slightly different and must be mapped. + let display_hint; + let mut debug_as_hex = None; + { + let remainder = chars.collect::(); + display_hint = match remainder.as_str() { + "" => DisplayHint::NoHint, + "?" => DisplayHint::Debug, + "x?" => { + debug_as_hex = Some(DebugAsHex::Lower); + DisplayHint::Debug + }, + "X?" => { + debug_as_hex = Some(DebugAsHex::Upper); + DisplayHint::Debug + }, + "o" => DisplayHint::Octal, + "x" => DisplayHint::LowerHex, + "X" => DisplayHint::UpperHex, + "p" => DisplayHint::Pointer, + "b" => DisplayHint::Binary, + "e" => DisplayHint::LowerExp, + "E" => DisplayHint::UpperExp, + _ => return Err(ParseError(format!("unknown display hint: {remainder}"))), + }; + } + + // Construct format spec. + let mut spec = FormatSpec::new(); + spec.display_hint(display_hint) + .fill(fill) + .align(align) + .sign(sign) + .alternate(alternate) + .zero_pad(zero_pad) + .debug_as_hex(debug_as_hex) + .width(width) + .precision(precision); + + Ok(spec) +} + +/// Tokenize format spec constructor. +fn tokenize_spec(spec: &FormatSpec) -> proc_macro2::TokenStream { + // Additional helpers are required to properly tokenize enums and options. + fn tokenize_display_hint(display_hint: DisplayHint) -> proc_macro2::TokenStream { + match display_hint { + DisplayHint::NoHint => quote! { mw_log_fmt::DisplayHint::NoHint }, + DisplayHint::Debug => quote! { mw_log_fmt::DisplayHint::Debug }, + DisplayHint::Octal => quote! { mw_log_fmt::DisplayHint::Octal }, + DisplayHint::LowerHex => quote! { mw_log_fmt::DisplayHint::LowerHex }, + DisplayHint::UpperHex => quote! { mw_log_fmt::DisplayHint::UpperHex }, + DisplayHint::Pointer => quote! { mw_log_fmt::DisplayHint::Pointer }, + DisplayHint::Binary => quote! { mw_log_fmt::DisplayHint::Binary }, + DisplayHint::LowerExp => quote! { mw_log_fmt::DisplayHint::LowerExp }, + DisplayHint::UpperExp => quote! { mw_log_fmt::DisplayHint::UpperExp }, + } + } + + fn tokenize_alignment(align: Option) -> proc_macro2::TokenStream { + match align { + Some(v) => match v { + Alignment::Left => quote! { Some(mw_log_fmt::Alignment::Left) }, + Alignment::Right => quote! { Some(mw_log_fmt::Alignment::Right) }, + Alignment::Center => quote! { Some(mw_log_fmt::Alignment::Center) }, + }, + None => quote! { None }, + } + } + + fn tokenize_sign(sign: Option) -> proc_macro2::TokenStream { + match sign { + Some(v) => match v { + Sign::Plus => quote! { Some(mw_log_fmt::Sign::Plus) }, + Sign::Minus => quote! { Some(mw_log_fmt::Sign::Minus) }, + }, + None => quote! { None }, + } + } + + fn tokenize_debug_as_hex(debug_as_hex: Option) -> proc_macro2::TokenStream { + match debug_as_hex { + Some(v) => match v { + DebugAsHex::Lower => quote! { Some(mw_log_fmt::DebugAsHex::Lower) }, + DebugAsHex::Upper => quote! { Some(mw_log_fmt::DebugAsHex::Upper) }, + }, + None => quote! { None }, + } + } + + fn tokenize_option_u16(o: Option) -> proc_macro2::TokenStream { + match o { + Some(v) => quote! { Some(#v) }, + None => quote! { None }, + } + } + + let display_hint = tokenize_display_hint(spec.get_display_hint()); + let fill = spec.get_fill(); + let align = tokenize_alignment(spec.get_align()); + let sign = tokenize_sign(spec.get_sign()); + let alternate = spec.get_alternate(); + let zero_pad = spec.get_zero_pad(); + let debug_as_hex = tokenize_debug_as_hex(spec.get_debug_as_hex()); + let width = tokenize_option_u16(spec.get_width()); + let precision = tokenize_option_u16(spec.get_precision()); + + quote! {{ + mw_log_fmt::FormatSpec::from_params( + #display_hint, + #fill, + #align, + #sign, + #alternate, + #zero_pad, + #debug_as_hex, + #width, + #precision + ) + }} +} + +struct Placeholder { + argument: Argument, + spec: FormatSpec, +} + +impl Placeholder { + fn from(s: &str) -> Result { + // Strip surrounding "{}", trim whitespace. + let s = s + .strip_prefix('{') + .ok_or(ParseError("failed to strip placeholder prefix".to_string()))? + .strip_suffix('}') + .ok_or(ParseError("failed to strip placeholder suffix".to_string()))? + .trim(); + + // Check placeholder is empty: `{}`. + if s.is_empty() { + return Ok(Placeholder { + argument: Argument::Position, + spec: FormatSpec::default(), + }); + } + + // Split by `:`. + let (arg, spec) = match s.split_once(':') { + Some((arg, spec)) => (arg, Some(spec)), + None => (s, None), + }; + + // Parse argument. + let argument = parse_argument(arg)?; + + // Parse format spec. + let spec = match spec { + Some(s) => parse_spec(s)?, + None => FormatSpec::default(), + }; + + Ok(Placeholder { argument, spec }) + } +} + +enum Spec { + Literal(String), + Placeholder(Placeholder), +} + +/// Replace double escaped braces ("{{", "}}") with single ones ("{", "}"). +fn process_escaped_braces(string_literal: &str) -> String { + string_literal.replace("{{", "{").replace("}}", "}") +} + +fn process_format_string(format_string: &str) -> Result, ParseError> { + // Find braces locations. + #[derive(PartialEq)] + enum Brace { + SingleLeft, + DoubleLeft, + SingleRight, + DoubleRight, + } + + let mut chars = format_string.chars().enumerate().peekable(); + let mut braces = Vec::new(); + while let Some((i, c)) = chars.next() { + let next = chars.peek().map(|&(_, ch)| ch); + + // Check double left. + if c == '{' && next == Some('{') { + chars.next(); + braces.push((i, Brace::DoubleLeft)); + } + // Check single left. + else if c == '{' { + braces.push((i, Brace::SingleLeft)); + } + // Check double right. + else if c == '}' && next == Some('}') { + chars.next(); + braces.push((i, Brace::DoubleRight)); + } + // Check single right. + else if c == '}' { + braces.push((i, Brace::SingleRight)); + } + } + + // Process braces locations. + // - Process placeholder locations (must start with left and end with right brace). + // - Detect dangling braces. + // - Detect escaped braces inside placeholders. + let mut placeholders = Vec::new(); + let mut braces_it = braces.into_iter().peekable(); + while let Some((i, brace)) = braces_it.next() { + match brace { + // Single left brace might start placeholder. + Brace::SingleLeft => { + let (pi, pb) = braces_it + .peek() + .ok_or_else(|| ParseError("dangling left brace".to_string()))?; + match pb { + Brace::SingleLeft => { + return Err(ParseError("dangling left brace".to_string())); + }, + Brace::SingleRight => { + // Inclusive range cannot be used. + // `Range` and `RangeInclusive` are not compatible. + placeholders.push(i..*pi + 1); + braces_it.next(); + }, + Brace::DoubleLeft | Brace::DoubleRight => { + return Err(ParseError("escaped characters inside placeholder".to_string())); + }, + } + }, + // Dangling right brace. + Brace::SingleRight => { + return Err(ParseError("dangling right brace".to_string())); + }, + // Escaped characters are ignored. + Brace::DoubleLeft | Brace::DoubleRight => continue, + } + } + + // Get ranges of string literals - inverted `placeholders`. + let mut literals = Vec::new(); + let mut prev_end = 0; + let format_string_len = format_string.len(); + for range in &placeholders { + if range.start > prev_end { + literals.push(prev_end..range.start); + } + prev_end = range.end; + } + if prev_end < format_string_len { + literals.push(prev_end..format_string_len); + } + + // Merge literals and placeholders with correct order. + let mut types_and_ranges = Vec::new(); + types_and_ranges.extend(literals.iter().map(|r| (false, r.clone()))); + types_and_ranges.extend(placeholders.iter().map(|r| (true, r.clone()))); + types_and_ranges.sort_by_key(|(_, r)| r.start); + + // Create output - list of specs containing strings. + let mut specs = Vec::new(); + for (is_placeholder, range) in types_and_ranges { + let spec = if is_placeholder { + Spec::Placeholder(Placeholder::from(&format_string[range])?) + } else { + Spec::Literal(process_escaped_braces(&format_string[range])) + }; + specs.push(spec); + } + + Ok(specs) +} + +/// Check valid expression types are used. +/// Named expressions must come after all positional expressions. +fn validate_args(args: &[Expr]) -> Result<(), Error> { + let mut named_found = false; + for arg in args.iter() { + match arg { + Expr::Assign(_) => named_found = true, + // NOTE: the list of allowed expression types may not be complete. + Expr::Lit(_) | Expr::Field(_) | Expr::If(_) | Expr::Path(_) | Expr::Unary(_) => { + if named_found { + return Err(Error::new_spanned( + arg, + "positional arguments must be before named arguments", + )); + } + }, + _ => return Err(Error::new_spanned(arg, "invalid expression type")), + } + } + + Ok(()) +} + +/// Select argument with name. +/// +/// Following cases are supported: +/// - Name provided by spec and `args` - get argument expression from `args`. +/// E.g., `mw_log_format_args!("{arg}", arg)`. +/// - Name provided by spec, but aliased by `args` - get assigned argument expression from `args`. +/// E.g., `mw_log_format_args!("{arg}", arg=other_value)`. +/// +/// Not yet supported: +/// - Name provided by spec, but not `args` - create argument expression. +/// E.g., `mw_log_format_args!("{arg}")`. +fn select_arg_with_name(args: &[Expr], name: &str) -> Result { + // Find all arguments that match. Either zero or one are allowed. + let mut found: Vec = Vec::new(); + for arg in args.iter() { + let (arg_expr, alias_expr) = match arg { + Expr::Assign(expr_assign) => ( + expr_assign.left.as_ref().clone(), + Some(expr_assign.right.as_ref().clone()), + ), + Expr::Lit(_) | Expr::Field(_) | Expr::If(_) | Expr::Path(_) | Expr::Unary(_) => (arg.clone(), None), + _ => return Err(Error::new_spanned(arg, "invalid expression type")), + }; + + if arg_expr.to_token_stream().to_string() == name { + if let Some(alias_expr) = alias_expr { + found.push(alias_expr); + } else { + found.push(arg_expr); + } + } + } + + match found.len() { + // No matching args found - create argument expression. + 0 => Err(Error::new( + proc_macro2::Span::call_site(), + "no matching arguments found", + )), + // Matching arg found. + 1 => Ok(found[0].clone()), + // Multiple matching args found - invalid. + _ => Err(Error::new( + proc_macro2::Span::call_site(), + "multiple matching arguments found", + )), + } +} + +fn parse_fragments(punctuated_it: &mut IntoIter) -> Result, Error> { + // Get first argument - format string. + // Must be a string literal. + let format_string_expr = match punctuated_it.next() { + Some(Expr::Lit(ExprLit { lit: Lit::Str(s), .. })) => s, + Some(expr) => { + return Err(Error::new_spanned(expr, "first argument must be a string literal")); + }, + None => { + return Err(Error::new(proc_macro2::Span::call_site(), "expected a string literal")); + }, + }; + + // Process format string and create list of specs. + let format_string = format_string_expr.value(); + let specs = + process_format_string(&format_string).map_err(|e| Error::new_spanned(format_string_expr.clone(), e.0))?; + + // Process specs and match them to provided args. + let args: Vec = punctuated_it.collect(); + validate_args(&args)?; + let mut fragments = Vec::new(); + // Iterator is used for positional arguments. + let mut args_it = args.iter(); + for spec in specs.into_iter() { + match spec { + Spec::Literal(s) => fragments.push(quote! {{ + mw_log_fmt::Fragment::Literal(#s) + }}), + Spec::Placeholder(placeholder) => { + // Select argument based on provided argument. + let arg = match placeholder.argument { + Argument::Position => match args_it.next() { + Some(arg) => arg, + None => { + return Err(Error::new_spanned( + format_string_expr, + "argument with provided position not found", + )); + }, + }, + Argument::Index(i) => &args[i], + Argument::Name(name) => &select_arg_with_name(&args, &name)?, + }; + + let spec_ctor = tokenize_spec(&placeholder.spec); + + fragments.push(quote! {{ + mw_log_fmt::Fragment::Placeholder(mw_log_fmt::Placeholder::new(&#arg, #spec_ctor)) + }}); + }, + } + } + + Ok(fragments) +} + +pub(crate) fn expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + // Collect expressions separated by comma. + // NOTE: `parse_macro_input!` can't be build if function return type is not `TokenStream`. + let punctuated = parse_macro_input!(input with Punctuated::parse_terminated); + let mut punctuated_it = punctuated.into_iter(); + + // Parse string format into fragments. + let fragments = match parse_fragments(&mut punctuated_it) { + Ok(f) => f, + Err(e) => return e.to_compile_error().into(), + }; + + quote! { mw_log_fmt::Arguments(&[#(#fragments),*]) }.into() +} diff --git a/src/log/mw_log_fmt_macro/lib.rs b/src/log/mw_log_fmt_macro/lib.rs new file mode 100644 index 00000000..d7ee7388 --- /dev/null +++ b/src/log/mw_log_fmt_macro/lib.rs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Replacement for macros provided by Rust compiler: +//! - [`mw_log_format_args!`] - replacement for `format_args!` +//! - [`ScoreDebug`] - replacement for `Debug` + +// All errors should result in compilation error. +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(clippy::panic)] + +mod format_args; +mod score_debug; + +/// Constructs parameters for the other string-formatting macros. +/// +/// This macro takes a formatting string literal containing `{}` for each additional argument. +/// [`mw_log_format_args!`] prepares the additional parameters to ensure the output can be interpreted as a message. +#[proc_macro] +pub fn mw_log_format_args(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + format_args::expand(input) +} + +/// Automatically generate [`ScoreDebug`] implementation. +#[proc_macro_derive(ScoreDebug)] +pub fn score_debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + score_debug::expand(input) +} diff --git a/src/log/mw_log_fmt_macro/score_debug.rs b/src/log/mw_log_fmt_macro/score_debug.rs new file mode 100644 index 00000000..54881585 --- /dev/null +++ b/src/log/mw_log_fmt_macro/score_debug.rs @@ -0,0 +1,207 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use quote::{format_ident, quote}; +use syn::{ + parse_macro_input, Data, DataEnum, DataStruct, DeriveInput, Error, Fields, Ident, ImplGenerics, Index, TypeGenerics, +}; + +/// Generate `ScoreDebug` implementation for struct. +fn generate_for_struct( + ident: Ident, + data_struct: DataStruct, + impl_generics: ImplGenerics, + ty_generics: TypeGenerics, +) -> Result { + // Generate `.fmt` implementations for struct types. + let struct_name = ident.to_string(); + let fmt_impl = match data_struct.fields { + // Regular struct - contains named fields. + Fields::Named(fields) => { + // Generate `.field` method calls for named fields. + let mut field_methods = Vec::new(); + for field in fields.named.into_iter() { + let ident = match field.ident { + Some(ident) => ident, + None => return Err(Error::new_spanned(field, "identifier not found")), + }; + let name = ident.to_string(); + field_methods.push(quote! { .field(#name, &self.#ident) }); + } + + // Generate `.fmt` implementation using named struct helper. + quote! { + mw_log_fmt::DebugStruct::new(f, spec, #struct_name) + #(#field_methods)* + .finish() + } + }, + + // Tuple struct - contains unnamed fields. + Fields::Unnamed(fields) => { + // Generate `.field` method calls for unnamed fields. + let mut field_methods = Vec::new(); + for index in 0..fields.unnamed.len() { + let syn_index = Index::from(index); + field_methods.push(quote! { .field(&self.#syn_index) }); + } + + // Generate `.fmt` implementation using named tuple helper. + quote! { + mw_log_fmt::DebugTuple::new(f, spec, #struct_name) + #(#field_methods)* + .finish() + } + }, + + // Unit struct - no fields. + Fields::Unit => { + quote! { + mw_log_fmt::DebugStruct::new(f, spec, #struct_name).finish() + } + }, + }; + + // Generate `ScoreDebug` implementation for provided struct. + Ok(quote! { + #[automatically_derived] + impl #impl_generics mw_log_fmt::ScoreDebug for #ident #ty_generics { + fn fmt(&self, f: mw_log_fmt::Writer, spec: &mw_log_fmt::FormatSpec) -> mw_log_fmt::Result { + #fmt_impl + } + } + }) +} + +/// Generate `ScoreDebug` implementation for enum. +fn generate_for_enum( + ident: Ident, + data_enum: DataEnum, + impl_generics: ImplGenerics, + ty_generics: TypeGenerics, +) -> Result { + // Handle technically legal empty enum definition. + if data_enum.variants.is_empty() { + return Ok(quote! { + #[automatically_derived] + impl #impl_generics mw_log_fmt::ScoreDebug for #ident #ty_generics { + fn fmt(&self, f: mw_log_fmt::Writer, spec: &mw_log_fmt::FormatSpec) -> mw_log_fmt::Result { + Ok(()) + } + } + }); + } + + // Generate implementations for each variant. + let mut variants = Vec::new(); + for variant in data_enum.variants { + let variant_ident = variant.ident; + let variant_name = variant_ident.to_string(); + + let variant_impl = match variant.fields { + Fields::Named(fields) => { + // Generate arg names and `.field` method calls for named fields. + let mut arg_names = Vec::new(); + let mut field_methods = Vec::new(); + for field in fields.named { + let ident = match field.ident { + Some(ident) => ident, + None => return Err(Error::new_spanned(field, "identifier not found")), + }; + let name = ident.to_string(); + arg_names.push(quote! { #ident }); + field_methods.push(quote! { .field(#name, #ident) }); + } + + // Generate variant match implementation. + quote! { + Self::#variant_ident { #(#arg_names),* } => { + mw_log_fmt::DebugStruct::new(f, spec, #variant_name) + #(#field_methods)* + .finish() + }, + } + }, + Fields::Unnamed(fields) => { + // Generate arg names and `.field` method calls for unnamed fields. + let mut arg_names = Vec::new(); + let mut field_methods = Vec::new(); + for index in 0..fields.unnamed.len() { + let arg_name = format_ident!("arg{}", index); + arg_names.push(quote! { #arg_name }); + field_methods.push(quote! { .field(#arg_name) }); + } + + // Generate variant match implementation. + quote! { + Self::#variant_ident (#(#arg_names),*) => { + mw_log_fmt::DebugTuple::new(f, spec, #variant_name) + #(#field_methods)* + .finish() + }, + } + }, + Fields::Unit => { + quote! { + Self::#variant_ident => f.write_str(#variant_name, spec), + } + }, + }; + + variants.push(variant_impl) + } + + // Generate `ScoreDebug` implementation for provided enum. + Ok(quote! { + #[automatically_derived] + impl #impl_generics mw_log_fmt::ScoreDebug for #ident #ty_generics { + fn fmt(&self, f: mw_log_fmt::Writer, spec: &mw_log_fmt::FormatSpec) -> mw_log_fmt::Result { + match self { + #(#variants)* + } + } + } + }) +} + +/// Generate `ScoreDebug` implementation. +fn generate_score_debug(derive_input: DeriveInput) -> Result { + let DeriveInput { + attrs: _, + vis: _, + ident, + generics, + data, + } = derive_input; + + // Split generics. + let (impl_generics, ty_generics, _) = generics.split_for_impl(); + + match data { + Data::Struct(data_struct) => generate_for_struct(ident, data_struct, impl_generics, ty_generics), + Data::Enum(data_enum) => generate_for_enum(ident, data_enum, impl_generics, ty_generics), + Data::Union(_) => Err(Error::new( + proc_macro2::Span::call_site(), + "`#[derive(ScoreDebug)] does not support unions`", + )), + } +} + +pub(crate) fn expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let derive_input = parse_macro_input!(input as DeriveInput); + match generate_score_debug(derive_input) { + Ok(token_stream) => token_stream, + Err(e) => e.into_compile_error(), + } + .into() +} diff --git a/src/log/mw_log_fmt_macro/tests/test_format_args.rs b/src/log/mw_log_fmt_macro/tests/test_format_args.rs new file mode 100644 index 00000000..27c78a33 --- /dev/null +++ b/src/log/mw_log_fmt_macro/tests/test_format_args.rs @@ -0,0 +1,308 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Tests for `mw_log_format_args`. +//! +//! Only positive paths can be checked with regular unit tests. +//! This is due to error paths resulting in compilation errors (as expected with proc macros). +//! +//! Results are compared with Rust built-in `format_args` macro. + +// TODO: tests fail for `+nightly-2025-05-30` and older. +// Remove `#[cfg(not(miri))]` once updated in CI. +// https://github.com/eclipse-score/baselibs_rust/issues/31 + +mod utils; + +use crate::utils::StringWriter; +use mw_log_fmt::{write, Alignment, DebugAsHex, DisplayHint, Fragment, Sign}; +use mw_log_fmt_macro::mw_log_format_args; + +#[track_caller] +fn common_format_args_test( + mw_log_args: mw_log_fmt::Arguments, + std_args: core::fmt::Arguments, + expected_num_fragments: usize, + expected_output: &str, +) { + // Write data to string. + let mut w = StringWriter::new(); + let _ = write(&mut w, mw_log_args).map_err(|_| panic!("write failed")); + + // Check `mw_log` args. + assert_eq!(mw_log_args.0.len(), expected_num_fragments); + assert_eq!(w.get(), expected_output); + + // Compare with Rust built-in format args. + let expected = std::fmt::format(std_args); + assert_eq!(w.get(), expected); +} + +#[test] +fn test_single_literal() { + let mw_log_args = mw_log_format_args!("test_string"); + let core_fmt_args = format_args!("test_string"); + common_format_args_test(mw_log_args, core_fmt_args, 1, "test_string"); +} + +#[test] +fn test_escaped_braces() { + let mw_log_args = mw_log_format_args!("{{}}}}{{"); + let core_fmt_args = format_args!("{{}}}}{{"); + common_format_args_test(mw_log_args, core_fmt_args, 1, "{}}{"); +} + +#[test] +#[cfg(not(miri))] +fn test_single_placeholder() { + let mw_log_args = mw_log_format_args!("{}", 123); + let core_fmt_args = format_args!("{}", 123); + common_format_args_test(mw_log_args, core_fmt_args, 1, "123"); +} + +#[test] +#[cfg(not(miri))] +fn test_mixed_literals_and_placeholders() { + let mw_log_args = mw_log_format_args!("test_{}_string", 321); + let core_fmt_args = format_args!("test_{}_string", 321); + common_format_args_test(mw_log_args, core_fmt_args, 3, "test_321_string"); +} + +#[test] +#[cfg(not(miri))] +fn test_arg_index() { + let mw_log_args = mw_log_format_args!("test_{2}_{1}_{0}", 123, 234, 345); + let core_fmt_args = format_args!("test_{2}_{1}_{0}", 123, 234, 345); + common_format_args_test(mw_log_args, core_fmt_args, 6, "test_345_234_123"); +} + +#[test] +#[cfg(not(miri))] +fn test_arg_pos_and_index() { + let mw_log_args = mw_log_format_args!("test_{2}_{}_{1}_{}_{0}", 123, 234, 345); + let core_fmt_args = format_args!("test_{2}_{}_{1}_{}_{0}", 123, 234, 345); + common_format_args_test(mw_log_args, core_fmt_args, 10, "test_345_123_234_234_123"); +} + +#[test] +#[cfg(not(miri))] +fn test_arg_name() { + let x1 = 123; + let x2 = 234; + let x3 = 345; + let mw_log_args = mw_log_format_args!("test_{x3}_{x2}_{x1}", x1, x2, x3); + // NOTE: known misalignment. + // It is not allowed to have redundant arguments in Rust (`("{x1}", x1)`). + // This is currently not possible to do using `mw_log_format_args`. + let core_fmt_args = format_args!("test_{x3}_{x2}_{x1}"); + common_format_args_test(mw_log_args, core_fmt_args, 6, "test_345_234_123"); +} + +#[test] +#[cfg(not(miri))] +fn test_arg_name_alias() { + let x1 = 123; + let x2 = 234; + let x3 = 345; + let mw_log_args = mw_log_format_args!("test_{a3}_{a2}_{a1}", a1 = x1, a2 = x2, a3 = x3); + let core_fmt_args = format_args!("test_{a3}_{a2}_{a1}", a1 = x1, a2 = x2, a3 = x3); + common_format_args_test(mw_log_args, core_fmt_args, 6, "test_345_234_123"); +} + +#[test] +#[cfg(not(miri))] +fn test_arg_pos_and_name() { + let x1 = 123; + let x2 = 234; + let x3 = 345; + let mw_log_args = mw_log_format_args!("test_{x3}_{}_{x2}_{}_{x1}", x1, x2, x3); + // NOTE: known misalignment. + // It is not allowed to have redundant arguments in Rust (`("{x1}", x1)`). + // This is currently not possible to do using `mw_log_format_args`. + let core_fmt_args = format_args!("test_{x3}_{}_{x2}_{}_{x1}", x1, x2); + common_format_args_test(mw_log_args, core_fmt_args, 10, "test_345_123_234_234_123"); +} + +#[test] +#[cfg(not(miri))] +fn test_arg_mixed() { + let x1 = 111; + let x2 = 222; + let mw_log_args = mw_log_format_args!("test_{x1}_{1}_{}", x1, x2); + let core_fmt_args = format_args!("test_{x1}_{1}_{}", x1, x2); + common_format_args_test(mw_log_args, core_fmt_args, 6, "test_111_222_111"); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_empty() { + let args = mw_log_format_args!("{:}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::NoHint); + assert_eq!(format_spec.get_fill(), ' '); + assert!(format_spec.get_align().is_none()); + assert!(format_spec.get_sign().is_none()); + assert!(!format_spec.get_alternate()); + assert!(!format_spec.get_zero_pad()); + assert!(format_spec.get_debug_as_hex().is_none()); + assert_eq!(format_spec.get_width(), None); + assert_eq!(format_spec.get_precision(), None); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_all() { + let args = mw_log_format_args!("{:c<-#0333.555x}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::LowerHex); + assert_eq!(format_spec.get_fill(), 'c'); + assert!(format_spec.get_align() == Some(Alignment::Left)); + assert!(format_spec.get_sign() == Some(Sign::Minus)); + assert!(format_spec.get_alternate()); + assert!(format_spec.get_zero_pad()); + assert!(format_spec.get_debug_as_hex().is_none()); + assert_eq!(format_spec.get_width(), Some(333)); + assert_eq!(format_spec.get_precision(), Some(555)); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_debug() { + let args = mw_log_format_args!("{:#X?}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::Debug); + assert_eq!(format_spec.get_fill(), ' '); + assert!(format_spec.get_align().is_none()); + assert!(format_spec.get_sign().is_none()); + assert!(format_spec.get_alternate()); + assert!(!format_spec.get_zero_pad()); + assert!(format_spec.get_debug_as_hex() == Some(DebugAsHex::Upper)); + assert_eq!(format_spec.get_width(), None); + assert_eq!(format_spec.get_precision(), None); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_display_hint_octal() { + let args = mw_log_format_args!("{:o}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::Octal); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_display_hint_lower_hex() { + let args = mw_log_format_args!("{:x}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::LowerHex); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_display_hint_upper_hex() { + let args = mw_log_format_args!("{:X}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::UpperHex); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_display_hint_pointer() { + let args = mw_log_format_args!("{:p}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::Pointer); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_display_hint_binary() { + let args = mw_log_format_args!("{:b}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::Binary); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_display_hint_lower_exp() { + let args = mw_log_format_args!("{:e}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::LowerExp); +} + +#[test] +#[cfg(not(miri))] +fn test_format_spec_display_hint_upper_exp() { + let args = mw_log_format_args!("{:E}", 123); + + let placeholder = match args.0.first().unwrap() { + Fragment::Literal(_) => panic!("invalid variant"), + Fragment::Placeholder(placeholder) => placeholder, + }; + + let format_spec = placeholder.format_spec(); + assert!(format_spec.get_display_hint() == DisplayHint::UpperExp); +} diff --git a/src/log/mw_log_fmt_macro/tests/test_score_debug.rs b/src/log/mw_log_fmt_macro/tests/test_score_debug.rs new file mode 100644 index 00000000..e11590d8 --- /dev/null +++ b/src/log/mw_log_fmt_macro/tests/test_score_debug.rs @@ -0,0 +1,176 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Tests for `ScoreDebug` derive macro. +//! +//! Only positive paths can be checked with regular unit tests. +//! This is due to error paths resulting in compilation errors (as expected with proc macros). +//! +//! Results are compared with Rust built-in `Debug` derive macro. + +// TODO: tests fail for `+nightly-2025-05-30` and older. +// Remove `#[cfg(not(miri))]` once updated in CI. +// https://github.com/eclipse-score/baselibs_rust/issues/31 + +mod utils; + +use crate::utils::StringWriter; +use mw_log_fmt::{write, ScoreDebug}; +use mw_log_fmt_macro::{mw_log_format_args, ScoreDebug}; + +#[test] +#[cfg(not(miri))] +fn test_struct_named() { + #[derive(Debug, ScoreDebug)] + struct Point { + x: i32, + y: i32, + name: String, + } + + let p = Point { + x: 123, + y: -321, + name: "example".to_string(), + }; + + let args = mw_log_format_args!("{:?}", p); + let mut w = StringWriter::new(); + let _ = write(&mut w, args).map_err(|_| panic!("write failed")); + + // Compare with Rust built-in `Debug` derive macro. + let expected = format!("{:?}", p); + assert_eq!(w.get(), expected); +} + +#[test] +#[cfg(not(miri))] +fn test_struct_unnamed() { + #[derive(Debug, ScoreDebug)] + struct Point(i32, i32, String); + + let p = Point(123, -123, "example".to_string()); + + let args = mw_log_format_args!("{:?}", p); + let mut w = StringWriter::new(); + let _ = write(&mut w, args).map_err(|_| panic!("write failed")); + + // Compare with Rust built-in `Debug` derive macro. + let expected = format!("{:?}", p); + assert_eq!(w.get(), expected); +} + +#[test] +#[cfg(not(miri))] +fn test_struct_unit() { + #[derive(Debug, ScoreDebug)] + struct UnitStruct; + + let unit_struct = UnitStruct; + + let args = mw_log_format_args!("{:?}", unit_struct); + let mut w = StringWriter::new(); + let _ = write(&mut w, args).map_err(|_| panic!("write failed")); + + // Compare with Rust built-in `Debug` derive macro. + let expected = format!("{:?}", unit_struct); + assert_eq!(w.get(), expected); +} + +#[test] +#[cfg(not(miri))] +fn test_struct_generics() { + #[derive(Debug, ScoreDebug)] + // #[derive(Debug)] + struct Example<'a, const N: usize, T: PartialEq + ScoreDebug> { + lifetime: &'a str, + generic: [T; N], + } + + let p = Example { + lifetime: "example", + generic: [123; 10], + }; + + let args = mw_log_format_args!("{:?}", p); + let mut w = StringWriter::new(); + let _ = write(&mut w, args).map_err(|_| panic!("write failed")); + + // Compare with Rust built-in `Debug` derive macro. + let expected = format!("{:?}", p); + assert_eq!(w.get(), expected); +} + +#[test] +#[cfg(not(miri))] +fn test_enum_plain() { + #[allow(dead_code)] + #[derive(Debug, ScoreDebug)] + enum Flag { + Ignored, + Optional, + Required, + } + + let flag = Flag::Optional; + + let args = mw_log_format_args!("{:?}", flag); + let mut w = StringWriter::new(); + let _ = write(&mut w, args).map_err(|_| panic!("write failed")); + + // Compare with Rust built-in `Debug` derive macro. + let expected = format!("{:?}", flag); + assert_eq!(w.get(), expected); +} + +#[test] +#[cfg(not(miri))] +fn test_enum_nested() { + #[allow(dead_code)] + #[derive(Debug, ScoreDebug)] + enum Variant<'a> { + Int(i32, i16), + Bool(bool), + String(String), + Nothing, + Nested(&'a Variant<'a>), + Struct { x: i32, y: i32 }, + } + + let nested = Variant::Bool(true); + let cases = [ + Variant::Int(135, 321), + Variant::Bool(true), + Variant::String("example".to_string()), + Variant::Nothing, + Variant::Nested(&nested), + Variant::Struct { x: 333, y: 444 }, + ]; + + for case in cases { + let args = mw_log_format_args!("{:?}", case); + let mut w = StringWriter::new(); + let _ = write(&mut w, args).map_err(|_| panic!("write failed")); + + // Compare with Rust built-in `Debug` derive macro. + let expected = format!("{:?}", case); + assert_eq!(w.get(), expected); + } +} + +#[test] +fn test_enum_empty() { + #[allow(dead_code)] + #[derive(ScoreDebug)] + enum X {} +} diff --git a/src/log/mw_log_fmt_macro/tests/tests.rs b/src/log/mw_log_fmt_macro/tests/tests.rs new file mode 100644 index 00000000..4483c5ed --- /dev/null +++ b/src/log/mw_log_fmt_macro/tests/tests.rs @@ -0,0 +1,14 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Workaround for `rust_test` Bazel target not being able to run without `crate_root`. diff --git a/src/log/mw_log_fmt_macro/tests/utils/mod.rs b/src/log/mw_log_fmt_macro/tests/utils/mod.rs new file mode 100644 index 00000000..b06870d6 --- /dev/null +++ b/src/log/mw_log_fmt_macro/tests/utils/mod.rs @@ -0,0 +1,81 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use core::fmt::Write; +use mw_log_fmt::{Error, FormatSpec, Result, ScoreWrite}; + +/// Writer implementation. +/// Writes everything to a string, so it can be compared with `format` macro. +pub(crate) struct StringWriter { + buf: String, +} + +impl StringWriter { + pub fn new() -> Self { + Self { buf: String::new() } + } + + pub fn get(&self) -> &str { + self.buf.as_str() + } +} + +impl ScoreWrite for StringWriter { + fn write_bool(&mut self, v: &bool, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_f32(&mut self, v: &f32, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_f64(&mut self, v: &f64, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_i8(&mut self, v: &i8, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_i16(&mut self, v: &i16, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_i32(&mut self, v: &i32, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_i64(&mut self, v: &i64, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_u8(&mut self, v: &u8, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_u16(&mut self, v: &u16, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_u32(&mut self, v: &u32, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_u64(&mut self, v: &u64, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } + + fn write_str(&mut self, v: &str, _spec: &FormatSpec) -> Result { + write!(self.buf, "{}", v).map_err(|_| Error) + } +}