Skip to content

Commit 385e882

Browse files
committed
editoast: derive: setup insta to snapshot test macros
Allows to easily test macro expansion, track changes, improves debuggability and ease review. HashMaps have been eliminated from `Model`'s internals to have a deterministic expansion. Otherwise snapshots would be flaky. Signed-off-by: Leo Valais <[email protected]>
1 parent 02ccfc8 commit 385e882

13 files changed

+1447
-91
lines changed

.github/workflows/build.yml

+2
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ jobs:
589589
docker load --input ./osrd-editoast-test.tar
590590
591591
- name: Execute tests within container
592+
# snapshot testing library `insta` requires CI=true
592593
run: |
593594
docker run --rm --net=host \
594595
-v $PWD/docker/init_db.sql:/init_db.sql \
@@ -597,6 +598,7 @@ jobs:
597598
598599
docker run --name=editoast-test --net=host -v $PWD/output:/output \
599600
-e DATABASE_URL="postgres://osrd:password@localhost:5432/osrd" \
601+
-e CI="true" \
600602
${{ fromJSON(needs.build.outputs.stable_tags).editoast-test }} \
601603
/bin/sh -c "diesel migration run --locked-schema && RUST_BACKTRACE=1 cargo test --workspace --verbose -- --test-threads=4 && grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore "/*" -o /output/lcov.info"
602604

editoast/Cargo.lock

+48-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editoast/editoast_derive/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ edition.workspace = true
88

99
[dependencies]
1010
darling = "0.20"
11+
insta = "1.41"
1112
paste.workspace = true
13+
prettyplease = "0.2"
1214
proc-macro2 = "1.0"
1315
quote = "1.0"
1416
serde_json.workspace = true

editoast/editoast_derive/src/error.rs

+23
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,26 @@ fn extract_type(ty: &syn::Type) -> Option<String> {
235235
_ => None,
236236
}
237237
}
238+
239+
#[cfg(test)]
240+
mod tests {
241+
use super::*;
242+
243+
#[test]
244+
fn test_construction() {
245+
crate::assert_macro_expansion!(
246+
expand_editoast_error,
247+
syn::parse_quote! {
248+
#[derive(EditoastError)]
249+
#[editoast_error(base_id = "infra", default_status = 500)]
250+
pub enum InfraApiError {
251+
#[editoast_error(status = 404, no_context)]
252+
NotFound { infra_id: i64 },
253+
#[editoast_error(status = 400)]
254+
BadRequest { message: String },
255+
InternalError,
256+
}
257+
}
258+
);
259+
}
260+
}

editoast/editoast_derive/src/lib.rs

+34
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,37 @@ pub fn model(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
237237
.unwrap_or_else(darling::Error::write_errors)
238238
.into()
239239
}
240+
241+
#[cfg(test)]
242+
pub(crate) fn pretty_tokens(tokens: &proc_macro2::TokenStream) -> String {
243+
let file = syn::parse_file(tokens.to_string().as_str()).unwrap();
244+
prettyplease::unparse(&file)
245+
}
246+
247+
// Extra allows because of some false negative lints, not sure why.
248+
// Maybe because editoast_derive is a proc-macro crate?
249+
250+
#[cfg(test)]
251+
#[allow(unused_macros)]
252+
macro_rules! assert_macro_expansion {
253+
($expansion:path, $derive_input:expr) => {
254+
let input: syn::DeriveInput = $derive_input;
255+
let source = crate::pretty_tokens(&<syn::DeriveInput as quote::ToTokens>::to_token_stream(&input));
256+
let expansion = $expansion(&input).expect("macro should expand faultlessly");
257+
let expected = crate::pretty_tokens(&expansion);
258+
259+
// HACK: sadly insta doesn't let us print multiline strings in the scnapshot description
260+
// or info sections. So we have to incorporate the source input into the snapshot content
261+
// in order to keep it pretty printed and next to its expansion.
262+
insta::with_settings!({
263+
omit_expression => true,
264+
}, {
265+
let sep = "-".repeat(77);
266+
insta::assert_snapshot!(format!("// Source\n// {sep}\n\n{source}\n// Macro expansion\n// {sep}\n\n{expected}"));
267+
});
268+
};
269+
}
270+
271+
#[cfg(test)]
272+
#[allow(unused_imports)]
273+
use assert_macro_expansion;

editoast/editoast_derive/src/model.rs

+23-17
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,27 @@ pub fn model(input: &DeriveInput) -> Result<TokenStream> {
8787
}
8888

8989
#[cfg(test)]
90-
#[test]
91-
fn test_construction() {
92-
let input = syn::parse_quote! {
93-
#[derive(Clone, Model)]
94-
#[model(table = editoast_models::tables::osrd_infra_document)]
95-
#[model(row(type_name = "DocumentRow", derive(Debug)))]
96-
#[model(changeset(type_name = "DocumentChangeset", public, derive(Debug)))] // fields are public
97-
#[model(gen(ops = crud, batch_ops = crud, list))]
98-
struct Document {
99-
#[model(column = "id", preferred, primary)]
100-
id_: i64,
101-
#[model(identifier, json)]
102-
content_type: String,
103-
data: Vec<u8>,
104-
}
105-
};
106-
let _ = model(&input).expect("should generate");
90+
mod tests {
91+
use super::*;
92+
93+
#[test]
94+
fn test_construction() {
95+
crate::assert_macro_expansion!(
96+
model,
97+
syn::parse_quote! {
98+
#[derive(Clone, Model)]
99+
#[model(table = editoast_models::tables::osrd_infra_document)]
100+
#[model(row(type_name = "DocumentRow", derive(Debug)))]
101+
#[model(changeset(type_name = "DocumentChangeset", public, derive(Debug)))] // fields are public
102+
#[model(gen(ops = crud, batch_ops = crud, list))]
103+
struct Document {
104+
#[model(column = "id", preferred, primary)]
105+
id_: i64,
106+
#[model(identifier, json)]
107+
content_type: String,
108+
data: Vec<u8>,
109+
}
110+
}
111+
);
112+
}
107113
}

editoast/editoast_derive/src/model/config.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
collections::HashSet,
2+
collections::BTreeSet,
33
ops::{Deref, DerefMut},
44
};
55

@@ -19,9 +19,9 @@ pub(crate) struct ModelConfig {
1919
pub(crate) fields: Fields,
2020
pub(crate) row: GeneratedTypeArgs,
2121
pub(crate) changeset: GeneratedTypeArgs,
22-
pub(crate) identifiers: HashSet<Identifier>, // identifiers ⊆ fields
23-
pub(crate) preferred_identifier: Identifier, // preferred_identifier ∈ identifiers
24-
pub(crate) primary_identifier: Identifier, // primary_identifier ∈ identifiers
22+
pub(crate) identifiers: BTreeSet<Identifier>, // identifiers ⊆ fields
23+
pub(crate) preferred_identifier: Identifier, // preferred_identifier ∈ identifiers
24+
pub(crate) primary_identifier: Identifier, // primary_identifier ∈ identifiers
2525
pub(crate) impl_plan: ImplPlan,
2626
}
2727

editoast/editoast_derive/src/model/identifier.rs

+24
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,30 @@ impl darling::FromMeta for RawIdentifier {
8585
}
8686
}
8787

88+
impl PartialOrd for RawIdentifier {
89+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
90+
Some(self.cmp(other))
91+
}
92+
}
93+
94+
impl Ord for RawIdentifier {
95+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
96+
self.get_idents().cmp(&other.get_idents())
97+
}
98+
}
99+
100+
impl PartialOrd for Identifier {
101+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
102+
Some(self.raw.cmp(&other.raw))
103+
}
104+
}
105+
106+
impl Ord for Identifier {
107+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
108+
self.raw.cmp(&other.raw)
109+
}
110+
}
111+
88112
fn extract_ident_of_path(path: &syn::Path) -> darling::Result<syn::Ident> {
89113
let mut segments = path.segments.iter();
90114
let first = segments.next().unwrap();

0 commit comments

Comments
 (0)