code wiki / (root) / prop_test.nx

prop_test.nx source

↩ module page · 207 lines · 6748 B

1// prop_test.nx -- property-based testing infrastructure. 2// 3// Verify that a property holds across a LARGE random sample of 4// inputs instead of enumerating hand-picked cases. Catches bugs 5// that manual test cases miss. Inspired by QuickCheck (Haskell, 6// 2000), Hypothesis (Python), proptest (Rust). 7// 8// Pattern: 9// 1. Define a property P(x) that should be true for all x of 10// some type T. 11// 2. Write a generator that produces random T values from a 12// seeded PRNG. 13// 3. Run P(gen()) for N iterations (default 1000). 14// 4. On failure, record the first counter-example + shrink it 15// to a minimal case. 16// 17// This scaffold ships the GENERATOR + RUNNER primitives. Each 18// property test lives in its own .nx file (see prop_*_test.nx). 19// 20// Reference: Claessen & Hughes 2000 "QuickCheck: A Lightweight 21// Tool for Random Testing of Haskell Programs"; MacIver 2019 22// "Hypothesis: A new approach to property-based testing" 23// (particularly the shrinking algorithm). 24 25import "syscalls.nx" 26import "nx_assert.nx" 27 28// ---- PRNG --------------------------------------------------------- 29// 30// xorshift64 -- seeded, reproducible, passes most tests in the 31// TestU01 SmallCrush suite. Wider mixing via xorshift*. We don't 32// need cryptographic randomness; we need REPRODUCIBILITY across 33// machines so prop failures can be replayed by seed. 34 35struct PropGen { 36 state: i64, 37 iter: i64, 38 max: i64, 39} 40 41const PROP_GEN_BYTES: i64 = 32 42 43func prop_gen_new(seed: i64) -> *PropGen { 44 let g_raw: *u8 = sys_mmap(PROP_GEN_BYTES) 45 let g: *PropGen = g_raw as *PropGen 46 // Non-zero state required for xorshift; mix in a prime if 47 // caller passes 0. 48 if seed == 0 { 49 g.state = 0x9E3779B97F4A7C15 // golden ratio mix 50 } 51 if seed != 0 { 52 g.state = seed 53 } 54 g.iter = 0 55 g.max = 1000 56 return g 57} 58 59// xorshift64 next step -- returns a pseudorandom i64. Advances 60// internal state; each call is independent. 61func prop_gen_i64(g: *PropGen) -> i64 { 62 var s: i64 = g.state 63 s = s ^ (s << 13) 64 s = s ^ (s >> 7) 65 s = s ^ (s << 17) 66 g.state = s 67 return s 68} 69 70// Generate an i64 in [lo, hi] inclusive. Uses modulo which has 71// tiny bias for non-power-of-2 ranges; fine for property tests. 72func prop_gen_range(g: *PropGen, lo: i64, hi: i64) -> i64 { 73 if hi <= lo { return lo } 74 let span: i64 = hi - lo + 1 75 let raw: i64 = prop_gen_i64(g) 76 // Make positive; xorshift can produce negative i64. 77 var r: i64 = raw 78 if r < 0 { r = 0 - r } 79 if r < 0 { r = 0 } // handle INT64_MIN edge case 80 return lo + (r - (r / span) * span) 81} 82 83// Generate a "small" i64 -- biased toward 0. Good for edge-case 84// discovery (shrinking is effectively free if most generated 85// values are tiny). 86func prop_gen_small(g: *PropGen) -> i64 { 87 let r: i64 = prop_gen_i64(g) & 0xFF // 0..255 mostly 88 let sign: i64 = prop_gen_i64(g) & 1 89 if sign == 1 { return r } 90 return 0 - r 91} 92 93// Fill `buf` with `n` pseudorandom bytes. Used to generate input 94// for parsers, lexers, deserializers. 95func prop_gen_bytes(g: *PropGen, buf: *u8, n: i64) -> i64 { 96 var i: i64 = 0 97 while i < n { 98 buf[i] = prop_gen_i64(g) & 0xFF 99 i = i + 1 100 } 101 return 0 102} 103 104// Generate a printable ASCII byte (0x20..0x7E). Skips control 105// chars + DEL. 106func prop_gen_printable(g: *PropGen) -> i64 { 107 let r: i64 = prop_gen_i64(g) & 0x3F // 0..63 108 return 0x20 + r // 0x20..0x5F (subset) 109} 110 111// ---- property runner ---------------------------------------------- 112// 113// Since NishiLang doesn't have first-class function pointers used 114// as arbitrary callbacks, each property gets its own top-level 115// `main()` in a dedicated .nx file that: 116// 1. Creates a PropGen with a fixed seed (reproducibility). 117// 2. Loops `iters` times generating + checking. 118// 3. On failure: nx_assert fires + exits 200, reporting the 119// iteration index + seed. 120// 4. On success: exits 0. 121// 122// prop_test.nx itself just ships the generators above. The 123// pattern is visible in each prop_*_test.nx consumer. 124 125// Helper: emit 'prop_test: iter=N seed=S\n' on failure so the 126// reporter knows how to replay. Call before any nx_assert call 127// inside a property body. 128func prop_report(g: *PropGen, tag: *u8) -> i64 { 129 sys_write(2, "\nprop_test: ITER=" as *u8, 17) 130 nx_puti_err(g.iter) 131 sys_write(2, "prop_test: SEED=" as *u8, 16) 132 nx_putx_err(g.state) 133 sys_write(2, "\nprop_test: TAG=" as *u8, 16) 134 nx_puts_err(tag) 135 sys_write(2, "\n" as *u8, 1) 136 return 0 137} 138 139// ---- self-test ---------------------------------------------------- 140// 141// Minimal property: i64 addition is commutative. Generates 1000 142// pairs (a, b) and checks a+b == b+a. Success => exit 0. 143// 144// Also sanity-checks PRNG output: consecutive calls produce 145// different values (seed isn't stuck). 146 147func main() -> i64 { 148 let g: *PropGen = prop_gen_new(42) 149 150 // PRNG liveness -- 10 consecutive calls produce 10 distinct values. 151 let prev_raw: *u8 = sys_mmap(128) 152 let prev: *i64 = prev_raw as *i64 153 var i: i64 = 0 154 while i < 10 { 155 prev[i] = prop_gen_i64(g) 156 i = i + 1 157 } 158 i = 0 159 while i < 10 { 160 var j: i64 = i + 1 161 while j < 10 { 162 if prev[i] == prev[j] { 163 return __syscall(93, 10, 0, 0, 0, 0, 0) 164 } 165 j = j + 1 166 } 167 i = i + 1 168 } 169 170 // Commutativity of i64 addition. 171 var iter: i64 = 0 172 while iter < 1000 { 173 g.iter = iter 174 let a: i64 = prop_gen_i64(g) 175 let b: i64 = prop_gen_i64(g) 176 if a + b != b + a { 177 prop_report(g, "i64 + commutes" as *u8) 178 return __syscall(93, 20, 0, 0, 0, 0, 0) 179 } 180 iter = iter + 1 181 } 182 183 // Range bounds -- prop_gen_range stays within [lo, hi]. 184 iter = 0 185 while iter < 100 { 186 let r: i64 = prop_gen_range(g, -50, 50) 187 if r < -50 { 188 return __syscall(93, 30, 0, 0, 0, 0, 0) 189 } 190 if r > 50 { 191 return __syscall(93, 31, 0, 0, 0, 0, 0) 192 } 193 iter = iter + 1 194 } 195 196 // prop_gen_bytes fills the buffer without going past. Can't 197 // detect an off-by-one write past the end without a canary -- 198 // add a sentinel byte and verify it's untouched. 199 let buf: *u8 = sys_mmap(40) 200 buf[32] = 0xFF // canary 201 prop_gen_bytes(g, buf, 32) 202 if buf[32] != 0xFF { 203 return __syscall(93, 40, 0, 0, 0, 0, 0) 204 } 205 206 return 0 207}