use rrgen::{Error, RRgen};
use serde_json::json;
const MIGRATOR: &str = "\
pub struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_users::Migration),
// inject-above (do not remove this comment)
]
}
}
";
const MIGRATOR_WITHOUT_ANCHOR: &str = "\
pub struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_users::Migration)]
}
}
";
fn template(placement: &str) -> String {
format!(
"to: migration/src/m20240101_000000_posts.rs\n\
injections:\n\
- into: migration/src/lib.rs\n \
{placement}\n \
content: \" Box::new(m20240101_000000_posts::Migration),\"\n\
---\n\
// the migration body\n"
)
}
fn tree(migrator: &str) -> tree_fs::Tree {
tree_fs::TreeBuilder::default()
.drop(true)
.add("migration/src/lib.rs", migrator)
.create()
.expect("create tree")
}
#[test]
fn a_missing_anchor_is_an_error_for_every_placement() {
for placement in [
r#"before: "inject-above""#,
r#"before_last: "inject-above""#,
r#"after: "inject-above""#,
r#"after_last: "inject-above""#,
] {
let tree = tree(MIGRATOR_WITHOUT_ANCHOR);
let result = RRgen::with_working_dir(&tree.root).generate(&template(placement), &json!({}));
assert!(
matches!(result, Err(Error::InjectionAnchorNotFound { .. })),
"`{placement}` matched nothing and did not report it: {result:?}"
);
}
}
#[test]
fn the_error_names_the_file_the_pattern_and_the_dropped_content() {
let tree = tree(MIGRATOR_WITHOUT_ANCHOR);
let err = RRgen::with_working_dir(&tree.root)
.generate(&template(r#"before: "inject-above""#), &json!({}))
.expect_err("a missing anchor is an error");
let message = err.to_string();
for expected in [
"migration/src/lib.rs",
"before",
"inject-above",
"Box::new(m20240101_000000_posts::Migration),",
] {
assert!(
message.contains(expected),
"the error does not mention `{expected}`:\n{message}"
);
}
}
#[test]
fn a_failed_injection_writes_nothing() {
let tree = tree(MIGRATOR_WITHOUT_ANCHOR);
let generated = tree.root.join("migration/src/m20240101_000000_posts.rs");
RRgen::with_working_dir(&tree.root)
.generate(&template(r#"before: "inject-above""#), &json!({}))
.expect_err("a missing anchor is an error");
assert!(
!generated.exists(),
"the generated file was left behind by a generation that failed"
);
assert_eq!(
std::fs::read_to_string(tree.root.join("migration/src/lib.rs")).unwrap(),
MIGRATOR_WITHOUT_ANCHOR,
"the injection target was rewritten by a generation that failed"
);
}
#[test]
fn an_anchor_that_matches_injects_and_keeps_the_trailing_newline() {
let tree = tree(MIGRATOR);
RRgen::with_working_dir(&tree.root)
.generate(&template(r#"before: "inject-above""#), &json!({}))
.expect("the anchor is present");
let migrator = std::fs::read_to_string(tree.root.join("migration/src/lib.rs")).unwrap();
assert!(
migrator.contains("Box::new(m20240101_000000_posts::Migration),"),
"the migration was not registered:\n{migrator}"
);
assert!(
migrator.ends_with("}\n"),
"the injection stripped the file's trailing newline, which shows up in \
the user's diff as `\\ No newline at end of file`:\n{migrator:?}"
);
}
#[test]
fn injections_into_the_same_file_compose() {
let tree = tree(MIGRATOR);
let template = "to: out.txt\n\
injections:\n\
- into: migration/src/lib.rs\n \
before: \"inject-above\"\n \
content: \" FIRST\"\n\
- into: migration/src/lib.rs\n \
before: \"inject-above\"\n \
content: \" SECOND\"\n\
---\n\
body\n";
RRgen::with_working_dir(&tree.root)
.generate(template, &json!({}))
.expect("both anchors are present");
let migrator = std::fs::read_to_string(tree.root.join("migration/src/lib.rs")).unwrap();
assert!(
migrator.contains("FIRST") && migrator.contains("SECOND"),
"one injection overwrote the other:\n{migrator}"
);
}
#[test]
fn an_injection_with_no_placement_is_an_error() {
let tree = tree(MIGRATOR);
let template = "to: out.txt\n\
injections:\n\
- into: migration/src/lib.rs\n \
content: \"nowhere in particular\"\n\
---\n\
body\n";
let result = RRgen::with_working_dir(&tree.root).generate(template, &json!({}));
assert!(
matches!(result, Err(Error::InjectionHasNoPlacement { .. })),
"expected a placement error, got {result:?}"
);
}
#[test]
fn a_removal_that_matches_nothing_is_not_an_error() {
let tree = tree(MIGRATOR);
let template = "to: out.txt\n\
injections:\n\
- into: migration/src/lib.rs\n \
remove_lines: \"this line is not in the file\"\n \
content: \"\"\n\
---\n\
body\n";
RRgen::with_working_dir(&tree.root)
.generate(template, &json!({}))
.expect("removing nothing is not a failure");
assert_eq!(
std::fs::read_to_string(tree.root.join("migration/src/lib.rs")).unwrap(),
MIGRATOR,
"a removal that matched nothing still rewrote the file"
);
}
#[test]
fn skip_glob_resolves_against_the_working_dir() {
let tree = tree(MIGRATOR);
let generated = tree.root.join("migration/src/m20240101_000000_posts.rs");
let template = |body: &str| {
format!(
"to: migration/src/m20240101_000000_posts.rs\n\
skip_glob: \"migration/src/m????????_??????_posts.rs\"\n\
---\n\
{body}\n"
)
};
let rrgen = RRgen::with_working_dir(&tree.root);
rrgen
.generate(&template("the first body"), &json!({}))
.expect("nothing matches the glob yet");
let second = rrgen.generate(&template("a body that must not be written"), &json!({}));
assert!(
matches!(second, Ok(rrgen::GenResult::Skipped)),
"skip_glob did not fire against the working dir: {second:?}"
);
assert_eq!(
std::fs::read_to_string(&generated).unwrap(),
"the first body\n",
"the skipped generation wrote anyway"
);
}