diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0edfde0d7b..7d6bb611a7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -314,6 +314,21 @@ jobs: - name: "Test tracing-mock with all features enabled" run: cargo test --all-features -p tracing-mock + check-tracing-macros-stable: + # Feature flag tests that run on stable Rust. + name: cargo check (tracing macros) + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: "Generate macro tests" + run: cargo run --bin xtask -- gen-macro-tests + - name: "Check tracing macro invocations (" + # TODO(hds): remove `|| true` when all macros compile + run: cargo check --tests || true + working-directory: "tracing/test-macros" + # all required checks except for the main test run (which we only require # specific matrix combinations from) all_required: @@ -326,5 +341,6 @@ jobs: - test-build-wasm - test-wasm - test-features-stable + - check-tracing-macros-stable steps: - run: exit 0 diff --git a/Cargo.toml b/Cargo.toml index 282f93bb75..fbcf98731d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ members = [ "tracing-test", "tracing-appender", "tracing-journald", - "examples" + "examples", + "xtask", ] # This will be ignored with Rust older than 1.74, but for now that's okay; diff --git a/tracing/test-macros/Cargo.toml b/tracing/test-macros/Cargo.toml new file mode 100644 index 0000000000..a118ad349e --- /dev/null +++ b/tracing/test-macros/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "test-macros" +version = "0.1.0" +publish = false +edition = "2018" + +[dependencies] +tracing = { path = ".." } diff --git a/tracing/test-macros/tests/.gitignore b/tracing/test-macros/tests/.gitignore new file mode 100644 index 0000000000..72e8ffc0db --- /dev/null +++ b/tracing/test-macros/tests/.gitignore @@ -0,0 +1 @@ +* diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000000..abe56d853a --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +clap = { version = "4.5", features = ["derive"] } + +[lints] +workspace = true diff --git a/xtask/src/macro_tests.rs b/xtask/src/macro_tests.rs new file mode 100644 index 0000000000..a63d11c5f1 --- /dev/null +++ b/xtask/src/macro_tests.rs @@ -0,0 +1,668 @@ +use std::{ + error, fmt, fs, io, + path::{self, PathBuf}, + sync::OnceLock, +}; + +pub fn gen_macro_tests() -> Result<(), Box> { + let proj_root = { + let mut path = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")); + if !path.pop() { + return Err(format!("Manifest dir should be absolute, not relative: {path:?}").into()); + } + path + }; + + for macro_invocation in MacroInvocation::all() { + create_macro_tests(&proj_root, macro_invocation)?; + } + + Ok(()) +} + +fn create_macro_tests( + proj_root: &path::Path, + macro_inv: &MacroInvocation, +) -> Result<(), io::Error> { + for directives in Directives::all() { + let test_file = proj_root.join(format!( + "tracing/test-macros/tests/{macro_name}{suffix}.rs", + macro_name = macro_inv.macro_name(), + suffix = directives.file_suffix() + )); + eprintln!("generating test file: {test_file:?}"); + let mut test_fh = fs::File::create(test_file).unwrap(); + + emit_header(&mut test_fh, macro_inv.macro_name()).unwrap(); + emit_macro_test_file(&mut test_fh, macro_inv, directives)?; + } + + Ok(()) +} + +fn emit_header(writer: &mut W, name: &str) -> Result<(), io::Error> { + let header = r#"#![deny(warnings)] +#![no_implicit_prelude] + +extern crate tracing; + +// We call all macros in this module with `no_implicit_prelude` to ensure they do not depend on the standard prelude. +#[cfg(target_arch = "wasm32")] +extern crate wasm_bindgen_test; + +struct Disp { + val: u64, +} + +impl ::std::fmt::Display for Disp { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::std::write!(f, "Disp.val={val}", val = self.val) + } +} + +struct Deb { + val: u64, +} + +/// Manual implementation because otherwise `val` is unused. +impl ::std::fmt::Debug for Deb { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + f.debug_struct("Deb").field("val", &self.val).finish() + } +} + +struct Sub { + field: u64, +}"#; + + writeln!( + writer, + "//! DO NOT EDIT! This file is generated by `xtask`.\n//! Contains tests for the `{name}!` macro." + )?; + writeln!(writer, "{}", header) +} + +fn emit_macro_test_file( + writer: &mut W, + invocation: &MacroInvocation, + directives: &Directives, +) -> Result<(), io::Error> { + let braces_fmt_args = [ + (FieldBraces::None, FmtArgs::None), + (FieldBraces::None, FmtArgs::Fmt), + (FieldBraces::Braces, FmtArgs::Fmt), + (FieldBraces::None, FmtArgs::WithArgs), + (FieldBraces::Braces, FmtArgs::WithArgs), + ]; + + writeln!( + writer, + r#"#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[test] +fn {name}() {{ + const CONST_VAR: &str = "const-field"; + + let var = true; + let sub = Sub {{ field: 4, }}; + let disp = Disp {{ val: 61, }}; + let deb = Deb {{ val: 102, }}; +"#, + name = invocation.macro_name() + )?; + + for field in Field::all() { + for (field_braces, fmt_args) in &braces_fmt_args { + for surrounding in SurroundingFields::all() { + let emit_macro = EmitMacro { + invocation, + directives, + field_braces, + surrounding, + fmt_args, + field, + }; + writeln!(writer, "{emit_macro} // DEBUG:{emit_macro:?}")?; + //emit_invocations(writer, invocation, directives, field_braces, surrounding, fmt_args, field)?; + } + } + } + writeln!(writer, "}}")?; + + Ok(()) +} + +/// This struct emits the macro code. +/// +/// It contains all the combined elements for a single invocation. +struct EmitMacro<'a> { + /// The macro being called + /// + /// For example, `event!` or `trace!` + invocation: &'a MacroInvocation, + /// The directives, such as `name:` + directives: &'a Directives, + /// Whether the fields have braces around them + field_braces: &'a FieldBraces, + /// Other fields before or after the field we're focussed on + surrounding: &'a SurroundingFields, + /// A message text which can also have format arguments + fmt_args: &'a FmtArgs, + /// The field we are focussed on + field: &'a Field, +} + +impl<'a> fmt::Display for EmitMacro<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + " tracing::{name}!({directives}{level}{lb}{preceding}{field}{following}{rb}{fmt_args});", + name = self.invocation.macro_name(), + directives = self.directives, + level = self.invocation.level(), + lb = self.field_braces.lb(), + preceding = self.surrounding.preceding(), + field = self.field, + following = self.surrounding.following(), + rb = self.field_braces.rb(), + fmt_args = self.fmt_args, + ) + } +} + +impl<'a> fmt::Debug for EmitMacro<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{invocation:?},{directives:?},{field_braces:?},{surrounding:?},{fmt_args:?},{field:?}", + invocation = self.invocation, + directives = self.directives, + field_braces = self.field_braces, + surrounding = self.surrounding, + fmt_args = self.fmt_args, + field = self.field, + ) + } +} + +/// The macro being called +/// +/// For example, `event!` or `trace!` +enum MacroInvocation { + /// `event!` macro (with level INFO) + Event, + /// `trace!` macro + Trace, + /// `debug!` macro + Debug, + /// `info!` macro + Info, + /// `warn!` macro + Warn, + /// `error!` macro + Error, +} + +impl MacroInvocation { + fn macro_name(&self) -> &'static str { + match self { + Self::Event => "event", + Self::Trace => "trace", + Self::Debug => "debug", + Self::Info => "info", + Self::Warn => "warn", + Self::Error => "error", + } + } + + fn level(&self) -> &'static str { + match self { + Self::Event => "tracing::Level::INFO, ", + _ => "", + } + } + + const fn all() -> &'static [MacroInvocation; 6] { + const ALL: [MacroInvocation; 6] = [ + MacroInvocation::Event, + MacroInvocation::Trace, + MacroInvocation::Debug, + MacroInvocation::Info, + MacroInvocation::Warn, + MacroInvocation::Error, + ]; + + &ALL + } +} + +impl fmt::Debug for MacroInvocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.macro_name()) + } +} + +/// The directives, such as `name:` +enum Directives { + /// No directives + None, + /// `target:` directive + Target, + /// `parent:` directive (setting parent to `None`) + Parent, + /// `name:` directive + Name, + /// `name:` and `target:` directives together + NameTarget, + /// `name:` and `parent:` directives together (setting parent to `None`) + NameParent, + /// `target:` and `parent:` directives together (setting parent to `None`) + TargetParent, + /// `name:`, `target:`, and `parent:` directives together (setting parent to `None`) + NameTargetParent, +} + +impl Directives { + const fn file_suffix(&self) -> &str { + match self { + Self::None => "", + Self::Name => "_n", + Self::Target => "_t", + Self::Parent => "_p", + Self::NameTarget => "_nt", + Self::NameParent => "_np", + Self::TargetParent => "_tp", + Self::NameTargetParent => "_ntp", + } + } + + const fn all() -> &'static [Self; 8] { + const ALL: [Directives; 8] = [ + Directives::None, + Directives::Name, + Directives::Target, + Directives::Parent, + Directives::NameTarget, + Directives::NameParent, + Directives::TargetParent, + Directives::NameTargetParent, + ]; + + &ALL + } +} + +impl fmt::Display for Directives { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::None => "", + Self::Name => "name: \"mog\", ", + Self::Target => "target: \"my::module\", ", + Self::Parent => "parent: ::core::option::Option::None, ", + Self::NameTarget => "name: \"mog\", target: \"my::module\", ", + Self::NameParent => "name: \"mog\", parent: ::core::option::Option::None, ", + Self::TargetParent => + "target: \"my::module\", parent: ::core::option::Option::None, ", + Self::NameTargetParent => + "name: \"mog\", target: \"my::module\", parent: ::core::option::Option::None, ", + } + ) + } +} + +impl fmt::Debug for Directives { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::None => "-", + Self::Name => "n", + Self::Target => "t", + Self::Parent => "p", + Self::NameTarget => "nt", + Self::NameParent => "np", + Self::TargetParent => "tp", + Self::NameTargetParent => "ntp", + } + ) + } +} + +/// Whether the fields have braces around them +enum FieldBraces { + /// No braces around fields + None, + /// Braces `{ .. }` around fields. Only applicable when there is a message + /// at the end. + Braces, +} + +impl FieldBraces { + fn lb(&self) -> &'static str { + match self { + Self::None => "", + Self::Braces => "{ ", + } + } + + fn rb(&self) -> &'static str { + match self { + Self::None => "", + Self::Braces => " }", + } + } +} + +impl fmt::Debug for FieldBraces { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "-"), + Self::Braces => write!(f, "{{}}"), + } + } +} + +/// Other fields before or after the field we're focussed on +enum SurroundingFields { + /// Single field + None, + /// Field preceding the interesting field. + Preceding, + /// Field following the interesting field. + Following, + /// Fields preceding and following the interesting field. + PrecedingFollowing, +} + +impl SurroundingFields { + fn preceding(&self) -> &'static str { + match self { + Self::None | Self::Following => "", + Self::Preceding | Self::PrecedingFollowing => "foo = true, ", + } + } + + fn following(&self) -> &'static str { + match self { + Self::None | Self::Preceding => "", + Self::Following | Self::PrecedingFollowing => ", qux = 3", + } + } + + fn all() -> &'static [SurroundingFields] { + const ALL: [SurroundingFields; 4] = [ + SurroundingFields::None, + SurroundingFields::Preceding, + SurroundingFields::Following, + SurroundingFields::PrecedingFollowing, + ]; + + &ALL + } +} + +impl fmt::Debug for SurroundingFields { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "-"), + Self::Preceding => write!(f, "p"), + Self::Following => write!(f, "f"), + Self::PrecedingFollowing => write!(f, "pf"), + } + } +} + +/// A message text which can also have format arguments +#[derive(Clone, Copy)] +enum FmtArgs { + /// No message (macro only has fields) + None, + /// Message without format arguments + Fmt, + /// Message with format arguments after it + WithArgs, +} + +impl fmt::Display for FmtArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, ""), + Self::Fmt => write!(f, ", \"msg without args\""), + Self::WithArgs => write!(f, ", \"msg with arg: {{:?}}\", 56"), + } + } +} + +impl fmt::Debug for FmtArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "-"), + Self::Fmt => write!(f, "m"), + Self::WithArgs => write!(f, "ma"), + } + } +} + +/// The field we are focussed on +#[derive(Clone, Copy)] +enum Field { + /// A labeled field, for example `name = value` + Labeled { label: Label, value: Value }, + /// A labelless field where the field name comes from the value for example `foo` or `%bar` + Labelless { value: Labelless }, +} + +impl fmt::Display for Field { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Labeled { label, value } => write!(f, "{label} = {value}"), + Self::Labelless { value } => write!(f, "{value}"), + } + } +} + +impl fmt::Debug for Field { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Labeled { label, value } => write!(f, "{label:?},{value:?}"), + Self::Labelless { value } => write!(f, "-,{value:?}"), + } + } +} + +impl Field { + fn all() -> &'static Vec { + static ALL: OnceLock> = OnceLock::new(); + ALL.get_or_init(|| { + let mut all_fields = Vec::new(); + + for label in Label::all() { + for value in Value::all() { + all_fields.push(Field::Labeled { + label: *label, + value: *value, + }); + } + } + for value in Labelless::all() { + all_fields.push(Field::Labelless { value: *value }); + } + + all_fields + }) + } +} + +/// A field label, this is paired with a [`Value`] +#[derive(Clone, Copy)] +enum Label { + /// Label is an identifier, e.g. `status` + Ident, + /// Label is a dotted identifier, e.g. `order.id` + DottedIdent, + /// Label is a literal, e.g. `"foo"` + Literal, + /// Label is a const, e.g. `{ CONST_VAR }` + ConstVar, + /// Label is a raw identifier, e.g. `r#type` + RawIdent, +} + +impl fmt::Display for Label { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Ident => write!(f, "ident"), + Self::DottedIdent => write!(f, "dotted.ident"), + Self::Literal => write!(f, "\"literal\""), + Self::ConstVar => write!(f, "{{ CONST_VAR }}"), + Self::RawIdent => write!(f, "r#type"), + } + } +} + +impl fmt::Debug for Label { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", format!("{self}")) + } +} + +impl Label { + const fn all() -> &'static [Label; 5] { + const ALL: [Label; 5] = [ + Label::Ident, + Label::DottedIdent, + Label::Literal, + Label::ConstVar, + Label::RawIdent, + ]; + + &ALL + } +} + +/// A labelless field, where the field name is derived from the value +#[derive(Clone, Copy)] +enum Labelless { + /// Field is a variable name, e.g. `foo` + Var, + /// Field accesses a sub-field, e.g. `data.status` + SubField, + /// Field is a variable treated as `impl Display` via the sigil, e.g. `%foo` + DisplaySigil, + /// Field is a variable treated as `impl Debug` via the sigil, e.g. `?foo` + DebugSigil, + /// Field is a sub-field treated as `impl Display` via the sigil, e.g. `%data.status` + DisplaySigilSubField, + /// Field is a sub-field treated as `impl Debug` via the sigil, e.g. `?data.status` + DebugSigilSubField, +} + +impl fmt::Display for Labelless { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Var => write!(f, "var"), + Self::SubField => write!(f, "sub.field"), + Self::DisplaySigil => write!(f, "%disp"), + Self::DebugSigil => write!(f, "?deb"), + Self::DisplaySigilSubField => write!(f, "%sub.field"), + Self::DebugSigilSubField => write!(f, "?sub.field"), + } + } +} + +impl fmt::Debug for Labelless { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", format!("{self}")) + } +} + +impl Labelless { + const fn all() -> &'static [Labelless; 6] { + const ALL: [Labelless; 6] = [ + Labelless::Var, + Labelless::SubField, + Labelless::DisplaySigil, + Labelless::DebugSigil, + Labelless::DisplaySigilSubField, + Labelless::DebugSigilSubField, + ]; + + &ALL + } +} + +/// The value for a field with a label +#[derive(Clone, Copy)] +enum Value { + /// Value is a literal int, e.g. `3` + LiteralInt, + /// Value is a literal bool, e.g. `true` + LiteralBool, + /// Value is an integer treated as `impl Display` via the sigil, e.g. `%3` + DisplaySigilInt, + /// Value is an integer treated as `impl Debug` via the sigil, e.g. `?3` + DebugSigilInt, + /// Value is a variable treated as `impl Display` via the sigil, e.g. `%foo` + DisplaySigilVar, + /// Value is a variable treated as `impl Debug` via the sigil, e.g. `?foo` + DebugSigilVar, + /// Value is accessing a subfield, treated as `impl Display` via the sigil, e.g. `%order.id` + DisplaySigilSubField, + /// Value is accessing a subfield, treated as `impl Debug` via the sigil, e.g. `?order.id` + DebugSigilSubField, + /// Value is a variable treated as `impl Display` via the use of the bare function, e.g. + /// `display(foo)` + BareDisplayFn, + /// Value is a variable treated as `impl Debug` via the use of the bare function, e.g. + /// `debug(foo)` + BareDebugFn, + /// Value is tracing's Empty field, e.g. `tracing::field::Empty` + Empty, +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::LiteralInt => write!(f, "3"), + Self::LiteralBool => write!(f, "false"), + Self::DebugSigilInt => write!(f, "?3"), + Self::DisplaySigilInt => write!(f, "%3"), + Self::DebugSigilVar => write!(f, "?deb"), + Self::DisplaySigilVar => write!(f, "%disp"), + Self::DebugSigilSubField => write!(f, "?sub.field"), + Self::DisplaySigilSubField => write!(f, "%sub.field"), + Self::BareDebugFn => write!(f, "debug(&deb)"), + Self::BareDisplayFn => write!(f, "display(&disp)"), + Self::Empty => write!(f, "tracing::field::Empty"), + } + } +} + +impl fmt::Debug for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", format!("{self}")) + } +} + +impl Value { + const fn all() -> &'static [Value; 11] { + const ALL: [Value; 11] = [ + Value::LiteralInt, + Value::LiteralBool, + Value::DebugSigilInt, + Value::DisplaySigilInt, + Value::DebugSigilVar, + Value::DisplaySigilVar, + Value::DebugSigilSubField, + Value::DisplaySigilSubField, + Value::BareDebugFn, + Value::BareDisplayFn, + Value::Empty, + ]; + + &ALL + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000000..794ae85352 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,33 @@ +use std::error; + +use clap::{Parser, Subcommand}; + +mod macro_tests; + +use macro_tests::gen_macro_tests; + +#[derive(Debug, Parser)] +struct Args { + #[clap(subcommand)] + cmd: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Generate tests for `tracing` macros. + /// + /// This will be placed in the dedicated project `tracing/test-macros`. + GenMacroTests, +} + +impl Command { + fn run(&self) -> Result<(), Box> { + match self { + Self::GenMacroTests => gen_macro_tests(), + } + } +} + +fn main() -> Result<(), Box> { + Args::parse().cmd.run() +}