From af510e921004e75d41e6629961ddd527a4fbd16c Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Fri, 19 Jun 2026 14:53:14 +0800 Subject: [PATCH] test: add quote assertion helpers --- docs/src/sigil_quote.md | 40 +++++++++++++++++++++ src/lib.rs | 2 ++ src/testing.rs | 61 +++++++++++++++++++++++++++++++ tests/assert_quote_tests.rs | 71 +++++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 src/testing.rs create mode 100644 tests/assert_quote_tests.rs diff --git a/docs/src/sigil_quote.md b/docs/src/sigil_quote.md index d16f812..5a9e887 100644 --- a/docs/src/sigil_quote.md +++ b/docs/src/sigil_quote.md @@ -30,6 +30,46 @@ let block = sigil_quote!(TypeScript { The macro takes a language type followed by a braced body of target-language code. It returns `Result`. +## Testing Quoted Fragments + +Use `assert_quote!` for small exact snapshots of inline quoted code: + +```rust +# extern crate sigil_stitch; +# use sigil_stitch::{assert_quote, prelude::*}; +# use sigil_stitch::lang::typescript::TypeScript; +# fn main() { +assert_quote!(TypeScript, { + const x = 1; +}, "const x = 1;\n"); +# } +``` + +Use `assert_rendered!` when the block is built separately, needs imports, or uses +a configured language instance: + +```rust +# extern crate sigil_stitch; +# use sigil_stitch::{assert_rendered, prelude::*}; +# use sigil_stitch::lang::python::Python; +# use sigil_stitch::lang::config::QuoteStyle; +# fn main() { +let block = sigil_quote!(Python { + print($S("hi")) +}).unwrap(); + +assert_rendered!( + Python::new().with_quote_style(QuoteStyle::Double), + block, + "print(\"hi\")\n", +); +# } +``` + +Both helpers render through `FileSpec`, so import collection and language-specific +rendering match real files. Comparisons are exact: indentation, whitespace, and +final newlines are significant. + ## Interpolation Markers | Syntax | Specifier | Argument Type | Purpose | diff --git a/src/lib.rs b/src/lib.rs index 5efa4f6..08df79a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,8 @@ pub mod import_collector; pub mod lang; /// Structural builders (TypeSpec, FunSpec, FileSpec, etc.) that emit `CodeBlock`s. pub mod spec; +/// Exact rendered-code assertion helpers for tests. +pub mod testing; /// Type references with recursive import tracking and pretty-printing. pub mod type_name; pub(crate) mod type_name_import; diff --git a/src/testing.rs b/src/testing.rs new file mode 100644 index 0000000..262131f --- /dev/null +++ b/src/testing.rs @@ -0,0 +1,61 @@ +//! Test helpers for exact rendered-code assertions. + +use crate::code_block::CodeBlock; +use crate::error::SigilStitchError; +use crate::lang::CodeLang; +use crate::spec::file_spec::FileSpec; + +/// Render a `CodeBlock` through `FileSpec` for tests. +/// +/// This exercises the same import collection and language rendering path as a +/// real file render. The filename is synthetic and based on the language's file +/// extension. +pub fn render_block_for_test( + lang: impl CodeLang, + block: &CodeBlock, + width: usize, +) -> Result { + let filename = format!("test.{}", lang.file_extension()); + FileSpec::builder_with(&filename, lang) + .add_code(block.clone()) + .build()? + .render(width) +} + +/// Assert that a `CodeBlock` renders exactly through `FileSpec`. +/// +/// The assertion is exact: leading whitespace, trailing whitespace, and final +/// newlines are significant. +#[macro_export] +macro_rules! assert_rendered { + ($lang:expr, width = $width:expr, $block:expr, $expected:expr $(,)?) => {{ + let __sigil_block = $block; + let __sigil_actual = $crate::testing::render_block_for_test($lang, &__sigil_block, $width) + .expect("failed to render CodeBlock in assert_rendered!"); + let __sigil_expected = $expected; + assert_eq!( + __sigil_actual, __sigil_expected, + "rendered code mismatch\n\n--- expected ---\n{}\n--- actual ---\n{}", + __sigil_expected, __sigil_actual, + ); + }}; + ($lang:expr, $block:expr, $expected:expr $(,)?) => {{ + $crate::assert_rendered!($lang, width = 80, $block, $expected); + }}; +} + +/// Assert that an inline `sigil_quote!` body renders exactly. +/// +/// The language type must be in scope and provide `new()`. For configured +/// language instances, build the quote separately and use [`assert_rendered!`]. +#[macro_export] +macro_rules! assert_quote { + ($lang:ident, width = $width:expr, { $($body:tt)* }, $expected:expr $(,)?) => {{ + let __sigil_block = $crate::prelude::sigil_quote!($lang { $($body)* }) + .expect("failed to build CodeBlock in assert_quote!"); + $crate::assert_rendered!($lang::new(), width = $width, __sigil_block, $expected); + }}; + ($lang:ident, { $($body:tt)* }, $expected:expr $(,)?) => {{ + $crate::assert_quote!($lang, width = 80, { $($body)* }, $expected); + }}; +} diff --git a/tests/assert_quote_tests.rs b/tests/assert_quote_tests.rs new file mode 100644 index 0000000..5950e6b --- /dev/null +++ b/tests/assert_quote_tests.rs @@ -0,0 +1,71 @@ +use sigil_stitch::code_block::{CodeBlock, CodeFragment}; +use sigil_stitch::lang::config::QuoteStyle; +use sigil_stitch::lang::python::Python; +use sigil_stitch::lang::typescript::TypeScript; +use sigil_stitch::prelude::*; +use sigil_stitch::{assert_quote, assert_rendered}; + +#[test] +fn assert_rendered_matches_manual_codeblock() { + let block = CodeBlock::of("const x = 1", ()).unwrap(); + + assert_rendered!(TypeScript::new(), block, "const x = 1\n"); +} + +#[test] +fn assert_quote_matches_inline_quote() { + assert_quote!(TypeScript, { + const x = 1; + }, "const x = 1;\n"); +} + +#[test] +fn assert_rendered_uses_configured_language() { + let block = sigil_quote!(Python { + print($S("hi")) + }) + .unwrap(); + + assert_rendered!( + Python::new().with_quote_style(QuoteStyle::Double), + block, + "print(\"hi\")\n", + ); +} + +#[test] +fn assert_rendered_includes_imports() { + let user = TypeName::importable_type("./models", "User"); + let block = CodeBlock::of("const user: %T = getUser()", (user,)).unwrap(); + + assert_rendered!( + TypeScript::new(), + block, + "import type { User } from './models';\n\nconst user: User = getUser()\n", + ); +} + +#[test] +fn assert_quote_accepts_width() { + assert_quote!(TypeScript, width = 12, { + call($W first, $W second, $W third); + }, "call( first,\nsecond,\nthird);\n"); +} + +#[test] +fn assert_rendered_handles_code_fragment_composition() { + let fragment = CodeFragment::of("if enabled:\n%>return value%<", ()).unwrap(); + let block = sigil_quote!(Python { + def choose(enabled: bool, value: str) -> str: { + $L(fragment) + return "fallback" + } + }) + .unwrap(); + + assert_rendered!( + Python::new(), + block, + "def choose(enabled: bool, value: str) -> str:\n if enabled:\n return value\n return \"fallback\"\n\n", + ); +}