Skip to content

Commit 6adae51

Browse files
committed
tests: add WASI integration test support via wasmtime
Add UUTESTS_WASM_RUNNER support to the test framework, enabling host-compiled integration tests to exercise a WASI binary through wasmtime. This runs ~1,230 existing tests across 26 tools without needing to compile the test harness for WASI.
1 parent 11f29dd commit 6adae51

8 files changed

Lines changed: 286 additions & 125 deletions

File tree

.github/workflows/wasi.yml

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,94 @@ jobs:
3232
run: |
3333
curl https://wasmtime.dev/install.sh -sSf | bash
3434
echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH
35-
- name: Run tests
35+
- name: Run unit tests
3636
env:
3737
CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime
3838
run: |
3939
# Get all utilities and exclude ones that don't compile for wasm32-wasip1
4040
EXCLUDE="dd|df|du|env|expr|mktemp|more|tac|test"
4141
UTILS=$(./util/show-utils.sh | tr ' ' '\n' | grep -vE "^($EXCLUDE)$" | sed 's/^/-p uu_/' | tr '\n' ' ')
4242
cargo test --target wasm32-wasip1 --no-default-features $UTILS
43+
- name: Run integration tests via wasmtime
44+
run: |
45+
# Build the WASI binary
46+
cargo build --target wasm32-wasip1 --no-default-features --features feat_wasm
47+
# Run host-compiled integration tests against the WASI binary.
48+
# TODO: add integration tests for these tools as WASI support is extended:
49+
# arch b2sum cat cksum cp csplit date dir dircolors fmt join ln
50+
# ls md5sum mkdir mv nproc pathchk pr printenv ptx pwd readlink
51+
# realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum
52+
# sha512sum shred sleep sort split tail touch tsort uname uniq
53+
# vdir yes
54+
UUTESTS_BINARY_PATH="$(pwd)/target/wasm32-wasip1/debug/coreutils.wasm" \
55+
UUTESTS_WASM_RUNNER=wasmtime \
56+
cargo test --test tests -- \
57+
test_base32:: test_base64:: test_basenc:: test_basename:: \
58+
test_comm:: test_cut:: test_dirname:: test_echo:: \
59+
test_expand:: test_factor:: test_false:: test_fold:: \
60+
test_head:: test_link:: test_nl:: test_numfmt:: \
61+
test_od:: test_paste:: test_printf:: test_shuf:: test_sum:: \
62+
test_tee:: test_tr:: test_true:: test_truncate:: \
63+
test_unexpand:: test_unlink:: test_wc:: \
64+
\
65+
`# WASI sandbox: host paths (/proc, /sys, /dev) not visible in guest` \
66+
--skip test_factor::test_parallel \
67+
--skip test_cut::test_too_large \
68+
--skip test_read_backwards_bytes_proc_fs_version \
69+
--skip test_read_backwards_bytes_proc_fs_modules \
70+
--skip test_read_backwards_lines_proc_fs_modules \
71+
--skip test_read_backwards_bytes_sys_kernel_profiling \
72+
--skip test_files_from_pseudo_filesystem \
73+
--skip test_files0_stops_after_stdout_write_error \
74+
--skip test_dev_zero_write_error_dev_full \
75+
--skip test_dev_zero_closed_pipe \
76+
--skip test_simd_respects_glibc_tunables \
77+
--skip test_comm::test_comm_anonymous_pipes \
78+
--skip test_comm::test_read_error \
79+
--skip test_base64::test_read_error \
80+
--skip test_expand::test_read_error \
81+
--skip test_unexpand::test_read_error \
82+
--skip test_od::test_skip_bytes_proc_file_without_seeking \
83+
--skip test_shuf::write_errors_are_reported \
84+
--skip test_tee::linux_only \
85+
--skip test_tee::test_readonly \
86+
\
87+
`# WASI spec: argv/filenames must be valid UTF-8` \
88+
--skip test_invalid_utf8_args \
89+
--skip test_dirname_non_utf8_paths \
90+
--skip test_trailing_dot_non_utf8 \
91+
--skip test_8bit_non_utf8_delimiter \
92+
--skip test_non_utf8_delimiter \
93+
--skip test_echo_invalid_unicode_in_arguments \
94+
--skip mb_invalid_unicode \
95+
--skip non_utf_8_input \
96+
--skip non_utf_8 \
97+
--skip test_octal_escape_ambiguous_followed_by_non_utf8 \
98+
--skip test_truncate_non_utf8_set \
99+
--skip test_output_lossy_utf8 \
100+
--skip non_utf8_paths \
101+
--skip non_utf8_filename \
102+
--skip non_utf8_name \
103+
--skip test_invalid_unicode_in_filename \
104+
--skip test_one_byte_section_delimiter \
105+
--skip test_section_delimiter_non_utf8 \
106+
--skip test_number_separator_non_utf8 \
107+
\
108+
`# WASI: no FIFO/mkfifo support` \
109+
--skip test_fifo_error_reference_file_only \
110+
--skip test_fifo_error_reference_and_size \
111+
--skip test_fifo_error_size_only \
112+
\
113+
`# WASI: no pipe/signal support` \
114+
--skip test_broken_pipe_no_error \
115+
--skip test_tee_output_not_buffered \
116+
\
117+
`# WASI: no subprocess spawning (test shells out or invokes another binary)` \
118+
--skip test_all_but_last_bytes_large_file_piped \
119+
--skip test_all_but_last_lines_large_file \
120+
--skip test_cmd_result_stderr_check_and_stderr_str_check \
121+
\
122+
`# WASI: stdin file position not preserved through wasmtime` \
123+
--skip test_od::test_read_bytes \
124+
--skip test_validate_stdin_offset_lines \
125+
--skip test_validate_stdin_offset_bytes

tests/by-util/test_basenc.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ fn test_z85_not_padded_encode() {
3939

4040
#[test]
4141
fn test_invalid_input() {
42-
let error_message = if cfg!(windows) {
43-
"basenc: .: Permission denied\n"
42+
let cmd = new_ucmd!().args(&["--base32", "."]).fails();
43+
if cfg!(windows) {
44+
cmd.stderr_only("basenc: .: Permission denied\n");
45+
} else if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
46+
// wasi-libc may report a different error string than the host libc
47+
cmd.stderr_contains("basenc: read error:");
4448
} else {
45-
"basenc: read error: Is a directory\n"
46-
};
47-
new_ucmd!()
48-
.args(&["--base32", "."])
49-
.fails()
50-
.stderr_only(error_message);
49+
cmd.stderr_only("basenc: read error: Is a directory\n");
50+
}
5151
}
5252

5353
#[test]

tests/by-util/test_comm.rs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -451,19 +451,23 @@ fn test_is_dir() {
451451

452452
#[test]
453453
fn test_sorted() {
454-
let expected_stderr =
455-
"comm: file 2 is not in sorted order\ncomm: input is not in sorted order\n";
456-
457454
let scene = TestScenario::new(util_name!());
458455
let at = &scene.fixtures;
459456
at.write("comm1", "1\n3");
460457
at.write("comm2", "3\n2");
461-
scene
462-
.ucmd()
463-
.args(&["comm1", "comm2"])
464-
.fails_with_code(1)
465-
.stdout_is("1\n\t\t3\n\t2\n")
466-
.stderr_is(expected_stderr);
458+
let cmd = scene.ucmd().args(&["comm1", "comm2"]).run();
459+
// WASI's strcoll (C locale only) may not detect unsorted input,
460+
// but the comparison output is still correct.
461+
if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
462+
cmd.success().stdout_is("1\n\t\t3\n\t2\n");
463+
} else {
464+
cmd.failure()
465+
.code_is(1)
466+
.stdout_is("1\n\t\t3\n\t2\n")
467+
.stderr_is(
468+
"comm: file 2 is not in sorted order\ncomm: input is not in sorted order\n",
469+
);
470+
}
467471
}
468472

469473
#[test]
@@ -490,16 +494,19 @@ fn test_both_inputs_out_of_order() {
490494
at.write("file_a", "3\n1\n0\n");
491495
at.write("file_b", "3\n2\n0\n");
492496

493-
scene
494-
.ucmd()
495-
.args(&["file_a", "file_b"])
496-
.fails_with_code(1)
497-
.stdout_is("\t\t3\n1\n0\n\t2\n\t0\n")
498-
.stderr_is(
499-
"comm: file 1 is not in sorted order\n\
500-
comm: file 2 is not in sorted order\n\
501-
comm: input is not in sorted order\n",
502-
);
497+
let cmd = scene.ucmd().args(&["file_a", "file_b"]).run();
498+
if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
499+
cmd.success().stdout_is("\t\t3\n1\n0\n\t2\n\t0\n");
500+
} else {
501+
cmd.failure()
502+
.code_is(1)
503+
.stdout_is("\t\t3\n1\n0\n\t2\n\t0\n")
504+
.stderr_is(
505+
"comm: file 1 is not in sorted order\n\
506+
comm: file 2 is not in sorted order\n\
507+
comm: input is not in sorted order\n",
508+
);
509+
}
503510
}
504511

505512
#[test]
@@ -509,16 +516,19 @@ fn test_both_inputs_out_of_order_last_pair() {
509516
at.write("file_a", "3\n1\n");
510517
at.write("file_b", "3\n2\n");
511518

512-
scene
513-
.ucmd()
514-
.args(&["file_a", "file_b"])
515-
.fails_with_code(1)
516-
.stdout_is("\t\t3\n1\n\t2\n")
517-
.stderr_is(
518-
"comm: file 1 is not in sorted order\n\
519-
comm: file 2 is not in sorted order\n\
520-
comm: input is not in sorted order\n",
521-
);
519+
let cmd = scene.ucmd().args(&["file_a", "file_b"]).run();
520+
if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
521+
cmd.success().stdout_is("\t\t3\n1\n\t2\n");
522+
} else {
523+
cmd.failure()
524+
.code_is(1)
525+
.stdout_is("\t\t3\n1\n\t2\n")
526+
.stderr_is(
527+
"comm: file 1 is not in sorted order\n\
528+
comm: file 2 is not in sorted order\n\
529+
comm: input is not in sorted order\n",
530+
);
531+
}
522532
}
523533

524534
#[test]

tests/by-util/test_od.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -764,31 +764,47 @@ fn test_invalid_traditional_offsets_are_filenames() {
764764

765765
#[test]
766766
fn test_traditional_offset_overflow_diagnosed() {
767+
// The ERANGE message differs across platforms (e.g. "Result too large"
768+
// on glibc vs "Result not representable" on wasi-libc), so when testing
769+
// via a WASM runner we just check that stderr mentions the input value.
770+
let wasm = std::env::var("UUTESTS_WASM_RUNNER").is_ok();
767771
let erange = erange_message();
768772
let long_octal = "7".repeat(255);
769773
let long_decimal = format!("{}.", "9".repeat(254));
770774
let long_hex = format!("0x{}", "f".repeat(253));
771775

772-
new_ucmd!()
776+
let cmd = new_ucmd!()
773777
.arg("-")
774778
.arg(&long_octal)
775779
.pipe_in(Vec::<u8>::new())
776-
.fails_with_code(1)
777-
.stderr_only(format!("od: {long_octal}: {erange}\n"));
780+
.fails_with_code(1);
781+
if wasm {
782+
cmd.stderr_contains(&format!("od: {long_octal}:"));
783+
} else {
784+
cmd.stderr_only(format!("od: {long_octal}: {erange}\n"));
785+
}
778786

779-
new_ucmd!()
787+
let cmd = new_ucmd!()
780788
.arg("-")
781789
.arg(&long_decimal)
782790
.pipe_in(Vec::<u8>::new())
783-
.fails_with_code(1)
784-
.stderr_only(format!("od: {long_decimal}: {erange}\n"));
791+
.fails_with_code(1);
792+
if wasm {
793+
cmd.stderr_contains(&format!("od: {long_decimal}:"));
794+
} else {
795+
cmd.stderr_only(format!("od: {long_decimal}: {erange}\n"));
796+
}
785797

786-
new_ucmd!()
798+
let cmd = new_ucmd!()
787799
.arg("-")
788800
.arg(&long_hex)
789801
.pipe_in(Vec::<u8>::new())
790-
.fails_with_code(1)
791-
.stderr_only(format!("od: {long_hex}: {erange}\n"));
802+
.fails_with_code(1);
803+
if wasm {
804+
cmd.stderr_contains(&format!("od: {long_hex}:"));
805+
} else {
806+
cmd.stderr_only(format!("od: {long_hex}: {erange}\n"));
807+
}
792808
}
793809

794810
#[test]

tests/by-util/test_tr.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@ fn test_invalid_input() {
2121
.fails_with_code(1)
2222
.stderr_contains("tr: extra operand '<'");
2323
#[cfg(unix)]
24-
new_ucmd!()
25-
.args(&["1", "1"])
26-
// will test "tr 1 1 < ."
27-
.set_stdin(std::process::Stdio::from(std::fs::File::open(".").unwrap()))
28-
.fails_with_code(1)
29-
.stderr_contains("tr: read error: Is a directory");
24+
{
25+
let cmd = new_ucmd!()
26+
.args(&["1", "1"])
27+
// will test "tr 1 1 < ."
28+
.set_stdin(std::process::Stdio::from(std::fs::File::open(".").unwrap()))
29+
.fails_with_code(1);
30+
if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
31+
// On WASI the fluent translation key may appear instead of the
32+
// translated text, but the OS error string is still present.
33+
cmd.stderr_contains("Is a directory");
34+
} else {
35+
cmd.stderr_contains("tr: read error: Is a directory");
36+
}
37+
}
3038
}
3139

3240
#[test]

0 commit comments

Comments
 (0)