| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
use std::collections::BTreeMap; |
| 21 |
use std::fmt::Write as _; |
| 22 |
use std::path::PathBuf; |
| 23 |
|
| 24 |
|
| 25 |
const UPDATE_VAR: &str = "UPDATE_MIGRATION_CHECKSUMS"; |
| 26 |
|
| 27 |
fn manifest_path() -> PathBuf { |
| 28 |
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../migrations/sqlite/CHECKSUMS") |
| 29 |
} |
| 30 |
|
| 31 |
|
| 32 |
fn current_checksums() -> BTreeMap<i64, String> { |
| 33 |
sqlx::migrate!("../../migrations/sqlite") |
| 34 |
.iter() |
| 35 |
.map(|m| { |
| 36 |
let hex = m.checksum.iter().fold(String::new(), |mut acc, b| { |
| 37 |
let _ = write!(acc, "{b:02x}"); |
| 38 |
acc |
| 39 |
}); |
| 40 |
(m.version, hex) |
| 41 |
}) |
| 42 |
.collect() |
| 43 |
} |
| 44 |
|
| 45 |
fn render(checksums: &BTreeMap<i64, String>) -> String { |
| 46 |
let mut out = String::from( |
| 47 |
"# sha384 of each migration file, as computed by sqlx.\n\ |
| 48 |
# Regenerate: UPDATE_MIGRATION_CHECKSUMS=1 cargo test -p goingson-db-sqlite\n\ |
| 49 |
# A changed line means a shipped migration was edited. See migration_checksum_tests.rs.\n", |
| 50 |
); |
| 51 |
for (version, checksum) in checksums { |
| 52 |
let _ = writeln!(out, "{version:03} {checksum}"); |
| 53 |
} |
| 54 |
out |
| 55 |
} |
| 56 |
|
| 57 |
fn parse(raw: &str) -> BTreeMap<i64, String> { |
| 58 |
raw.lines() |
| 59 |
.map(str::trim) |
| 60 |
.filter(|line| !line.is_empty() && !line.starts_with('#')) |
| 61 |
.map(|line| { |
| 62 |
let (version, checksum) = line |
| 63 |
.split_once(char::is_whitespace) |
| 64 |
.unwrap_or_else(|| panic!("malformed CHECKSUMS line: {line:?}")); |
| 65 |
let version = version |
| 66 |
.trim_start_matches('0') |
| 67 |
.parse() |
| 68 |
.unwrap_or_else(|e| panic!("bad version in {line:?}: {e}")); |
| 69 |
(version, checksum.trim().to_string()) |
| 70 |
}) |
| 71 |
.collect() |
| 72 |
} |
| 73 |
|
| 74 |
#[test] |
| 75 |
fn shipped_migrations_are_unmodified() { |
| 76 |
let current = current_checksums(); |
| 77 |
let path = manifest_path(); |
| 78 |
|
| 79 |
if std::env::var_os(UPDATE_VAR).is_some() { |
| 80 |
std::fs::write(&path, render(¤t)).expect("write CHECKSUMS"); |
| 81 |
return; |
| 82 |
} |
| 83 |
|
| 84 |
let raw = std::fs::read_to_string(&path).expect("read CHECKSUMS"); |
| 85 |
let expected = parse(&raw); |
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
let modified: Vec<i64> = current |
| 90 |
.iter() |
| 91 |
.filter(|(version, checksum)| { |
| 92 |
expected |
| 93 |
.get(*version) |
| 94 |
.is_some_and(|pinned| pinned != *checksum) |
| 95 |
}) |
| 96 |
.map(|(version, _)| *version) |
| 97 |
.collect(); |
| 98 |
|
| 99 |
assert!( |
| 100 |
modified.is_empty(), |
| 101 |
"migration(s) {modified:?} were edited after shipping.\n\ |
| 102 |
Every install that already ran them will abort at launch with \ |
| 103 |
\"migration N was previously applied but has been modified\".\n\ |
| 104 |
Revert the edit and put the change in a new migration. If these \ |
| 105 |
migrations have genuinely never shipped, regenerate with \ |
| 106 |
{UPDATE_VAR}=1." |
| 107 |
); |
| 108 |
|
| 109 |
assert_eq!( |
| 110 |
expected, current, |
| 111 |
"migration set changed. New migrations are expected; regenerate with \ |
| 112 |
{UPDATE_VAR}=1 and commit the result. A missing version means a \ |
| 113 |
migration file was deleted, which breaks installs that ran it." |
| 114 |
); |
| 115 |
} |
| 116 |
|