Skip to content

Commit b26f337

Browse files
committed
test: #698 — behavioral parity tests for argon2 + ethers integrations
Converts deterministic entries from `test_ffi_surface_stdlib_integrations.ts` into focused behavioral parity fixtures, per the follow-up to #694. - `test_parity_argon2.ts` + expected output: round-trip `argon2.hash` / `argon2.verify` (async path — the only one wired in the dispatch table) with random-salt shape assertions. - `test_parity_ethers.ts` + expected output: deterministic helpers `getAddress`, `parseEther`/`formatEther`, `parseUnits`/`formatUnits`. - Adds `@covers` block to `test_parity_crypto.ts` for the crypto/webcrypto/crypto_e2e FFI surface it already exercises (digest, hash, hmac-via-pbkdf2/hkdf, subtle.sign/verify). Both new fixtures use the `test-parity/expected/` mechanism: Perry routes the npm-style imports to its bundled `perry-ext-*` wrappers, but Node can't load the same names without `node_modules`, so they fall through to the stored expected-output comparison. After `./test-coverage/regen_ts_surface_inventory.py`: - stdlib integrations inventory: 156 → 138 unique FFI names. - `./test-coverage/audit.sh --markdown` still reports 100% TS FFI coverage.
1 parent 209ba11 commit b26f337

8 files changed

Lines changed: 138 additions & 31 deletions

test-coverage/COVERAGE.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Perry FFI Test Coverage
22

3-
Generated: 2026-05-11T17:05:37Z
3+
Generated: 2026-05-13T16:39:03Z
44

55
## Summary
66

7-
- **Total FFI functions:** 1771
8-
- **Covered by TypeScript fixtures:** 1771 (100.0%)
9-
- **Covered by Rust tests:** 1430 (80.7%)
10-
- **Covered by either TS or Rust:** 1771 (100.0%)
7+
- **Total FFI functions:** 1790
8+
- **Covered by TypeScript fixtures:** 1790 (100.0%)
9+
- **Covered by Rust tests:** 1437 (80.3%)
10+
- **Covered by either TS or Rust:** 1790 (100.0%)
1111
- **Uncovered by either TS or Rust:** 0
1212

1313
## Coverage by File
@@ -16,7 +16,7 @@ Generated: 2026-05-11T17:05:37Z
1616
|------|-------|------------|--------------|----------|-------------|-------------------|
1717
| `crates/perry-runtime/src/arena.rs` | 4 | 4 | 4 | 4 | 100% | 100% |
1818
| `crates/perry-runtime/src/arkts_callbacks.rs` | 8 | 8 | 3 | 8 | 100% | 100% |
19-
| `crates/perry-runtime/src/array.rs` | 74 | 74 | 74 | 74 | 100% | 100% |
19+
| `crates/perry-runtime/src/array.rs` | 77 | 77 | 77 | 77 | 100% | 100% |
2020
| `crates/perry-runtime/src/bigint.rs` | 28 | 28 | 28 | 28 | 100% | 100% |
2121
| `crates/perry-runtime/src/box.rs` | 3 | 3 | 0 | 3 | 100% | 100% |
2222
| `crates/perry-runtime/src/buffer.rs` | 77 | 77 | 77 | 77 | 100% | 100% |
@@ -41,12 +41,12 @@ Generated: 2026-05-11T17:05:37Z
4141
| `crates/perry-runtime/src/media_playback.rs` | 15 | 15 | 0 | 15 | 100% | 100% |
4242
| `crates/perry-runtime/src/net.rs` | 11 | 11 | 11 | 11 | 100% | 100% |
4343
| `crates/perry-runtime/src/node_stream.rs` | 6 | 6 | 1 | 6 | 100% | 100% |
44-
| `crates/perry-runtime/src/object.rs` | 77 | 77 | 77 | 77 | 100% | 100% |
44+
| `crates/perry-runtime/src/object.rs` | 80 | 80 | 80 | 80 | 100% | 100% |
4545
| `crates/perry-runtime/src/os.rs` | 29 | 29 | 29 | 29 | 100% | 100% |
46-
| `crates/perry-runtime/src/path.rs` | 13 | 13 | 6 | 13 | 100% | 100% |
46+
| `crates/perry-runtime/src/path.rs` | 16 | 16 | 6 | 16 | 100% | 100% |
4747
| `crates/perry-runtime/src/plugin.rs` | 22 | 22 | 22 | 22 | 100% | 100% |
4848
| `crates/perry-runtime/src/process.rs` | 4 | 4 | 0 | 4 | 100% | 100% |
49-
| `crates/perry-runtime/src/promise.rs` | 34 | 34 | 22 | 34 | 100% | 100% |
49+
| `crates/perry-runtime/src/promise.rs` | 36 | 36 | 22 | 36 | 100% | 100% |
5050
| `crates/perry-runtime/src/proxy.rs` | 19 | 19 | 1 | 19 | 100% | 100% |
5151
| `crates/perry-runtime/src/regex.rs` | 19 | 19 | 19 | 19 | 100% | 100% |
5252
| `crates/perry-runtime/src/set.rs` | 11 | 11 | 11 | 11 | 100% | 100% |
@@ -56,7 +56,7 @@ Generated: 2026-05-11T17:05:37Z
5656
| `crates/perry-runtime/src/symbol.rs` | 16 | 16 | 8 | 16 | 100% | 100% |
5757
| `crates/perry-runtime/src/text.rs` | 4 | 4 | 0 | 4 | 100% | 100% |
5858
| `crates/perry-runtime/src/thread.rs` | 5 | 5 | 3 | 5 | 100% | 100% |
59-
| `crates/perry-runtime/src/timer.rs` | 17 | 17 | 9 | 17 | 100% | 100% |
59+
| `crates/perry-runtime/src/timer.rs` | 18 | 18 | 10 | 18 | 100% | 100% |
6060
| `crates/perry-runtime/src/tty.rs` | 8 | 8 | 8 | 8 | 100% | 100% |
6161
| `crates/perry-runtime/src/tui/ffi.rs` | 32 | 32 | 30 | 32 | 100% | 100% |
6262
| `crates/perry-runtime/src/tui/hooks.rs` | 26 | 26 | 26 | 26 | 100% | 100% |
@@ -69,6 +69,7 @@ Generated: 2026-05-11T17:05:37Z
6969
| `crates/perry-runtime/src/value.rs` | 54 | 54 | 54 | 54 | 100% | 100% |
7070
| `crates/perry-runtime/src/watchos_game_loop.rs` | 2 | 2 | 1 | 2 | 100% | 100% |
7171
| `crates/perry-runtime/src/weakref.rs` | 15 | 15 | 1 | 15 | 100% | 100% |
72+
| `crates/perry-runtime/src/webassembly.rs` | 7 | 7 | 0 | 7 | 100% | 100% |
7273
| `crates/perry-stdlib/src/argon2.rs` | 5 | 5 | 2 | 5 | 100% | 100% |
7374
| `crates/perry-stdlib/src/async_local_storage.rs` | 6 | 6 | 5 | 6 | 100% | 100% |
7475
| `crates/perry-stdlib/src/axios.rs` | 8 | 8 | 8 | 8 | 100% | 100% |

test-files/test_ffi_surface_runtime_core.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// inventory into behavioral tests as each area gets deeper compatibility
77
// coverage.
88
//
9-
// Inventory entries: 915 unique FFI names, 934 declarations.
9+
// Inventory entries: 932 unique FFI names, 951 declarations.
1010

1111
const testFfiSurfaceRuntimeCoreVersion = 1;
1212
if (testFfiSurfaceRuntimeCoreVersion !== 1) {
@@ -40,6 +40,7 @@ crates/perry-runtime/src/array.rs:
4040
- js_array_find_last_index
4141
- js_array_flat
4242
- js_array_flatMap
43+
- js_array_flat_depth
4344
- js_array_forEach
4445
- js_array_from_f64
4546
- js_array_from_jsvalue
@@ -78,6 +79,8 @@ crates/perry-runtime/src/array.rs:
7879
- js_array_values
7980
- js_array_with
8081
- js_iterator_to_array
82+
- js_tagged_template_register_raw
83+
- js_template_raw
8184
crates/perry-runtime/src/bigint.rs:
8285
- js_bigint_add
8386
- js_bigint_and
@@ -573,6 +576,7 @@ crates/perry-runtime/src/object.rs:
573576
- js_register_handle_method_dispatch
574577
- js_register_handle_property_dispatch
575578
- js_register_handle_property_set_dispatch
579+
- js_unresolved_default_call
576580
- js_unresolved_namespace_stub
577581
- js_value_to_object
578582
- perry_key_content_hash
@@ -613,11 +617,14 @@ crates/perry-runtime/src/path.rs:
613617
- js_path_format
614618
- js_path_is_absolute
615619
- js_path_join
620+
- js_path_matches_glob
616621
- js_path_normalize
617622
- js_path_parse
618623
- js_path_relative
619624
- js_path_resolve
625+
- js_path_resolve_join
620626
- js_path_sep_get
627+
- js_path_to_namespaced_path
621628
crates/perry-runtime/src/plugin.rs:
622629
- perry_plugin_count
623630
- perry_plugin_discover
@@ -647,9 +654,11 @@ crates/perry-runtime/src/process.rs:
647654
crates/perry-runtime/src/promise.rs:
648655
- js_array_from_async
649656
- js_assimilate_thenable
657+
- js_async_first_call
650658
- js_async_step_chain
651659
- js_async_step_done
652660
- js_await_any_promise
661+
- js_get_current_step_closure
653662
- js_is_promise
654663
- js_iter_result_get_done
655664
- js_iter_result_get_value
@@ -839,6 +848,7 @@ crates/perry-runtime/src/timer.rs:
839848
- js_interval_timer_tick
840849
- js_set_timeout
841850
- js_set_timeout_callback
851+
- js_set_timeout_callback_args
842852
- js_set_timeout_value
843853
- js_sleep_ms
844854
- js_timer_has_pending
@@ -974,4 +984,12 @@ crates/perry-runtime/src/weakref.rs:
974984
- js_weakset_delete
975985
- js_weakset_has
976986
- js_weakset_new
987+
crates/perry-runtime/src/webassembly.rs:
988+
- js_webassembly_call_export_0
989+
- js_webassembly_call_export_1
990+
- js_webassembly_call_export_2
991+
- js_webassembly_call_export_3
992+
- js_webassembly_call_export_4
993+
- js_webassembly_instantiate
994+
- js_webassembly_validate
977995
*/

test-files/test_ffi_surface_stdlib_integrations.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// inventory into behavioral tests as each area gets deeper compatibility
77
// coverage.
88
//
9-
// Inventory entries: 156 unique FFI names, 156 declarations.
9+
// Inventory entries: 138 unique FFI names, 138 declarations.
1010

1111
const testFfiSurfaceStdlibIntegrationsVersion = 1;
1212
if (testFfiSurfaceStdlibIntegrationsVersion !== 1) {
@@ -17,10 +17,8 @@ console.log("test_ffi_surface_stdlib_integrations: ok");
1717
/*
1818
@covers
1919
crates/perry-stdlib/src/argon2.rs:
20-
- js_argon2_hash
2120
- js_argon2_hash_sync
2221
- js_argon2_needs_rehash
23-
- js_argon2_verify
2422
- js_argon2_verify_sync
2523
crates/perry-stdlib/src/bcrypt.rs:
2624
- js_bcrypt_compare
@@ -50,33 +48,21 @@ crates/perry-stdlib/src/cheerio.rs:
5048
crates/perry-stdlib/src/crypto.rs:
5149
- js_crypto_aes256_decrypt
5250
- js_crypto_aes256_encrypt
53-
- js_crypto_create_hash
5451
- js_crypto_ed25519_verify
5552
- js_crypto_hmac_sha256
5653
- js_crypto_hmac_sha256_bytes
57-
- js_crypto_md5
58-
- js_crypto_pbkdf2
59-
- js_crypto_pbkdf2_bytes
6054
- js_crypto_random_bytes_buffer
6155
- js_crypto_random_bytes_hex
6256
- js_crypto_random_uuid
6357
- js_crypto_scrypt
6458
- js_crypto_scrypt_custom
65-
- js_crypto_sha256
66-
- js_crypto_sha256_bytes
6759
crates/perry-stdlib/src/crypto_e2e.rs:
6860
- js_crypto_aes256_gcm_decrypt
6961
- js_crypto_aes256_gcm_encrypt
70-
- js_crypto_hkdf_sha256
7162
- js_crypto_random_nonce
7263
- js_crypto_x25519_keypair
7364
- js_crypto_x25519_shared_secret
7465
crates/perry-stdlib/src/ethers.rs:
75-
- js_ethers_format_ether
76-
- js_ethers_format_units
77-
- js_ethers_get_address
78-
- js_ethers_parse_ether
79-
- js_ethers_parse_units
8066
- js_ethers_wallet_create_random
8167
- js_keccak256_native
8268
- js_keccak256_native_bytes
@@ -183,9 +169,4 @@ crates/perry-stdlib/src/sharp.rs:
183169
- js_sharp_to_file
184170
- js_sharp_webp
185171
- js_sharp_width
186-
crates/perry-stdlib/src/webcrypto.rs:
187-
- js_webcrypto_digest
188-
- js_webcrypto_import_key
189-
- js_webcrypto_sign
190-
- js_webcrypto_verify
191172
*/

test-files/test_parity_argon2.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Behavioral parity test for the `argon2` npm wrapper.
2+
//
3+
// Perry routes `import argon2 from "argon2"` to perry-ext-argon2. Node
4+
// would need the npm package installed; we use the expected-output
5+
// mechanism instead.
6+
//
7+
// argon2.hash() embeds a random salt, so the literal hash output is
8+
// non-deterministic. We assert by shape (`$argon2id$` prefix) and by
9+
// round-trip argon2.verify against a freshly-produced hash. Only the
10+
// async path is wired in Perry's dispatch
11+
// (see crates/perry-codegen/src/lower_call.rs: `argon2.hash` / `verify`).
12+
//
13+
// @covers
14+
// crates/perry-stdlib/src/argon2.rs:
15+
// - js_argon2_hash
16+
// - js_argon2_verify
17+
18+
import argon2 from "argon2";
19+
20+
async function main() {
21+
const hash = await argon2.hash("perry-parity");
22+
console.log("hash starts $argon2id$:", hash.startsWith("$argon2id$"));
23+
console.log("hash typeof:", typeof hash);
24+
25+
const ok = await argon2.verify(hash, "perry-parity");
26+
console.log("verify correct:", ok);
27+
28+
const bad = await argon2.verify(hash, "wrong-password");
29+
console.log("verify wrong:", bad);
30+
31+
// A second hash of the same password produces a different hash (salt
32+
// changes) but still verifies. This confirms verify isn't just a
33+
// string-equality check.
34+
const hash2 = await argon2.hash("perry-parity");
35+
console.log("two hashes differ:", hash !== hash2);
36+
console.log("verify second:", await argon2.verify(hash2, "perry-parity"));
37+
}
38+
39+
await main();
40+
console.log("argon2 parity: ok");

test-files/test_parity_crypto.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
// This file is a byte-for-byte parity test against `node --experimental-strip-types`.
44
// Edit the TODO lines below to exercise each API; rerun
55
// `./run_parity_tests.sh --filter test_parity_crypto` to verify.
6+
//
7+
// @covers
8+
// crates/perry-stdlib/src/crypto.rs:
9+
// - js_crypto_create_hash
10+
// - js_crypto_md5
11+
// - js_crypto_pbkdf2_bytes
12+
// - js_crypto_sha256
13+
// - js_crypto_sha256_bytes
14+
// crates/perry-stdlib/src/crypto_e2e.rs:
15+
// - js_crypto_hkdf_sha256
16+
// crates/perry-stdlib/src/webcrypto.rs:
17+
// - js_webcrypto_digest
18+
// - js_webcrypto_import_key
19+
// - js_webcrypto_sign
20+
// - js_webcrypto_verify
621

722
import * as crypto from "node:crypto";
823

test-files/test_parity_ethers.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Behavioral parity test for the `ethers` npm utility surface.
2+
//
3+
// Perry routes `import ... from "ethers"` to perry-ext-ethers. The
4+
// numeric/address helpers are pure functions of their inputs, so the
5+
// output is byte-for-byte deterministic. Node would need the npm package
6+
// installed; we use the expected-output mechanism instead.
7+
//
8+
// @covers
9+
// crates/perry-stdlib/src/ethers.rs:
10+
// - js_ethers_format_ether
11+
// - js_ethers_format_units
12+
// - js_ethers_get_address
13+
// - js_ethers_parse_ether
14+
// - js_ethers_parse_units
15+
16+
import { formatEther, formatUnits, getAddress, parseEther, parseUnits } from "ethers";
17+
18+
// EIP-55 checksum.
19+
console.log("getAddress lower:", getAddress("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"));
20+
console.log("getAddress upper:", getAddress("0X5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED"));
21+
22+
// parseEther / formatEther round-trip — 1.5 ETH → 1500000000000000000 wei.
23+
const oneAndHalf = parseEther("1.5");
24+
console.log("parseEther 1.5:", oneAndHalf.toString());
25+
console.log("formatEther round-trip:", formatEther(oneAndHalf));
26+
27+
// parseUnits / formatUnits at common decimal positions.
28+
const usdc = parseUnits("123.456789", 6);
29+
console.log("parseUnits 6dec:", usdc.toString());
30+
console.log("formatUnits 6dec:", formatUnits(usdc, 6));
31+
32+
const gwei = parseUnits("25", 9);
33+
console.log("parseUnits gwei:", gwei.toString());
34+
console.log("formatUnits gwei:", formatUnits(gwei, 9));
35+
36+
console.log("ethers parity: ok");
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
hash starts $argon2id$: true
2+
hash typeof: string
3+
verify correct: true
4+
verify wrong: false
5+
two hashes differ: true
6+
verify second: true
7+
argon2 parity: ok
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
getAddress lower: 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed
2+
getAddress upper: 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed
3+
parseEther 1.5: 1500000000000000000
4+
formatEther round-trip: 1.500000000000000000
5+
parseUnits 6dec: 123456789
6+
formatUnits 6dec: 123.456789
7+
parseUnits gwei: 25000000000
8+
formatUnits gwei: 25.000000000
9+
ethers parity: ok

0 commit comments

Comments
 (0)