Skip to content

Commit 5e4a65b

Browse files
jgarzikclaude
andcommitted
Let a test fail when the program it runs crashes
The probe step reported nothing and took no time, which says the first probe crashed and the step then died formatting its exit code. Two bugs, and the formatting one is why the table nobody got would have named the statement: in PowerShell 0xFFFFFFFF is Int32 -1, so the -band was a no-op, and [uint32] threw on the very value the cast existed to display. `-f` with X8 renders a negative Int32 as two's-complement hex by itself. The first probe is CLS, and CLS is the one console statement whose exit code no test checks. That is not a coincidence. compile_and_run_raw returns Ok whatever the program did, so a crash arrives as truncated output -- and test_cls_resets_the_column asserts that a newline does *not* appear after the escape, which a program that died writing the escape satisfies perfectly. It has been green on Windows throughout. RunOutput::assert_ran_to_completion fixes the shape: it fails on any nonzero exit and prints the code in hex, since Windows says what went wrong in it. Every test that reads stdout for what a console statement wrote now calls it, and a new test runs all sixteen console programs and asks only whether they survived -- which a crash cannot fake. Verified by pointing one entry at `PRINT 1/0`, which fails it. The probes now split CLS by what its program links: alone it pulls in almost nothing, while the same statement after a PRINT pulls in the whole file half of the runtime. If one crashes and the other does not, the fault is in what the linker kept rather than in the instructions, which is the question a harness running the real Win64 output on Linux cannot answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a894c9b commit 5e4a65b

4 files changed

Lines changed: 76 additions & 8 deletions

File tree

.github/workflows/TestingCI.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ jobs:
8080
# a different defect, and none of the three can be reproduced on
8181
# the Linux job.
8282
if ($LASTEXITCODE -ne 0) {
83-
# Widen before masking: -band on two int32s stays int32, so the
84-
# cast of a negative status threw and hid the number it was
85-
# printed to reveal.
86-
$code = [uint32]([int64]$LASTEXITCODE -band 0xFFFFFFFF)
87-
throw ("run failed: {0} (exit {1}, 0x{2:X8})" -f $_.Name, $LASTEXITCODE, $code)
83+
# `-f` with X8 renders a negative Int32 as its two's-complement
84+
# hex on its own. Masking first needed no cast and got one: in
85+
# PowerShell 0xFFFFFFFF is Int32 -1, so the -band was a no-op and
86+
# [uint32] then threw on the very value it was there to show.
87+
throw ("run failed: {0} (exit {1}, 0x{2:X8})" -f $_.Name, $LASTEXITCODE, $LASTEXITCODE)
8888
}
8989
}
9090
@@ -102,6 +102,9 @@ jobs:
102102
run: |
103103
$probes = [ordered]@{
104104
'CLS' = 'CLS'
105+
'CLS after PRINT' = "PRINT `"x`"`nCLS"
106+
'CLS twice' = "CLS`nCLS"
107+
'PRINT alone' = 'PRINT "x"'
105108
'COLOR' = 'COLOR 14, 1'
106109
'LOCATE' = 'LOCATE 2, 5'
107110
'LOCATE row only' = 'LOCATE 3'
@@ -125,14 +128,13 @@ jobs:
125128
Set-Content -Path probe.bas -Value $p.Value
126129
& ./target/release/xbasic64.exe probe.bas -o probe.exe | Out-Null
127130
if ($LASTEXITCODE -ne 0) {
128-
Write-Host ("{0,-16} DID NOT COMPILE" -f $p.Key)
131+
Write-Host ("{0,-20} DID NOT COMPILE" -f $p.Key)
129132
$failed += $p.Key
130133
continue
131134
}
132135
& ./probe.exe | Out-Null
133136
$code = $LASTEXITCODE
134-
$hex = [uint32]([int64]$code -band 0xFFFFFFFF)
135-
Write-Host ("{0,-16} exit {1} (0x{2:X8})" -f $p.Key, $code, $hex)
137+
Write-Host ("{0,-20} exit {1} (0x{2:X8})" -f $p.Key, $code, $code)
136138
if ($code -ne 0) { $failed += $p.Key }
137139
}
138140
if ($failed.Count -gt 0) { throw "crashed: $($failed -join ', ')" }

tests/common/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,29 @@ impl RunOutput {
3232
pub fn lines(&self) -> Vec<&str> {
3333
self.stdout.trim().lines().collect()
3434
}
35+
36+
/// Panic unless the program ran to completion.
37+
///
38+
/// [`compile_and_run_raw`] deliberately returns `Ok` whatever the program
39+
/// did, which makes a crash look like truncated output -- and an assertion
40+
/// phrased as "this must not appear" then passes *because* the program
41+
/// died. The CLS test was written that way and stayed green on Windows
42+
/// while the program it ran was aborting with an access violation.
43+
///
44+
/// Any test that reads stdout for what a statement produced wants this
45+
/// too. The exit code is printed in hex: Windows says what went wrong in
46+
/// it, and 0xC0000005 is not a number the compiler ever chooses.
47+
pub fn assert_ran_to_completion(&self, what: &str) {
48+
assert_eq!(
49+
self.exit_code,
50+
Some(0),
51+
"{what} did not run to completion: exit {:?} (0x{:08X}), stdout {:?}, stderr {:?}",
52+
self.exit_code,
53+
self.exit_code.unwrap_or(-1),
54+
self.stdout,
55+
self.stderr
56+
);
57+
}
3558
}
3659

3760
/// A failure of the compiler itself (lexer, parser, sema, codegen, assembler, linker).

tests/control/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,7 @@ fn test_system_ends_the_program() {
11001100
#[test]
11011101
fn test_beep_rings_the_bell() {
11021102
let run = crate::common::compile_and_run_raw("BEEP\n", "").expect("should compile");
1103+
run.assert_ran_to_completion("BEEP");
11031104
assert!(
11041105
run.stdout.contains('\u{7}'),
11051106
"BEEP writes BEL: {:?}",

tests/print/mod.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ fn test_locate_and_color_emit_escapes() {
151151
"",
152152
)
153153
.expect("should compile");
154+
out.assert_ran_to_completion("LOCATE then COLOR");
154155
assert!(
155156
out.stdout.contains("\u{1b}[5;10H"),
156157
"LOCATE 5,10 should home the cursor there: {:?}",
@@ -172,6 +173,7 @@ fn test_locate_and_color_emit_escapes() {
172173
#[test]
173174
fn test_locate_row_only() {
174175
let out = crate::common::compile_and_run_raw("LOCATE 7\n", "").expect("should compile");
176+
out.assert_ran_to_completion("LOCATE with a row only");
175177
assert!(
176178
out.stdout.contains("\u{1b}[7;"),
177179
"row given, column preserved: {:?}",
@@ -209,6 +211,10 @@ fn test_cls_resets_the_column() {
209211
"",
210212
)
211213
.expect("should compile");
214+
// Before this line the test could not fail on a crash: a program that died
215+
// in CLS left nothing after the escape, which is exactly what the
216+
// assertion below wants to see.
217+
out.assert_ran_to_completion("PRINT then CLS then TAB");
212218
let after_cls = out.stdout.rsplit("\u{1b}[H").next().unwrap_or("");
213219
assert!(
214220
!after_cls.starts_with('\n'),
@@ -217,6 +223,42 @@ fn test_cls_resets_the_column() {
217223
);
218224
}
219225

226+
/// Every console statement's program runs to completion.
227+
///
228+
/// Blunt on purpose. These helpers are written twice, and the Win64 half is
229+
/// the one no developer runs -- CI is the only place it executes at all. The
230+
/// tests above read what each statement *wrote*, which a crash can satisfy by
231+
/// writing nothing; this one only asks whether the program survived, which a
232+
/// crash cannot.
233+
#[test]
234+
fn test_console_statements_run_to_completion() {
235+
for (what, source) in [
236+
("CLS", "CLS\n"),
237+
("CLS after PRINT", "PRINT \"x\"\nCLS\n"),
238+
("CLS twice", "CLS\nCLS\n"),
239+
("LOCATE", "LOCATE 2, 5\n"),
240+
("LOCATE row only", "LOCATE 3\n"),
241+
("LOCATE column only", "LOCATE , 8\n"),
242+
("COLOR", "COLOR 14, 1\n"),
243+
("POS", "PRINT POS(0)\n"),
244+
("BEEP", "BEEP\n"),
245+
("TIMER", "PRINT TIMER\n"),
246+
("RANDOMIZE", "RANDOMIZE 42\n"),
247+
("RANDOMIZE TIMER", "RANDOMIZE TIMER\n"),
248+
("RND", "PRINT RND\n"),
249+
("DATE$ and TIME$", "PRINT DATE$\nPRINT TIME$\n"),
250+
("FRE", "PRINT FRE(0)\n"),
251+
(
252+
"the lot together",
253+
"CLS\nCOLOR 14, 1\nLOCATE 2, 5\nPRINT \"x\"; POS(0)\n",
254+
),
255+
] {
256+
let run = crate::common::compile_and_run_raw(source, "")
257+
.unwrap_or_else(|e| panic!("{what} should compile: {e}"));
258+
run.assert_ran_to_completion(what);
259+
}
260+
}
261+
220262
/// `LOCATE` also sets the column the tracker believes, for the same reason.
221263
#[test]
222264
fn test_locate_sets_the_column() {

0 commit comments

Comments
 (0)