Skip to content

Commit c14adea

Browse files
committed
ci: add tiered engine validation
1 parent 5d3668a commit c14adea

10 files changed

Lines changed: 314 additions & 17 deletions

File tree

.github/workflows/ci.yml

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
name: Engine CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
schedule:
8+
- cron: "23 9 * * *"
9+
- cron: "47 18 * * *"
10+
workflow_dispatch:
11+
inputs:
12+
tier:
13+
description: Validation tier to run
14+
required: true
15+
default: fast
16+
type: choice
17+
options:
18+
- fast
19+
- quickjs-differential
20+
- test262-full
21+
22+
permissions:
23+
contents: read
24+
25+
env:
26+
CARGO_TERM_COLOR: always
27+
RUST_BACKTRACE: "1"
28+
RUST_VERSION: "1.88.0"
29+
30+
jobs:
31+
fast:
32+
if: >-
33+
github.event_name == 'pull_request' ||
34+
github.event_name == 'push' ||
35+
(github.event_name == 'workflow_dispatch' && inputs.tier == 'fast')
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 30
38+
steps:
39+
- name: Check out repository
40+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
41+
with:
42+
persist-credentials: false
43+
44+
- name: Install pinned Rust toolchain
45+
run: |
46+
rustup toolchain install "${RUST_VERSION}" --profile minimal --component rustfmt,clippy
47+
rustup default "${RUST_VERSION}"
48+
49+
- name: Install gate dependencies
50+
run: |
51+
sudo apt-get update
52+
sudo apt-get install --yes ripgrep
53+
54+
- name: Cache Cargo inputs
55+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
56+
with:
57+
path: |
58+
~/.cargo/registry
59+
~/.cargo/git
60+
key: ${{ runner.os }}-cargo-${{ env.RUST_VERSION }}-${{ hashFiles('Cargo.lock') }}
61+
restore-keys: |
62+
${{ runner.os }}-cargo-${{ env.RUST_VERSION }}-
63+
64+
- name: Check formatting and architecture gates
65+
run: |
66+
cargo fmt --all -- --check
67+
./scripts/check-no-test262-special-casing.sh
68+
./scripts/check-rust-only.sh
69+
70+
- name: Run fast engine tests
71+
run: |
72+
cargo test --locked --workspace --lib --bins
73+
cargo test --locked --test cli --test unsupported_diagnostics
74+
75+
- name: Lint production targets
76+
run: cargo clippy --locked --workspace --lib --bins -- -D warnings
77+
78+
quickjs-differential:
79+
if: >-
80+
(github.event_name == 'schedule' && github.event.schedule == '23 9 * * *') ||
81+
(github.event_name == 'workflow_dispatch' && inputs.tier == 'quickjs-differential')
82+
runs-on: ubuntu-latest
83+
timeout-minutes: 180
84+
env:
85+
QJS_ORACLE_CACHE: ${{ github.workspace }}/.ci-cache/oracle
86+
steps:
87+
- name: Check out repository
88+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
89+
with:
90+
persist-credentials: false
91+
92+
- name: Install pinned Rust toolchain
93+
run: |
94+
rustup toolchain install "${RUST_VERSION}" --profile minimal
95+
rustup default "${RUST_VERSION}"
96+
97+
- name: Install oracle dependencies
98+
run: |
99+
sudo apt-get update
100+
sudo apt-get install --yes build-essential curl git ripgrep xz-utils
101+
102+
- name: Restore authenticated input caches
103+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
104+
with:
105+
path: |
106+
~/.cargo/registry
107+
~/.cargo/git
108+
.ci-cache/oracle
109+
key: >-
110+
${{ runner.os }}-differential-${{ env.RUST_VERSION }}-${{
111+
hashFiles('Cargo.lock', 'compat/upstream.toml',
112+
'scripts/build-quickjs-oracle.sh', 'scripts/prepare-test262.sh') }}
113+
restore-keys: |
114+
${{ runner.os }}-differential-${{ env.RUST_VERSION }}-
115+
116+
- name: Run the pinned QuickJS differential corpus
117+
run: |
118+
oracle=$(./scripts/build-quickjs-oracle.sh)
119+
QJS_ORACLE="$oracle" cargo test --locked --workspace --all-targets
120+
121+
test262-full:
122+
if: >-
123+
(github.event_name == 'schedule' && github.event.schedule == '47 18 * * *') ||
124+
(github.event_name == 'workflow_dispatch' && inputs.tier == 'test262-full')
125+
runs-on: ubuntu-latest
126+
timeout-minutes: 350
127+
env:
128+
QJS_ORACLE_CACHE: ${{ github.workspace }}/.ci-cache/oracle
129+
TEST262_WORKERS: "2"
130+
steps:
131+
- name: Check out repository
132+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
133+
with:
134+
persist-credentials: false
135+
136+
- name: Install pinned Rust toolchain
137+
run: |
138+
rustup toolchain install "${RUST_VERSION}" --profile minimal
139+
rustup default "${RUST_VERSION}"
140+
141+
- name: Install Test262 dependencies
142+
run: |
143+
sudo apt-get update
144+
sudo apt-get install --yes build-essential curl git ripgrep xz-utils
145+
146+
- name: Restore authenticated input caches
147+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
148+
with:
149+
path: |
150+
~/.cargo/registry
151+
~/.cargo/git
152+
.ci-cache/oracle
153+
key: >-
154+
${{ runner.os }}-test262-${{ env.RUST_VERSION }}-${{
155+
hashFiles('Cargo.lock', 'compat/upstream.toml',
156+
'scripts/build-quickjs-oracle.sh', 'scripts/prepare-test262.sh') }}
157+
restore-keys: |
158+
${{ runner.os }}-test262-${{ env.RUST_VERSION }}-
159+
160+
- name: Run the complete frozen Test262 vector
161+
run: |
162+
set -o pipefail
163+
mkdir -p target
164+
./scripts/test-test262-full.sh 2>&1 | tee target/test262-full.log
165+
166+
- name: Upload complete Test262 receipts
167+
if: always()
168+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
169+
with:
170+
name: test262-full-${{ github.sha }}
171+
path: |
172+
target/test262-full.tsv
173+
target/test262-full.jsonl
174+
target/test262-full.log
175+
tests/test262-full-baseline.txt
176+
compat/upstream.toml
177+
if-no-files-found: warn
178+
retention-days: 30

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "quickjs-oxide"
33
version = "0.0.1"
44
edition = "2024"
5-
rust-version = "1.85"
5+
rust-version = "1.88"
66
description = "A memory-safe Rust rewrite aiming for QuickJS feature parity"
77
license = "MIT"
88
repository = "https://github.com/pocket-stack/quickjs-oxide"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Feature Parity claim.
2525

2626
## Try it
2727

28-
Rust 1.85 or newer is required.
28+
Rust 1.88 or newer is required.
2929

3030
```sh
3131
git clone https://github.com/pocket-stack/quickjs-oxide.git
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env bash
2+
# Reject fixture-specific behavior in production engine sources.
3+
4+
set -euo pipefail
5+
export LC_ALL=C
6+
7+
script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
8+
root=$(CDPATH='' cd -- "$script_dir/.." && pwd)
9+
cd "$root"
10+
11+
die() {
12+
echo "error: $*" >&2
13+
exit 1
14+
}
15+
16+
command -v rg >/dev/null 2>&1 || die "ripgrep is required"
17+
18+
scan_roots=(src web/wasm/src)
19+
scan_globs=(
20+
--glob '*.rs'
21+
--glob '!**/tests.rs'
22+
--glob '!src/bin/run_test262.rs'
23+
--glob '!src/bin/run_test262/**'
24+
)
25+
26+
path_pattern='\b(?:test/)?(?:built-ins|language|intl402|annexB|staging|harness)/[A-Za-z0-9_./@+-]+\.js\b|[A-Za-z0-9_.@+-]+_FIXTURE\.js\b'
27+
source_hash_pattern='\b(?:source|source_text|script|program|code)(?:_[a-z0-9_]*(?:hash|digest|sha_?(?:1|256|512))[a-z0-9_]*|\b[^;\n]{0,100}\.[a-z0-9_]*(?:hash|digest|sha_?(?:1|256|512))[a-z0-9_]*)\b|\b[a-z0-9_]*(?:hash|digest|sha_?(?:1|256|512))[a-z0-9_]*\s*\([^;\n]{0,100}\b(?:source|source_text|script|program|code)\b'
28+
source_literal_pattern='(?i:\b(?:source|source_text|script|program|code)\b)[^;\n]{0,80}(?:==|!=)\s*(?:r\#*)?"[^"\n]{16,}"\#*|(?:r\#*)?"[^"\n]{16,}"\#*\s*(?:==|!=)[^;\n]{0,80}(?i:\b(?:source|source_text|script|program|code)\b)'
29+
source_probe_pattern='(?i:\b(?:source|source_text|script|program|code)\b)[^;\n]{0,120}\.(?:contains|starts_with|ends_with)\(\s*(?:r\#*)?"[^"\n]{16,}"'
30+
filename_probe_pattern='(?i:\b(?:filename|file_name|path)\b)[^;\n]{0,120}\.(?:contains|starts_with|ends_with)\(\s*(?:r\#*)?"[^"\n]+\.js'
31+
32+
scan_regex() {
33+
local label=$1 pattern=$2 output status
34+
set +e
35+
output=$(rg --line-number --no-heading --color never --pcre2 \
36+
"${scan_globs[@]}" -- "$pattern" "${scan_roots[@]}" 2>&1)
37+
status=$?
38+
set -e
39+
case $status in
40+
0)
41+
printf '%s\n' "$output" >&2
42+
die "production sources contain $label"
43+
;;
44+
1) ;;
45+
*)
46+
printf '%s\n' "$output" >&2
47+
die "could not scan production sources for $label"
48+
;;
49+
esac
50+
}
51+
52+
scan_regex "a Test262 path or fixture name" "$path_pattern"
53+
scan_regex "source-derived hash dispatch" "$source_hash_pattern"
54+
scan_regex "an exact authored-source comparison" "$source_literal_pattern"
55+
scan_regex "an authored-source substring probe" "$source_probe_pattern"
56+
scan_regex "a JavaScript filename-specific branch" "$filename_probe_pattern"
57+
58+
tmp=$(mktemp -d "${TMPDIR:-/tmp}/quickjs-oxide-anticheat.XXXXXX")
59+
trap 'rm -rf -- "$tmp"' EXIT
60+
61+
# Production code has one authenticated data-provenance digest: the pinned
62+
# QuickJS Unicode table source. Reject every other SHA-1/SHA-256-shaped literal
63+
# instead of coupling this gate to the runner's current profile history.
64+
hash_pattern='(?i:\b(?=[0-9a-f]{40}\b)(?=[0-9a-f]*[a-f])[0-9a-f]{40}\b|\b(?=[0-9a-f]{64}\b)(?=[0-9a-f]*[a-f])[0-9a-f]{64}\b)'
65+
unicode_source_sha=cf782bc7a07549e976f606bd3cb8555858482b279574554dcb8d46412986006c
66+
set +e
67+
hash_output=$(rg --line-number --no-heading --color never --pcre2 \
68+
"${scan_globs[@]}" -- "$hash_pattern" "${scan_roots[@]}" 2>&1)
69+
hash_status=$?
70+
set -e
71+
case $hash_status in
72+
0)
73+
unexpected_hashes=$tmp/unexpected-hashes.txt
74+
: > "$unexpected_hashes"
75+
while IFS= read -r occurrence; do
76+
case $occurrence in
77+
src/unicode_*"$unicode_source_sha"*) ;;
78+
*) printf '%s\n' "$occurrence" >> "$unexpected_hashes" ;;
79+
esac
80+
done <<< "$hash_output"
81+
if [[ -s "$unexpected_hashes" ]]; then
82+
cat "$unexpected_hashes" >&2
83+
die "production sources contain an unauthenticated test-shaped hash"
84+
fi
85+
;;
86+
1) ;;
87+
*)
88+
printf '%s\n' "$hash_output" >&2
89+
die "could not scan production sources for test-shaped hashes"
90+
;;
91+
esac
92+
93+
# Keep the patterns honest. Legitimate host vocabulary must stay allowed,
94+
# while each prohibited coupling class must have a positive canary.
95+
printf 'const HOST_NAME: &str = "$262";\n' > "$tmp/allowed.rs"
96+
! rg --quiet --pcre2 -- "$path_pattern|$source_hash_pattern|$source_literal_pattern|$source_probe_pattern|$filename_probe_pattern" "$tmp/allowed.rs" \
97+
|| die 'anti-cheat patterns reject the legitimate $262 host name'
98+
99+
printf '// language/statements/fixture-special-case.js\n' > "$tmp/path.rs"
100+
rg --quiet --pcre2 -- "$path_pattern" "$tmp/path.rs" \
101+
|| die "Test262 path canary escaped the anti-cheat pattern"
102+
103+
printf 'const FIXTURE_DIGEST: &str = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";\n' > "$tmp/hash.rs"
104+
rg --quiet --pcre2 -- "$hash_pattern" "$tmp/hash.rs" \
105+
|| die "test-shaped hash canary escaped the anti-cheat pattern"
106+
107+
printf 'if source.contains("this exact fixture body") {}\n' > "$tmp/source.rs"
108+
rg --quiet --pcre2 -- "$source_probe_pattern" "$tmp/source.rs" \
109+
|| die "source-special-casing canary escaped the anti-cheat pattern"
110+
111+
printf 'if source.trim().contains("this exact fixture body") {}\n' > "$tmp/source-chain.rs"
112+
rg --quiet --pcre2 -- "$source_probe_pattern" "$tmp/source-chain.rs" \
113+
|| die "chained source-special-casing canary escaped the anti-cheat pattern"
114+
115+
printf 'if "this exact fixture body" == source {}\n' > "$tmp/source-reverse.rs"
116+
rg --quiet --pcre2 -- "$source_literal_pattern" "$tmp/source-reverse.rs" \
117+
|| die "reversed source-special-casing canary escaped the anti-cheat pattern"
118+
119+
printf 'if source.content_hash() == 0xdeadbeef {}\n' > "$tmp/source-hash.rs"
120+
rg --quiet --pcre2 -- "$source_hash_pattern" "$tmp/source-hash.rs" \
121+
|| die "source-hash canary escaped the anti-cheat pattern"
122+
123+
echo "Production engine Test262 anti-cheat gate passed."

src/bin/run_test262.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,8 +1030,7 @@ fn run_coordinator(options: &CoordinatorOptions) -> Result<bool, String> {
10301030
| OxideProfileKind::DestructuringAssignmentGlobalCandidate
10311031
) {
10321032
return Err(format!(
1033-
"Test262 agent host opt-in is unavailable to profile {:?}",
1034-
oxide_profile_kind
1033+
"Test262 agent host opt-in is unavailable to profile {oxide_profile_kind:?}"
10351034
));
10361035
}
10371036
if !is_exact_agent_host_test(&relative, &source, &metadata)? {

src/bytecode.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ pub enum DefineMethodKind {
9090
/// nullish methods preserve the input value and return a true missing flag;
9191
/// present methods replace that value with their result and return false.
9292
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93+
#[allow(clippy::enum_variant_names)]
9394
pub enum IteratorCallKind {
9495
ReturnWithValue,
9596
ThrowWithValue,

src/runtime/intrinsics/date/calendar.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ mod tests {
451451

452452
#[test]
453453
fn maketime_and_makedate_preserve_specified_fp_evaluation_order() {
454-
// test262: built-ins/Date/UTC/fp-evaluation-order.js
454+
// Preserve the specified left-to-right IEEE-754 evaluation order.
455455
assert_eq!(
456456
set_date_fields_checked(
457457
[

src/runtime/intrinsics/date/parse.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,13 @@ fn get_time_zone_offset(
200200
if digit_count > 2 {
201201
minutes = hours % 100;
202202
hours /= 100;
203+
} else if skip_char(bytes, &mut position, b':') {
204+
minutes = get_digits(bytes, &mut position, 2, 2)?;
203205
} else {
204-
if skip_char(bytes, &mut position, b':') {
205-
minutes = get_digits(bytes, &mut position, 2, 2)?;
206-
} else {
207-
if strict {
208-
return None;
209-
}
210-
minutes = 0;
206+
if strict {
207+
return None;
211208
}
209+
minutes = 0;
212210
}
213211

214212
if hours > 23 || minutes > 59 {

src/runtime/module.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,10 +1198,8 @@ impl Runtime {
11981198
{
11991199
stack.pop();
12001200
completed = Some(ModuleExportResolveResult::Ambiguous);
1201-
} else {
1202-
if found.is_none() {
1203-
*found = Some(binding);
1204-
}
1201+
} else if found.is_none() {
1202+
*found = Some(binding);
12051203
}
12061204
}
12071205
ModuleExportResolveResult::Ambiguous => {

web/wasm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "quickjs-oxide-web"
33
version = "0.0.1"
44
edition = "2024"
5-
rust-version = "1.85"
5+
rust-version = "1.88"
66
publish = false
77

88
[lib]

0 commit comments

Comments
 (0)