1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//! Regression: the structured line resolver shares ONE line-offset builder.
//!
//! `structured/parsers/line.rs::resolve_line_number_options` used to carry its
//! own `build_line_starts(text)`: a byte-for-byte copy of
//! `pipeline::compute_line_offsets` (same `bytes.len()/40 + 1` capacity, same
//! leading `push(0)`, same `memchr_iter(b'\n') -> push(pos + 1)` loop). That
//! duplicate was deleted; the resolver now calls `crate::compute_line_offsets`
//! directly (NO DUPLICATION). The line attribution it produces is unchanged
//! because the builder output is identical.
//!
//! This test pins the exact contract that attribution depends on: the shared
//! `compute_line_offsets` table, plus the resolver's `partition_point(|&start|
//! start <= offset)` lookup (`line_number_for_offset`), must map each byte
//! offset to the correct 1-based line. If a future edit re-introduces a
//! divergent local builder (a missing leading `0`, an off-by-one `pos` vs
//! `pos + 1`), the offsets below change and every structured pair's reported
//! line drifts (this catches it with asserted integers, not shape).
use keyhog_scanner::testing::compute_line_offsets;
/// The resolver's exact lookup: the first line whose start is `> offset` minus
/// one, i.e. `partition_point(start <= offset)`. Kept in lockstep with
/// `structured/parsers/line.rs::line_number_for_offset`.
fn line_number_for_offset(line_starts: &[usize], offset: usize) -> usize {
line_starts.partition_point(|&start| start <= offset)
}
#[test]
fn structured_line_resolver_uses_shared_offset_builder() {
// alpha\n -> line 1, bytes 0..5, '\n' at 5
// beta\n -> line 2, bytes 6..10, '\n' at 10
// GAMMA=secret\n -> line 3, bytes 11..23, '\n' at 23
// delta\n -> line 4, bytes 24..29, '\n' at 29
// (trailing empty) -> line 5, byte 30
let text = "alpha\nbeta\nGAMMA=secret\ndelta\n";
let offsets = compute_line_offsets(text);
// Exact table: leading 0, then one entry per newline at `pos + 1`.
assert_eq!(
offsets,
vec![0, 6, 11, 24, 30],
"shared compute_line_offsets must produce the canonical line-start table \
(leading 0 + each newline+1) the structured resolver relies on"
);
// The resolver's partition_point lookup over that table -> 1-based line.
assert_eq!(
line_number_for_offset(&offsets, 0),
1,
"offset 0 is line 1 (start of 'alpha')"
);
assert_eq!(
line_number_for_offset(&offsets, 11),
3,
"offset 11 is line 3 (the 'G' of GAMMA=secret, the keyword anchor)"
);
assert_eq!(
line_number_for_offset(&offsets, 17),
3,
"offset 17 (mid 'secret') still resolves to line 3, not the next line"
);
assert_eq!(
line_number_for_offset(&offsets, 24),
4,
"offset 24 is line 4 (start of 'delta')"
);
assert_eq!(
line_number_for_offset(&offsets, 30),
5,
"offset 30 is the trailing empty line 5"
);
}
// ── Property tier ────────────────────────────────────────────────────────────
// The fixed vector pins one text's table + lookups; these SWEEP both contracts.
// `compute_line_offsets` must EXACTLY equal the naive `[0] ++ [i+1 for each '\n']`
// table (a DIFFERENTIAL keeping the memchr_iter builder behavior-identical to the
// scalar loop, a divergence drifts every structured pair's reported line). And the
// resolver's `partition_point` lookup must map any offset to `1 + (newlines strictly
// before it)` (the true 1-based line, independent of the table's construction).
// Traced against `compute_line_offsets` + `line_number_for_offset`. No proptest before.
use proptest::prelude::*;
/// Naive line-start table: a leading 0, then one entry per newline at `pos + 1`.
fn naive_line_offsets(text: &str) -> Vec<usize> {
let mut v = vec![0usize];
for (i, b) in text.bytes().enumerate() {
if b == b'\n' {
v.push(i + 1);
}
}
v
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(4_000))]
/// The shared builder equals the naive table for any text (letters, spaces and
/// newlines exercise both the leading-0 and per-newline pushes).
#[test]
fn line_offsets_match_the_naive_table(text in "[a-z \n]{0,80}") {
prop_assert_eq!(compute_line_offsets(&text), naive_line_offsets(&text));
}
/// The resolver's partition_point lookup maps any offset to `1 + (number of
/// newlines strictly before it)` (the correct 1-based line).
#[test]
fn line_number_is_one_plus_preceding_newlines(
text in "[a-z \n]{0,80}",
offset in 0usize..90,
) {
let offsets = compute_line_offsets(&text);
let line = line_number_for_offset(&offsets, offset);
let cap = offset.min(text.len());
let newlines_before = text.as_bytes()[..cap].iter().filter(|&&b| b == b'\n').count();
prop_assert_eq!(line, 1 + newlines_before);
}
}