Skip to content

Commit a1be0b5

Browse files
committed
Improve test runner ergonomics
Add `./run-tests` executable that doesn't require arguments, automatically detecting available runtimes and tests. Simplify `README.md` accordingly. Console test case reporter more concise and useful: - Single line to indicate total results - One copy-pastable command line per failure - Requires refactors to record command lines, remove cwd change Allow testing multiple engines in one run. - Required changing `TEST_RUNTIME_EXE` environment variable convention to one env var per executable - Requires refactor to collect which runtime ran a testcase
1 parent 852d8e7 commit a1be0b5

File tree

17 files changed

+267
-141
lines changed

17 files changed

+267
-141
lines changed

README.md

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,31 @@ executor is quite simple; see the [specification] document for the details and t
4444
5. Execute the test suites from this repository:
4545

4646
```bash
47-
python3 test-runner/wasi_test_runner.py \
48-
-t ./tests/assemblyscript/testsuite/ `# path to folders containing .wasm test files` \
49-
./tests/c/testsuite/wasm32-wasip1 \
50-
./tests/rust/testsuite/ \
51-
-r adapters/wasmtime.py # path to a runtime adapter
47+
./run-tests
5248
```
5349

54-
Optionally you can specify test cases to skip with the `--exclude-filter` option.
50+
By default, the test runner will detect available WASI runtimes from
51+
those available in [adapters/](adapters/), and will run tests on all
52+
available runtimes. Pass `--runtime` to instead use a specific runtime.
53+
54+
```
55+
./run-tests --runtime adapters/wasmtime.py
56+
```
57+
58+
Running tests will invoke the WASI runtime's binary in a subprocess:
59+
`wasmtime` for `adapters/wasmtime.py`, `iwasm` for
60+
`adapters/wasm-micro-runtime.py`, and so on. These binaries can be
61+
overridden by setting corresponding environment variables (`WASMTIME`,
62+
`IWASM`, etc):
5563

56-
```bash
57-
python3 test-runner/wasi_test_runner.py \
58-
-t ./tests/assemblyscript/testsuite/ `# path to folders containing .wasm test files` \
59-
./tests/c/testsuite/wasm32-wasip1 \
60-
./tests/rust/testsuite/ \
61-
--exclude-filter examples/skip.json \
62-
-r adapters/wasmtime.py # path to a runtime adapter
64+
```
65+
WASMTIME="wasmtime --wasm-features all" ./run-tests
6366
```
6467

65-
The default executable in the adapter used for test execution can be
66-
overridden using `TEST_RUNTIME_EXE` variable. This only works with adapters defined in
67-
[adapters/](adapters/), and might not work with 3rd party adapters.
68+
Optionally you can specify test cases to skip with the `--exclude-filter` option.
6869

6970
```bash
70-
TEST_RUNTIME_EXE="wasmtime --wasm-features all" python3 test-runner/wasi_test_runner.py \
71-
-t ./tests/assemblyscript/testsuite/ \
72-
-r adapters/wasmtime.py
71+
./run-tests --exclude-filter examples/skip.json \
7372
```
7473

7574
## Contributing

adapters/wasm-micro-runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shlex
66

77
# shlex.split() splits according to shell quoting rules
8-
IWASM = shlex.split(os.getenv("TEST_RUNTIME_EXE", "iwasm"))
8+
IWASM = shlex.split(os.getenv("IWASM", "iwasm"))
99

1010
parser = argparse.ArgumentParser()
1111
parser.add_argument("--version", action="store_true")
@@ -23,7 +23,7 @@
2323
TEST_FILE = args.test_file
2424
PROG_ARGS = args.arg
2525
ENV_ARGS = [f"--env={i}" for i in args.env]
26-
DIR_ARGS = [f"--dir={i}" for i in args.dir]
26+
DIR_ARGS = [f"--map-dir={i}" for i in args.dir]
2727

2828
r = subprocess.run(IWASM + ENV_ARGS + DIR_ARGS + [TEST_FILE] + PROG_ARGS)
2929
sys.exit(r.returncode)

adapters/wasmtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shlex
66

77
# shlex.split() splits according to shell quoting rules
8-
WASMTIME = shlex.split(os.getenv("TEST_RUNTIME_EXE", "wasmtime"))
8+
WASMTIME = shlex.split(os.getenv("WASMTIME", "wasmtime"))
99

1010
parser = argparse.ArgumentParser()
1111
parser.add_argument("--version", action="store_true")

adapters/wizard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shlex
66

77
# shlex.split() splits according to shell quoting rules
8-
WIZARD = shlex.split(os.getenv("TEST_RUNTIME_EXE", "wizeng.x86-64-linux"))
8+
WIZARD = shlex.split(os.getenv("WIZARD", "wizeng.x86-64-linux"))
99

1010
parser = argparse.ArgumentParser()
1111
parser.add_argument("--version", action="store_true")

run-tests

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import subprocess
5+
import sys
6+
from typing import List
7+
from pathlib import Path
8+
9+
sys.path.insert(0, str(Path(__file__).parent / "test-runner"))
10+
11+
from wasi_test_runner.harness import run_tests
12+
from wasi_test_runner.runtime_adapter import RuntimeAdapter
13+
14+
parser = argparse.ArgumentParser(
15+
description="WASI test runner"
16+
)
17+
18+
parser.add_argument(
19+
"-f",
20+
"--exclude-filter",
21+
action="append",
22+
default=[],
23+
help="Path to JSON file indicating tests to exclude.",
24+
)
25+
parser.add_argument(
26+
"-r", "--runtime-adapter", help="Path to a runtime adapter."
27+
)
28+
parser.add_argument(
29+
"--json-output-location",
30+
help="JSON test result destination. If not specified, JSON output won't be generated.",
31+
)
32+
parser.add_argument(
33+
"--disable-colors",
34+
action="store_true",
35+
default=False,
36+
help="Disables color for console output reporter.",
37+
)
38+
39+
def find_test_dirs(root):
40+
test_dirs = []
41+
for root, dirs, files in root.walk(on_error=print):
42+
if "manifest.json" in files:
43+
test_dirs.append(root)
44+
return test_dirs
45+
46+
def find_runtime_adapters(root, verbose=False):
47+
print(f"Detecting WASI runtime availability:")
48+
adapters = []
49+
for candidate in root.glob("*.py"):
50+
adapter = RuntimeAdapter(candidate)
51+
try:
52+
print(f" {candidate.name}: {adapter.get_version()}")
53+
adapters.append(adapter)
54+
except subprocess.CalledProcessError as e:
55+
print(f" {candidate.name}: unavailable; pass `--runtime {candidate}` to debug.")
56+
print("")
57+
if len(adapters) == 0:
58+
print("Error: No WASI runtimes found")
59+
sys.exit(1)
60+
return adapters
61+
62+
options = parser.parse_args()
63+
test_suite = find_test_dirs(Path(__file__).parent / "tests")
64+
if options.runtime_adapter:
65+
runtime_adapters = [RuntimeAdapter(options.runtime_adapter)]
66+
# Ensure it works.
67+
try:
68+
runtime_adapters[0].get_version()
69+
except subprocess.CalledProcessError as e:
70+
print(f"Error: failed to load {options.runtime_adapter}:")
71+
print(f" Failed command line: {' '.join(e.cmd)}")
72+
if e.stdout.strip() != "":
73+
print(f" stdout:\n{e.stdout}")
74+
if e.stderr.strip() != "":
75+
print(f" stderr:\n{e.stderr}")
76+
sys.exit(1)
77+
else:
78+
runtime_adapters = find_runtime_adapters(Path(__file__).parent / "adapters")
79+
80+
exclude_filters = [JSONTestExcludeFilter(filt) for filt in options.exclude_filter]
81+
82+
sys.exit(run_tests(runtime_adapters, test_suite,
83+
color=not options.disable_colors,
84+
json_log_file=options.json_output_location,
85+
exclude_filters=exclude_filters))

test-runner/tests/test_test_suite.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import wasi_test_runner.test_case as tc
44
import wasi_test_runner.test_suite as ts
5+
from wasi_test_runner.runtime_adapter import RuntimeVersion
56

67

78
def create_test_case(name: str, is_executed: bool, is_failed: bool) -> tc.TestCase:
89
failures = [tc.Failure("a", "b")] if is_failed else []
910
return tc.TestCase(
1011
name,
12+
["test-runtime-exe", name],
1113
tc.Config(),
1214
tc.Result(tc.Output(0, "", ""), is_executed, failures),
1315
1.0,
@@ -17,6 +19,7 @@ def create_test_case(name: str, is_executed: bool, is_failed: bool) -> tc.TestCa
1719
def test_test_suite_should_return_correct_count() -> None:
1820
suite = ts.TestSuite(
1921
"suite",
22+
RuntimeVersion("test-runtime", "3.14"),
2023
10.0,
2124
datetime.now(),
2225
[

test-runner/tests/test_test_suite_runner.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import wasi_test_runner.test_case as tc
55
import wasi_test_runner.test_suite_runner as tsr
6+
from wasi_test_runner.runtime_adapter import RuntimeVersion
67

78

89
def get_mock_open() -> Mock:
@@ -45,14 +46,20 @@ def test_runner_end_to_end() -> None:
4546
tc.Config(stdout="output", env={"x": "1"}),
4647
]
4748

49+
runtime_name = "rt1"
50+
runtime_version_str = "4.2"
51+
runtime_version = RuntimeVersion(runtime_name, runtime_version_str)
52+
4853
expected_test_cases = [
49-
tc.TestCase(test_name, config, result, ANY)
54+
tc.TestCase(test_name, [runtime_name, test_name], config, result, ANY)
5055
for config, test_name, result in zip(
5156
expected_config, ["test1", "test2", "test3"], expected_results
5257
)
5358
]
5459

5560
runtime = Mock()
61+
runtime.get_name.return_value = runtime_name
62+
runtime.get_version.return_value = runtime_version
5663
runtime.run_test.side_effect = outputs
5764

5865
validators = [
@@ -67,7 +74,10 @@ def test_runner_end_to_end() -> None:
6774
filters = [filt]
6875

6976
with patch("glob.glob", return_value=test_paths):
70-
suite = tsr.run_tests_from_test_suite("my-path", runtime, validators, reporters, filters) # type: ignore
77+
suite = tsr.run_tests_from_test_suite("my-path", runtime,
78+
validators, # type: ignore
79+
reporters, # type: ignore
80+
filters) # type: ignore
7181

7282
# Assert manifest was read correctly
7383
assert suite.name == "test-suite"
@@ -99,7 +109,8 @@ def test_runner_end_to_end() -> None:
99109
for filt in filters:
100110
assert filt.should_skip.call_count == 3
101111
for test_case in expected_test_cases:
102-
filt.should_skip.assert_any_call(suite.name, test_case.name)
112+
filt.should_skip.assert_any_call(runtime, suite.name,
113+
test_case.name)
103114

104115

105116
@patch("os.path.exists", Mock(return_value=False))

test-runner/wasi_test_runner/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def main() -> int:
6060
filters.append(JSONTestExcludeFilter(filt))
6161

6262
return run_all_tests(
63-
RuntimeAdapter(options.runtime_adapter),
63+
[RuntimeAdapter(options.runtime_adapter)],
6464
options.test_suite,
6565
validators,
6666
reporters,

test-runner/wasi_test_runner/filters.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
import json
66

7+
from .runtime_adapter import RuntimeVersion
8+
79

810
class TestFilter(ABC):
911
@abstractmethod
1012
def should_skip(
11-
self, test_suite_name: str, test_name: str
13+
self, runtime: RuntimeVersion, test_suite_name: str, test_name: str
1214
) -> Union[Tuple[Literal[True], str], Tuple[Literal[False], Literal[None]]]:
1315
pass
1416

@@ -19,7 +21,7 @@ def __init__(self, filename: str) -> None:
1921
self.filter_dict = json.load(file)
2022

2123
def should_skip(
22-
self, test_suite_name: str, test_name: str
24+
self, runtime: RuntimeVersion, test_suite_name: str, test_name: str
2325
) -> Union[Tuple[Literal[True], str], Tuple[Literal[False], Literal[None]]]:
2426
test_suite_filter = self.filter_dict.get(test_suite_name)
2527
if test_suite_filter is None:
Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
from typing import List
2+
from pathlib import Path
23

3-
from .filters import TestFilter
4+
from .filters import TestFilter, JSONTestExcludeFilter
45
from .reporters import TestReporter
6+
from .reporters.console import ConsoleTestReporter
7+
from .reporters.json import JSONTestReporter
58
from .test_suite_runner import run_tests_from_test_suite
69
from .runtime_adapter import RuntimeAdapter
7-
from .validators import Validator
10+
from .validators import exit_code_validator, stdout_validator, Validator
11+
12+
13+
def run_tests(runtimes: List[RuntimeAdapter],
14+
test_suite_paths: List[Path],
15+
exclude_filters: List[Path] = [],
16+
color: bool = True,
17+
json_log_file: str | None = None) -> int:
18+
validators: List[Validator] = [exit_code_validator, stdout_validator]
19+
reporters: List[TestReporter] = [ConsoleTestReporter(color)]
20+
if json_log_file:
21+
reporters.append(JSONTestReporter(json_log_file))
22+
filters: List[TestFilter] = [JSONTestExcludeFilter(str(filt))
23+
for filt in exclude_filters]
24+
25+
return run_all_tests(runtimes, [str(p) for p in test_suite_paths],
26+
validators, reporters, filters)
827

928

1029
def run_all_tests(
11-
runtime: RuntimeAdapter,
30+
runtimes: List[RuntimeAdapter],
1231
test_suite_paths: List[str],
1332
validators: List[Validator],
1433
reporters: List[TestReporter],
@@ -17,15 +36,16 @@ def run_all_tests(
1736
ret = 0
1837

1938
for test_suite_path in test_suite_paths:
20-
test_suite = run_tests_from_test_suite(
21-
test_suite_path, runtime, validators, reporters, filters,
22-
)
23-
for reporter in reporters:
24-
reporter.report_test_suite(test_suite)
25-
if test_suite.fail_count > 0:
26-
ret = 1
39+
for runtime in runtimes:
40+
test_suite = run_tests_from_test_suite(
41+
test_suite_path, runtime, validators, reporters, filters,
42+
)
43+
for reporter in reporters:
44+
reporter.report_test_suite(test_suite)
45+
if test_suite.fail_count > 0:
46+
ret = 1
2747

2848
for reporter in reporters:
29-
reporter.finalize(runtime.get_version())
49+
reporter.finalize()
3050

3151
return ret

0 commit comments

Comments
 (0)