fortrangoingonforty/afs-ld / 9c5dcc1

Browse files

add differential harness and empty-invocation test

Authored by espadonne
SHA
9c5dcc17660e57947f9ce80e41bd2299fb621ee1
Parents
e4af513
Tree
3d59d69

5 changed files

StatusFile+-
A tests/common/harness.rs 96 0
A tests/common/mod.rs 4 0
A tests/diff_harness_finds_critical.rs 30 0
A tests/diff_harness_sanity.rs 18 0
A tests/reader_empty.rs 28 0
tests/common/harness.rsadded
@@ -0,0 +1,96 @@
1
+//! Differential harness: compare afs-ld output against Apple `ld` output.
2
+//!
3
+//! Sprint 0 lands the diffing surface. The `link_both` function that actually
4
+//! shells out to both linkers arrives once afs-ld can produce a real binary
5
+//! (Sprint 18). Until then, tests exercise `diff_macho` directly against
6
+//! synthesized byte slices.
7
+
8
+#![allow(dead_code)]
9
+
10
+use std::path::PathBuf;
11
+
12
+pub struct LinkCase {
13
+    pub name: &'static str,
14
+    pub inputs: Vec<PathBuf>,
15
+    pub args: Vec<String>,
16
+}
17
+
18
+pub struct LinkOutputs {
19
+    pub ours: Vec<u8>,
20
+    pub theirs: Vec<u8>,
21
+}
22
+
23
+#[derive(Debug, Clone, PartialEq, Eq)]
24
+pub enum DiffCategory {
25
+    /// A diff we expect: UUID bytes, timestamps, hash-backed temp paths, etc.
26
+    Tolerated(&'static str),
27
+    /// Anything else. Fails the parity test.
28
+    Critical,
29
+}
30
+
31
+#[derive(Debug, Clone, PartialEq, Eq)]
32
+pub struct DiffChunk {
33
+    pub offset: usize,
34
+    pub len: usize,
35
+    pub reason: String,
36
+    pub category: DiffCategory,
37
+}
38
+
39
+#[derive(Debug, Default)]
40
+pub struct DiffReport {
41
+    pub tolerated: Vec<DiffChunk>,
42
+    pub critical: Vec<DiffChunk>,
43
+}
44
+
45
+impl DiffReport {
46
+    pub fn is_clean(&self) -> bool {
47
+        self.critical.is_empty()
48
+    }
49
+}
50
+
51
+/// Byte-level diff between two Mach-O images. Sprint 0 treats every byte diff
52
+/// as Critical; later sprints layer in the tolerated-diff predicates (UUID,
53
+/// timestamp, code-signature hashes, string-table suffix-dedup variance).
54
+pub fn diff_macho(ours: &[u8], theirs: &[u8]) -> DiffReport {
55
+    let mut report = DiffReport::default();
56
+
57
+    if ours.len() != theirs.len() {
58
+        report.critical.push(DiffChunk {
59
+            offset: 0,
60
+            len: ours.len().max(theirs.len()),
61
+            reason: format!(
62
+                "total size differs: ours = {}, theirs = {}",
63
+                ours.len(),
64
+                theirs.len()
65
+            ),
66
+            category: DiffCategory::Critical,
67
+        });
68
+        return report;
69
+    }
70
+
71
+    let mut i = 0;
72
+    while i < ours.len() {
73
+        if ours[i] != theirs[i] {
74
+            let start = i;
75
+            while i < ours.len() && ours[i] != theirs[i] {
76
+                i += 1;
77
+            }
78
+            report.critical.push(DiffChunk {
79
+                offset: start,
80
+                len: i - start,
81
+                reason: format!("{} byte(s) differ starting at 0x{start:x}", i - start),
82
+                category: DiffCategory::Critical,
83
+            });
84
+        } else {
85
+            i += 1;
86
+        }
87
+    }
88
+
89
+    report
90
+}
91
+
92
+/// Placeholder for the full linker-spawning contract. Sprint 18 wires this to
93
+/// real invocations of afs-ld and the system `ld` via `xcrun -f ld`.
94
+pub fn link_both(_case: &LinkCase) -> LinkOutputs {
95
+    panic!("link_both is not implemented until Sprint 18 (hello-world milestone)");
96
+}
tests/common/mod.rsadded
@@ -0,0 +1,4 @@
1
+//! Shared test infrastructure. Integration tests that need the harness
2
+//! declare `mod common;` and then use `common::harness::*`.
3
+
4
+pub mod harness;
tests/diff_harness_finds_critical.rsadded
@@ -0,0 +1,30 @@
1
+//! Two byte slices that differ in a non-tolerated region must surface as
2
+//! critical diffs. Proves the harness actually notices regressions — otherwise
3
+//! every later differential test becomes a silent false-negative generator.
4
+
5
+mod common;
6
+
7
+use common::harness::diff_macho;
8
+
9
+#[test]
10
+fn differing_bytes_surface_as_critical() {
11
+    let ours = b"hello world".to_vec();
12
+    let mut theirs = ours.clone();
13
+    theirs[6] = b'W'; // capitalize the W in "world"
14
+
15
+    let report = diff_macho(&ours, &theirs);
16
+    assert!(!report.is_clean(), "harness missed a real byte difference");
17
+    assert_eq!(report.critical.len(), 1);
18
+    let chunk = &report.critical[0];
19
+    assert_eq!(chunk.offset, 6);
20
+    assert_eq!(chunk.len, 1);
21
+}
22
+
23
+#[test]
24
+fn size_mismatch_is_critical() {
25
+    let ours = b"short".to_vec();
26
+    let theirs = b"considerably longer bytes".to_vec();
27
+    let report = diff_macho(&ours, &theirs);
28
+    assert!(!report.is_clean());
29
+    assert!(report.critical[0].reason.contains("size differs"));
30
+}
tests/diff_harness_sanity.rsadded
@@ -0,0 +1,18 @@
1
+//! Two identical byte slices must produce zero critical diffs.
2
+
3
+mod common;
4
+
5
+use common::harness::diff_macho;
6
+
7
+#[test]
8
+fn identical_inputs_produce_zero_critical_diffs() {
9
+    let a = b"same bytes everywhere".to_vec();
10
+    let b = a.clone();
11
+    let report = diff_macho(&a, &b);
12
+    assert!(
13
+        report.is_clean(),
14
+        "identical inputs produced critical diffs: {:#?}",
15
+        report.critical
16
+    );
17
+    assert!(report.tolerated.is_empty());
18
+}
tests/reader_empty.rsadded
@@ -0,0 +1,28 @@
1
+//! Running afs-ld with no inputs must produce the exact diagnostic line
2
+//! `afs-ld: error: no input files` on stderr and exit with code 2.
3
+//!
4
+//! The CLI surface grows over the next sprints but this invariant is the
5
+//! baseline every later change must preserve.
6
+
7
+use std::process::Command;
8
+
9
+#[test]
10
+fn empty_invocation_produces_no_input_files_diagnostic() {
11
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
12
+    let out = Command::new(exe)
13
+        .output()
14
+        .expect("afs-ld binary should exist and be executable");
15
+
16
+    assert!(!out.status.success(), "afs-ld should fail on empty input");
17
+    assert_eq!(
18
+        out.status.code(),
19
+        Some(2),
20
+        "empty-input exit code must be 2 (CLI misuse / missing input)"
21
+    );
22
+
23
+    let stderr = String::from_utf8_lossy(&out.stderr);
24
+    assert!(
25
+        stderr.contains("afs-ld: error: no input files"),
26
+        "expected the `no input files` diagnostic on stderr; got: {stderr}"
27
+    );
28
+}