Skip to content

Commit c82d3f0

Browse files
Add shared tccbin conformance test suite (#27924)
* Add shared tccbin conformance test suite thirdparty/tccbin_tests/: a single, shared home for the compatibility tests that verify a platform's prebuilt tcc + libgc.a pairing (from vlang/tccbin) actually works, instead of each platform branch there maintaining its own disconnected, ad hoc checks (or none at all). - shared/: platform-agnostic tests every branch is expected to pass identically (hello-world with GC init, a GC-heavy allocation test, and confirming a genuine crash is still caught and not swallowed). - platform/<name>/: a small bucket for tests that are legitimately platform-specific (currently just platform/windows/crash_message.c, which checks tcc's own SEH diagnostic wording - a Windows-only implementation detail). - run.ps1 (Windows) / run.sh (POSIX, unverified - no non-Windows tcc build to test it against yet): small runners taking a tcc path and that platform's link flags, so nothing platform-specific is baked into the shared test logic itself. windows-amd64's tccbin branch (PR vlang/tccbin#67) is the reference adopter - see this directory's README for the shape other platform branches would copy. * docs: run.sh verified against a real Linux tcc build Built tinycc from this same source natively on Linux (WSL/Ubuntu 24.04, gcc 16, ./configure && make install), linked the shared tests against libgc-dev 8.2.6 from apt, and ran run.sh for real - all three shared tests pass, stress-tested over 10 consecutive runs with no flakiness. Updates the README, which previously (accurately, at the time) flagged run.sh as unverified. Not a full replication of vlang's actual Linux tccbin build (that already has its own working, scripted pipeline via thirdparty/build_scripts/thirdparty-linux-amd64_{tcc,bdwgc}.sh) - this was specifically to verify the test *runner* script itself works correctly on POSIX, using apt's libgc-dev as a quick stand-in rather than building BDWGC from source to match exactly. * fix: address Codex review round 1 on #27924 Three P2 findings, all confirmed against the current tree and fixed: - run.sh was committed as mode 100644 (not executable), so `./run.sh` as documented/used in CI would fail with "Permission denied" on any real POSIX filesystem (reproduced via a fresh git-archive extract onto WSL's native ext4 - /mnt/* NTFS mounts mask this, since DrvFs synthesizes rwx regardless of the tracked mode). Set via `git update-index --chmod=+x`, since there's no native chmod semantics to pick up from the Windows-side file. - The adoption guide told platforms with a prebuilt libgc.a to skip checking out thirdparty/libgc entirely - but no platform branch bundles its own gc.h (confirmed: none exists anywhere under thirdparty/tcc in the windows-amd64 branch), so the *headers* are needed unconditionally to compile the mandatory shared tests. Only thirdparty/libgc/gc.c (the source) is conditional on whether the platform rebuilds libgc.a from scratch. Clarified. - README.md:57 was 162 chars against AGENTS.md's documented 100-char Markdown limit; `v check-md` confirmed the failure and the fix (wrapped into a fenced code block). Both from Codex, #27924 pullrequestreview-4767463437. * fix: address Codex review round 2 on #27924 - run.ps1: read stdout async while reading stderr sync, instead of ReadToEnd()-ing both sequentially - a process writing enough to the stream read second to fill its pipe buffer before the first stream closes would deadlock forever, since nothing drains the second stream while blocked on the first. Reproduced locally with a test program that only writes to stderr (15s hang), fixed, reverified (completes immediately, no regression in the 4-test suite). - run.ps1: use -cnotmatch instead of -notmatch for the expected- substring check - -notmatch is case-insensitive by default in PowerShell, making run.ps1 laxer than run.sh's case-sensitive `case` pattern match and able to miss a diagnostics-wording regression that only changes case. Reproduced (differently-cased substring wrongly "matched"), fixed, reverified (mismatched case now fails, exact case still passes, suite still 4/4). - run.sh: guard `work=$(mktemp -d)` with `|| exit 1` - an unguarded failure left $work empty, so exe="$work/$name.exe" silently became "/name.exe" (filesystem root). Reproduced via WSL with TMPDIR pointed at a nonexistent directory: the script proceeded, tried to write test binaries to /gc_alloc.exe and /hello.exe, and the crash test even produced a false PASS (wrong-reason nonzero exit) masking the real failure. Fixed, reverified (script now exits 1 immediately instead of limping along with a bogus work dir). All from Codex, #27924 pullrequestreview-4767856781.
1 parent 583bcf8 commit c82d3f0

11 files changed

Lines changed: 369 additions & 0 deletions

File tree

thirdparty/tccbin_tests/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# tccbin conformance tests
2+
3+
Shared compatibility tests for the prebuilt `tcc`/`libgc.a` pairs shipped
4+
on each platform branch of [vlang/tccbin](https://github.com/vlang/tccbin).
5+
Each `thirdparty-<platform>` branch there is an independent orphan branch
6+
with its own binaries and no shared history, so there was previously no
7+
single place testing "does this tcc still work as a C compiler + GC
8+
backend" the same way across all of them - regressions could land on one
9+
platform's rebuild and go unnoticed unless someone happened to test that
10+
specific platform by hand.
11+
12+
This directory is that single place. It doesn't contain any binaries
13+
itself - platform branches pull it in via a sparse checkout (see
14+
"Adopting this in a platform branch's CI" below) and run it against
15+
whatever `tcc`/`libgc.a` they just built.
16+
17+
## Layout
18+
19+
```
20+
shared/ tests every platform is expected to pass identically
21+
platform/<name>/ additional tests specific to one platform
22+
run.ps1 test runner for Windows
23+
run.sh test runner for POSIX platforms
24+
```
25+
26+
Put a test in `shared/` unless it's actually testing something
27+
platform-specific (e.g. `platform/windows/crash_message.c` checks the
28+
exact wording of tcc's own SEH-based crash diagnostic - a Windows
29+
implementation detail, not something other platforms produce or need
30+
to reproduce). Most tests belong in `shared/`; `platform/` should stay
31+
small.
32+
33+
## Test format
34+
35+
Each test is a pair of files with the same base name:
36+
37+
- `<name>.c` - the test program.
38+
- `<name>.expected` - 1 or 2 lines:
39+
- line 1: the expected exit code, or the literal word `nonzero`
40+
(for tests that are supposed to crash/fail on purpose).
41+
- line 2 (optional): a substring that must appear somewhere in the
42+
program's combined stdout+stderr. Omit this line if you only care
43+
about the exit code.
44+
45+
## Running
46+
47+
```
48+
# Windows
49+
.\run.ps1 -Tcc <path to tcc.exe> [-Platform windows] -- <extra tcc args>
50+
51+
# POSIX
52+
./run.sh <path to tcc> [platform] -- <extra tcc args>
53+
```
54+
55+
"Extra args" are whatever the given tcc build needs on that platform to
56+
link a program that uses the GC - e.g. on Windows:
57+
58+
```
59+
-DGC_NOT_DLL -DGC_WIN32_THREADS -DGC_THREADS -DGC_BUILTIN_ATOMIC
60+
-I <path to thirdparty/libgc/include> -bt25 -municode -ldbghelp -luser32
61+
<path to lib/libgc.a>
62+
```
63+
64+
The runners don't hardcode any platform-specific flags themselves -
65+
each platform branch's CI supplies them, since they genuinely differ
66+
(different GC threading defines, different Windows-only libs, etc).
67+
68+
## Adopting this in a platform branch's CI
69+
70+
The windows-amd64 branch's `.github/workflows/build-and-test.yml`
71+
(in [vlang/tccbin](https://github.com/vlang/tccbin), `thirdparty-windows-amd64`
72+
branch) is the reference implementation. The shape to copy for another
73+
platform:
74+
75+
1. Sparse-checkout this repo (`vlang/v`) for `thirdparty/tccbin_tests`
76+
and `thirdparty/libgc`. Both are needed unconditionally: no platform
77+
branch bundles its own `gc.h`, so `thirdparty/libgc/include` is
78+
required to compile `shared/hello.c`/`shared/gc_alloc.c` even if
79+
the platform ships a prebuilt `libgc.a` and never touches
80+
`thirdparty/libgc/gc.c` (the *source*, only needed if the platform
81+
is rebuilding `libgc.a` from scratch, as windows-amd64 does).
82+
2. Build that platform's `tcc` (and `libgc.a`, if applicable) as the
83+
branch already does.
84+
3. Run `run.sh <tcc> <platform-name> -- <that platform's link flags>`
85+
and fail the CI job if it exits nonzero.
86+
4. If there's a `platform/<platform-name>/` directory here with tests
87+
specific to that platform, they'll be picked up automatically by
88+
step 3 - add one (following `platform/windows/`'s example) if a
89+
platform-specific regression is worth pinning down permanently.
90+
91+
`run.sh` has been verified against a native Linux tcc (built from this
92+
same source via `./configure && make install`, WSL/Ubuntu 24.04, gcc
93+
16) linked against `libgc-dev` 8.2.6 from apt - `shared/hello.c`,
94+
`shared/gc_alloc.c`, and `shared/crash.c` all pass, stress-tested over
95+
10 consecutive runs with no flakiness. It has *not* been run on macOS
96+
or any BSD - the differences there (different GC threading defines,
97+
different linker behavior) are unverified; treat those as untested
98+
until a real platform branch adopts it.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* Windows-specific: same fault as shared/crash.c, but additionally
2+
verifies tcc's own SEH-based diagnostic message. The exact wording
3+
is a tcc-on-Windows implementation detail (tccrun.c's
4+
cpu_exception_handler), not something other platforms are expected
5+
to reproduce - that's why this lives here and not in shared/. */
6+
int main(void) {
7+
int *p = (int *)0;
8+
return *p;
9+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
nonzero
2+
invalid memory access

thirdparty/tccbin_tests/run.ps1

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Runs the shared tccbin conformance tests (and, if -Platform is given,
2+
# that platform's additional regression tests) against a built tcc.
3+
#
4+
# Usage:
5+
# run.ps1 -Tcc <path to tcc.exe> [-Platform windows] [-- <extra tcc args>]
6+
#
7+
# Extra args (everything after --) are passed to every test compile -
8+
# e.g. GC defines, -bt25, -municode, -ldbghelp -luser32, path to
9+
# libgc.a. These vary per platform, so the caller supplies them; this
10+
# script doesn't hardcode anything platform-specific itself.
11+
#
12+
# Each test is a pair: <name>.c and <name>.expected. The .expected
13+
# file has 1 or 2 lines:
14+
# line 1: expected exit code, or the literal word "nonzero"
15+
# line 2 (optional): a substring that must appear in the program's
16+
# combined stdout+stderr. Omit for no check.
17+
18+
param(
19+
[Parameter(Mandatory = $true)][string]$Tcc,
20+
[string]$Platform = "",
21+
[Parameter(ValueFromRemainingArguments = $true)][string[]]$ExtraArgs
22+
)
23+
24+
$ErrorActionPreference = "Stop"
25+
$Here = $PSScriptRoot
26+
27+
function Get-TestDirs {
28+
$dirs = @(Join-Path $Here "shared")
29+
if ($Platform -ne "") {
30+
$p = Join-Path $Here "platform\$Platform"
31+
if (Test-Path $p) { $dirs += $p }
32+
}
33+
return $dirs
34+
}
35+
36+
$passed = 0
37+
$failed = 0
38+
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("tccbin-tests-" + [guid]::NewGuid())
39+
New-Item -ItemType Directory -Path $work | Out-Null
40+
41+
foreach ($dir in (Get-TestDirs)) {
42+
foreach ($src in (Get-ChildItem (Join-Path $dir "*.c"))) {
43+
$name = $src.BaseName
44+
$expectedFile = Join-Path $dir "$name.expected"
45+
if (-not (Test-Path $expectedFile)) {
46+
Write-Host "FAIL $name (no .expected file)"
47+
$failed++
48+
continue
49+
}
50+
$lines = @(Get-Content $expectedFile)
51+
$expectExit = $lines[0].Trim()
52+
$expectSubstr = if ($lines.Count -gt 1) { $lines[1] } else { "" }
53+
54+
$exe = Join-Path $work "$name.exe"
55+
& $Tcc $src.FullName @ExtraArgs -o $exe 2>&1 | Out-Null
56+
if ($LASTEXITCODE -ne 0) {
57+
Write-Host "FAIL $name (compile error)"
58+
$failed++
59+
continue
60+
}
61+
62+
# Read via the .NET Process API directly rather than `&`/$LASTEXITCODE
63+
# (raced/read stale exit codes on processes that terminate via an
64+
# unhandled SEH exception) or Start-Process -RedirectStandardOutput
65+
# to a file (the file wasn't reliably flushed by the time -Wait
66+
# returned, for short-lived processes - both reproduced locally).
67+
# ReadToEnd() blocks until the stream closes (i.e. the process has
68+
# actually finished producing output), so there's no race.
69+
#
70+
# stdout and stderr are read one async + one sync (not both
71+
# ReadToEnd()'d sequentially) to avoid deadlocking: a process that
72+
# writes enough to the stream read second to fill its OS pipe
73+
# buffer before the stream read first closes would block forever,
74+
# since nothing drains the second stream while ReadToEnd() blocks
75+
# on the first - reproduced locally with a test program that only
76+
# writes to stderr.
77+
$psi = [System.Diagnostics.ProcessStartInfo]::new($exe)
78+
$psi.RedirectStandardOutput = $true
79+
$psi.RedirectStandardError = $true
80+
$psi.UseShellExecute = $false
81+
$proc = [System.Diagnostics.Process]::Start($psi)
82+
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
83+
$stderrText = $proc.StandardError.ReadToEnd()
84+
$stdoutText = $stdoutTask.GetAwaiter().GetResult()
85+
$proc.WaitForExit()
86+
$code = $proc.ExitCode
87+
$out = $stdoutText + $stderrText
88+
89+
$ok = $true
90+
if ($expectExit -eq "nonzero") {
91+
if ($code -eq 0) { $ok = $false }
92+
}
93+
elseif ($code -ne [int]$expectExit) {
94+
$ok = $false
95+
}
96+
# -notmatch is case-INSENSITIVE by default in PowerShell, which
97+
# would make this laxer than run.sh's case-sensitive `case`
98+
# pattern match and could miss a diagnostic-wording regression
99+
# that only changes case. -cnotmatch forces case-sensitivity.
100+
if ($expectSubstr -ne "" -and $out -cnotmatch [regex]::Escape($expectSubstr)) {
101+
$ok = $false
102+
}
103+
104+
if ($ok) {
105+
Write-Host "PASS $name"
106+
$passed++
107+
}
108+
else {
109+
Write-Host "FAIL $name (exit=$code, output=$out)"
110+
$failed++
111+
}
112+
}
113+
}
114+
115+
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
116+
Write-Host "---"
117+
Write-Host "$passed passed, $failed failed"
118+
if ($failed -gt 0) { exit 1 }

thirdparty/tccbin_tests/run.sh

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env bash
2+
# Runs the shared tccbin conformance tests (and, if a platform name is
3+
# given, that platform's additional regression tests) against a built tcc.
4+
#
5+
# Usage:
6+
# run.sh <path to tcc> [platform] [-- <extra tcc args>]
7+
#
8+
# Extra args (everything after --) are passed to every test compile -
9+
# e.g. GC defines, -bt flags, path to libgc.a. These vary per platform,
10+
# so the caller supplies them; this script doesn't hardcode anything
11+
# platform-specific itself.
12+
#
13+
# Each test is a pair: <name>.c and <name>.expected. The .expected
14+
# file has 1 or 2 lines:
15+
# line 1: expected exit code, or the literal word "nonzero"
16+
# line 2 (optional): a substring that must appear in the program's
17+
# combined stdout+stderr. Omit for no check.
18+
set -u
19+
20+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21+
tcc="$1"; shift
22+
platform=""
23+
if [ "$#" -gt 0 ] && [ "$1" != "--" ]; then
24+
platform="$1"
25+
shift
26+
fi
27+
if [ "$#" -gt 0 ] && [ "$1" = "--" ]; then
28+
shift
29+
fi
30+
extra_args=("$@")
31+
32+
dirs=("$here/shared")
33+
if [ -n "$platform" ] && [ -d "$here/platform/$platform" ]; then
34+
dirs+=("$here/platform/$platform")
35+
fi
36+
37+
work=$(mktemp -d) || exit 1
38+
trap 'rm -rf "$work"' EXIT
39+
40+
passed=0
41+
failed=0
42+
43+
for dir in "${dirs[@]}"; do
44+
for src in "$dir"/*.c; do
45+
[ -e "$src" ] || continue
46+
name=$(basename "$src" .c)
47+
expected="$dir/$name.expected"
48+
if [ ! -f "$expected" ]; then
49+
echo "FAIL $name (no .expected file)"
50+
failed=$((failed + 1))
51+
continue
52+
fi
53+
expect_exit=$(sed -n '1p' "$expected")
54+
expect_substr=$(sed -n '2p' "$expected")
55+
56+
exe="$work/$name.exe"
57+
if ! "$tcc" "$src" "${extra_args[@]}" -o "$exe" >/dev/null 2>&1; then
58+
echo "FAIL $name (compile error)"
59+
failed=$((failed + 1))
60+
continue
61+
fi
62+
63+
out=$("$exe" 2>&1)
64+
code=$?
65+
66+
ok=1
67+
if [ "$expect_exit" = "nonzero" ]; then
68+
[ "$code" -eq 0 ] && ok=0
69+
else
70+
[ "$code" -eq "$expect_exit" ] || ok=0
71+
fi
72+
if [ -n "$expect_substr" ]; then
73+
case "$out" in
74+
*"$expect_substr"*) ;;
75+
*) ok=0 ;;
76+
esac
77+
fi
78+
79+
if [ "$ok" -eq 1 ]; then
80+
echo "PASS $name"
81+
passed=$((passed + 1))
82+
else
83+
echo "FAIL $name (exit=$code, output=$out)"
84+
failed=$((failed + 1))
85+
fi
86+
done
87+
done
88+
89+
echo "---"
90+
echo "$passed passed, $failed failed"
91+
[ "$failed" -eq 0 ]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/* A genuine fault. Confirms the compiler's own crash-reporting/signal
2+
handling (if any) still catches real faults and doesn't silently
3+
swallow them - the mirror image of hello.c/gc_alloc.c, which check
4+
that nothing THAT SHOULDN'T crash does. */
5+
int main(void) {
6+
int *p = (int *)0;
7+
return *p;
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nonzero
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* Allocates through GC_malloc under real load, and reads the result
2+
back - exercises the GC's stack-scanning/collection machinery, not
3+
just its startup path. */
4+
#include <stdio.h>
5+
#include "gc.h"
6+
7+
struct node {
8+
int val;
9+
struct node *next;
10+
};
11+
12+
int main(void) {
13+
GC_INIT();
14+
struct node *head = NULL;
15+
for (int i = 0; i < 100000; i++) {
16+
struct node *n = (struct node *)GC_malloc(sizeof(struct node));
17+
n->val = i;
18+
n->next = head;
19+
head = n;
20+
}
21+
long long sum = 0;
22+
for (struct node *n = head; n; n = n->next)
23+
sum += n->val;
24+
printf("sum=%lld\n", sum);
25+
return 0;
26+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
0
2+
sum=4999950000
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* Trivial program, GC-initialized but no real allocation load.
2+
Exercises the compiler + GC startup path with minimal surface area -
3+
any platform where this doesn't pass and run cleanly has a basic
4+
compatibility problem in the prebuilt tcc/libgc pairing itself. */
5+
#include <stdio.h>
6+
#include "gc.h"
7+
8+
int main(void) {
9+
GC_INIT();
10+
printf("hello\n");
11+
return 0;
12+
}

0 commit comments

Comments
 (0)