Skip to content

test(stackable-versioned): Combine snapshot and compile tests #1041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 0 additions & 62 deletions crates/stackable-versioned-macros/fixtures/README.md

This file was deleted.

10 changes: 5 additions & 5 deletions crates/stackable-versioned-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,16 +922,16 @@ where
}

#[cfg(test)]
mod test {
mod snapshot_tests {
use insta::{assert_snapshot, glob};

use super::*;

#[test]
fn default_snapshots() {
fn default() {
let _settings_guard = test_utils::set_snapshot_path().bind_to_scope();

glob!("../fixtures/inputs/default", "*.rs", |path| {
glob!("../tests/inputs/default/pass", "*.rs", |path| {
let formatted = test_utils::expand_from_file(path)
.inspect_err(|err| eprintln!("{err}"))
.unwrap();
Expand All @@ -941,10 +941,10 @@ mod test {

#[cfg(feature = "k8s")]
#[test]
fn k8s_snapshots() {
fn k8s() {
let _settings_guard = test_utils::set_snapshot_path().bind_to_scope();

glob!("../fixtures/inputs/k8s", "*.rs", |path| {
glob!("../tests/inputs/k8s/pass", "*.rs", |path| {
let formatted = test_utils::expand_from_file(path)
.inspect_err(|err| eprintln!("{err}"))
.unwrap();
Expand Down
16 changes: 11 additions & 5 deletions crates/stackable-versioned-macros/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::{
use insta::Settings;
use proc_macro2::TokenStream;
use regex::Regex;
use snafu::{OptionExt, ResultExt, Snafu};
use snafu::{NoneError, OptionExt, ResultExt, Snafu};
use syn::Item;

use crate::versioned_impl;
Expand All @@ -24,8 +24,8 @@ pub(crate) enum Error {
#[snafu(display("failed to read input file"))]
ReadFile { source: std::io::Error },

#[snafu(display("failed to find delimiter"))]
MissingDelimiter,
#[snafu(display("failed to find delimiters"))]
MissingDelimiters,

#[snafu(display("failed to find regex match group"))]
MissingRegexMatchGroup,
Expand All @@ -51,7 +51,13 @@ pub(crate) fn expand_from_file(path: &Path) -> Result<String, Error> {
}

fn prepare_from_string(input: String) -> Result<(TokenStream, Item), Error> {
let (attrs, input) = input.split_once(DELIMITER).context(MissingDelimiterSnafu)?;
let parts: [&str; 4] = input
.split(DELIMITER)
.collect::<Vec<_>>()
.try_into()
.map_err(|_| NoneError)
.context(MissingDelimitersSnafu)?;
let [_, attrs, input, _] = parts;

let attrs = REGEX
.captures(attrs)
Expand All @@ -70,7 +76,7 @@ fn prepare_from_string(input: String) -> Result<(TokenStream, Item), Error> {
pub(crate) fn set_snapshot_path() -> Settings {
let dir = std::env::var("CARGO_MANIFEST_DIR").expect("env var CARGO_MANIFEST_DIR must be set");
let mut settings = Settings::clone_current();
settings.set_snapshot_path(PathBuf::from(dir).join("fixtures/snapshots"));
settings.set_snapshot_path(PathBuf::from(dir).join("tests/snapshots"));

settings
}
94 changes: 87 additions & 7 deletions crates/stackable-versioned-macros/tests/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,97 @@
# Compile-Fail Testing
# Testing the `#[versioned]` macro

> [!NOTE]
> Also see the snapshot tests, described [here](../fixtures/README.md).

This type of testing is part of UI testing. These tests assert two things: First, the code should
**not** compile and secondly should also produce the expected rustc (compiler) error message. For
this type of testing, we use the [`trybuild`][trybuild] crate.
This folder contains both snapshot and compile (trybuild) tests. Both types of tests use the same
set of input files to both ensure the macro generates the expected code and either compiles or
produces the expected compile error.

Tests are currently separated into two folders: `default` and `k8s`. The default test cases don't
require any additional features to be activated. The Kubernetes specific tests require the `k8s`
feature to be enabled. These tests can be run with `cargo test --all-features`.

## Snapshot Testing

> [!NOTE]
> Please have `rust-src` installed, e.g. using `rustup component add rust-src`.
>
> Also see the compile-fail tests, described [here](#compile-fail-testing).

Snapshot testing is done using the [insta] crate. It provides a [CLI tool][insta-cli] calle
`cargo-insta` and a [VS Code extension][insta-ext].

Test inputs and snapshots of the expected output are located in the `inputs` and `snapshots` folder
respectively. Each Rust attribute macro expects two inputs as a token stream:

> The first TokenStream is the delimited token tree following the attribute’s name, not including
> the outer delimiters. If the attribute is written as a bare attribute name, the attribute
> TokenStream is empty. The second TokenStream is the rest of the item including other attributes on
> the item.
>
> _(Taken from the [Rust reference][rust-ref])_

Because of that, a special delimiter is used in the input files which separates different sections
of the input file while still enabling developers to write valid Rust code. The delimiter is
`// ---\n`. Most of the inner workings are located in [this file](../src/test_utils.rs).

```rust
use stackable_versioned::versioned;
// --- <- See here!
#[versioned(
version(name = "v1alpha1"),
version(name = "v1beta1"),
version(name = "v1")
)]
// --- <- See here!
pub(crate) struct Foo {
#[versioned(
changed(since = "v1beta1", from_name = "jjj", from_type = "u8"),
changed(since = "v1", from_type = "u16"),
)]
bar: usize,
baz: bool,
}
// --- <- See here!
fn main() {}

// Rest of code ...
```

Input files must include **three** separators which produce **four** distinct sections:

- Imports, like `stackable_versioned::versioned`
- The attribute macro
- The item the macro is applied to
- The rest of the code, like the `main` function

### Recommended Workflow

First, add new input files (which automatically get picked up by `insta`) to the `inputs`
folder. Make sure the delimiter is placed correctly between the different sections. Doc comments on
the container have to be placed after the delimiter. Next, generate the snapshot files (initially
not accepted) by running

```shell
cargo insta test -p stackable-versioned-macros --all-features
```

This command will place the new snapshot files (with a `.new` extension) in the `snapshots` folder.
These new snapshot files must not appear on `main`, but can be shared on branches for collaboration.
To review them, run the `cargo insta review` command, then accept or fix the snapshots. Once all are
accepted (ie: no `.new` files remaining), check in the files.

## Compile-Fail Testing

> [!NOTE]
> Also see the snapshot tests, described [here](#snapshot-testing).

This type of testing is part of UI testing. These tests assert two things: First, some code should
compile without errors and secondly other code should produce the expected rustc (compiler) error
message. For this type of testing, we use the [`trybuild`][trybuild] crate.

Further information about the workflow are described [here][workflow].

[rust-ref]: https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros
[workflow]: https://docs.rs/trybuild/latest/trybuild/#workflow
[trybuild]: https://docs.rs/trybuild/latest/trybuild/
[insta-ext]: https://insta.rs/docs/vscode/
[insta-cli]: https://insta.rs/docs/cli/
[insta]: https://insta.rs/
23 changes: 0 additions & 23 deletions crates/stackable-versioned-macros/tests/default/pass/added.rs

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use stackable_versioned_macros::versioned;
// ---
#[versioned(
version(name = "v1alpha1"),
version(name = "v1alpha2"),
version(name = "v1beta1"),
version(name = "v1")
)]
// ---
struct Foo {
username: String,

#[versioned(added(since = "v1alpha2", default = default_first_name))]
first_name: String,

#[versioned(added(since = "v1beta1"))]
last_name: String,
}
// ---
fn main() {}

fn default_first_name() -> String {
"foo".into()
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use stackable_versioned::versioned;
// ---
#[versioned(
version(name = "v1alpha1"),
version(
Expand Down Expand Up @@ -36,3 +38,5 @@ enum Foo {
#[versioned(changed(since = "v1", from_name = "Qaax"))]
Quux,
}
// ---
fn main() {}
Loading
Loading