Skip to content

Commit 064f5ef

Browse files
committed
test runner: Report actual argv on errors
Instead of having the adapter files be top-level python scripts, now they are modules with exported functions. One of those functions computes the argv to execute when running a test. Also, clean up directories before and after each test, in addition to before running the suite. This makes one failure to clean up not cause other cascading failures, and makes it more reliable to be able to run test cases outside the test-runner harness.
1 parent 52c5c5e commit 064f5ef

File tree

6 files changed

+192
-102
lines changed

6 files changed

+192
-102
lines changed

adapters/wasm-micro-runtime.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
1-
import argparse
21
import subprocess
3-
import sys
42
import os
53
import shlex
4+
from pathlib import Path
5+
from typing import Dict, List, Tuple
66

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

10-
parser = argparse.ArgumentParser()
11-
parser.add_argument("--version", action="store_true")
12-
parser.add_argument("--test-file", action="store")
13-
parser.add_argument("--arg", action="append", default=[])
14-
parser.add_argument("--env", action="append", default=[])
15-
parser.add_argument("--dir", action="append", default=[])
1610

17-
args = parser.parse_args()
11+
def get_name() -> str:
12+
return "wamr"
1813

19-
if args.version:
20-
subprocess.run(IWASM + ["--version"])
21-
sys.exit(0)
2214

23-
TEST_FILE = args.test_file
24-
PROG_ARGS = args.arg
25-
ENV_ARGS = [f"--env={i}" for i in args.env]
26-
DIR_ARGS = [f"--map-dir={i}" for i in args.dir]
15+
def get_version() -> str:
16+
# ensure no args when version is queried
17+
result = subprocess.run(IWASM + ["--version"],
18+
encoding="UTF-8", capture_output=True,
19+
check=True)
20+
output = result.stdout.splitlines()[0].split(" ")
21+
return output[1]
2722

28-
r = subprocess.run(IWASM + ENV_ARGS + DIR_ARGS + [TEST_FILE] + PROG_ARGS)
29-
sys.exit(r.returncode)
23+
24+
def compute_argv(test_path: str,
25+
args: List[str],
26+
env: Dict[str, str],
27+
dirs: List[Tuple[Path, str]]) -> List[str]:
28+
argv = [] + IWASM
29+
for k, v in env.items():
30+
argv += ["--env", f"{k}={v}"]
31+
for host, guest in dirs:
32+
argv += ["--map-dir", f"{host}::{guest}"] # noqa: E231
33+
argv += [test_path]
34+
argv += args
35+
return argv

adapters/wasmtime.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
1-
import argparse
21
import subprocess
3-
import sys
42
import os
53
import shlex
4+
from pathlib import Path
5+
from typing import Dict, List, Tuple
66

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

10-
parser = argparse.ArgumentParser()
11-
parser.add_argument("--version", action="store_true")
12-
parser.add_argument("--test-file", action="store")
13-
parser.add_argument("--arg", action="append", default=[])
14-
parser.add_argument("--env", action="append", default=[])
15-
parser.add_argument("--dir", action="append", default=[])
1610

17-
args = parser.parse_args()
11+
def get_name() -> str:
12+
return "wasmtime"
1813

19-
if args.version:
14+
15+
def get_version() -> str:
2016
# ensure no args when version is queried
21-
subprocess.run(WASMTIME[0:1] + ["--version"])
22-
sys.exit(0)
17+
result = subprocess.run(WASMTIME[0:1] + ["--version"],
18+
encoding="UTF-8", capture_output=True,
19+
check=True)
20+
output = result.stdout.splitlines()[0].split(" ")
21+
return output[1]
2322

24-
TEST_FILE = args.test_file
25-
PROG_ARGS = args.arg
26-
ENV_ARGS = [j for i in args.env for j in ["--env", i]]
27-
DIR_ARGS = [j for i in args.dir for j in ["--dir", i]]
2823

29-
r = subprocess.run(WASMTIME + ENV_ARGS + DIR_ARGS + [TEST_FILE] + PROG_ARGS)
30-
sys.exit(r.returncode)
24+
def compute_argv(test_path: str,
25+
args: List[str],
26+
env: Dict[str, str],
27+
dirs: List[Tuple[Path, str]]) -> List[str]:
28+
argv = [] + WASMTIME
29+
for k, v in env.items():
30+
argv += ["--env", f"{k}={v}"]
31+
for host, guest in dirs:
32+
argv += ["--dir", f"{host}::{guest}"] # noqa: E231
33+
argv += [test_path]
34+
argv += args
35+
return argv

adapters/wizard.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
1-
import argparse
21
import subprocess
3-
import sys
42
import os
53
import shlex
4+
from pathlib import Path
5+
from typing import Dict, List, Tuple
66

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

10-
parser = argparse.ArgumentParser()
11-
parser.add_argument("--version", action="store_true")
12-
parser.add_argument("--test-file", action="store")
13-
parser.add_argument("--arg", action="append", default=[])
14-
parser.add_argument("--env", action="append", default=[])
15-
parser.add_argument("--dir", action="append", default=[])
1610

17-
args = parser.parse_args()
11+
def get_name() -> str:
12+
return "wizard"
1813

19-
if args.version:
14+
15+
def get_version() -> str:
2016
# ensure no args when version is queried
21-
subprocess.run(WIZARD[0:1] + ["-version"])
22-
sys.exit(0)
17+
output = ""
18+
try:
19+
result = subprocess.run(WIZARD[0:1] + ["--version"],
20+
encoding="UTF-8", capture_output=True,
21+
check=False)
22+
output = result.stdout;
23+
except subprocess.CalledProcessError as e:
24+
# https://github.com/titzer/wizard-engine/issues/483
25+
if e.returncode != 3:
26+
raise e
27+
output = result.stdout
28+
output = output.splitlines()[0].split(" ")
29+
return output[1]
2330

24-
TEST_FILE = args.test_file
25-
PROG_ARGS = args.arg
26-
ENV_ARGS = None if len(args.env) == 0 else f'--env={",".join(args.env)}'
27-
DIR_ARGS = None if len(args.dir) == 0 else f'--dir={",".join(args.dir)}'
2831

29-
r = subprocess.run([arg for arg in WIZARD + [ENV_ARGS, DIR_ARGS, TEST_FILE] + PROG_ARGS if arg])
30-
sys.exit(r.returncode)
32+
def compute_argv(test_path: str,
33+
args: List[str],
34+
env: Dict[str, str],
35+
dirs: List[Tuple[Path, str]]) -> List[str]:
36+
argv = [] + WIZARD
37+
for k, v in env.items():
38+
argv += [f"--env={k}={v}"]
39+
for host, guest in dirs:
40+
# FIXME: https://github.com/titzer/wizard-engine/issues/482
41+
argv += [f"--dir={host}"]
42+
argv += [test_path]
43+
argv += args
44+
return argv

run-tests

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ from pathlib import Path
99
sys.path.insert(0, str(Path(__file__).parent / "test-runner"))
1010

1111
from wasi_test_runner.harness import run_tests
12-
from wasi_test_runner.runtime_adapter import RuntimeAdapter
12+
from wasi_test_runner import runtime_adapter
1313

1414
parser = argparse.ArgumentParser(
1515
description="WASI test runner"
@@ -53,12 +53,14 @@ def find_runtime_adapters(root, verbose=False):
5353
print(f"Detecting WASI runtime availability:")
5454
adapters = []
5555
for candidate in root.glob("*.py"):
56-
adapter = RuntimeAdapter(candidate)
5756
try:
57+
adapter = runtime_adapter.RuntimeAdapter(candidate)
5858
print(f" {candidate.name}: {adapter.get_version()}")
5959
adapters.append(adapter)
60-
except subprocess.CalledProcessError as e:
61-
print(f" {candidate.name}: unavailable; pass `--runtime {candidate}` to debug.")
60+
except runtime_adapter.LegacyRuntimeAdapterError:
61+
print(f" {candidate} is too old; update to new module format.")
62+
except runtime_adapter.UnavailableRuntimeAdapterError:
63+
print(f" {candidate.name} unavailable; pass `--runtime {candidate}` to debug.")
6264
print("")
6365
if len(adapters) == 0:
6466
print("Error: No WASI runtimes found")
@@ -68,17 +70,15 @@ def find_runtime_adapters(root, verbose=False):
6870
options = parser.parse_args()
6971
test_suite = find_test_dirs(Path(__file__).parent / "tests")
7072
if options.runtime_adapter:
71-
runtime_adapters = [RuntimeAdapter(options.runtime_adapter)]
72-
# Ensure it works.
7373
try:
74-
runtime_adapters[0].get_version()
75-
except subprocess.CalledProcessError as e:
76-
print(f"Error: failed to load {options.runtime_adapter}:")
77-
print(f" Failed command line: {' '.join(e.cmd)}")
78-
if e.stdout.strip() != "":
79-
print(f" stdout:\n{e.stdout}")
80-
if e.stderr.strip() != "":
81-
print(f" stderr:\n{e.stderr}")
74+
runtime_adapters = [
75+
runtime_adapter.RuntimeAdapter(options.runtime_adapter)
76+
]
77+
except runtime_adapter.LegacyRuntimeAdapterError as e:
78+
print(f"Error: {e.adapter_path} is too old; update to new module format.")
79+
sys.exit(1)
80+
except runtime_adapter.UnavailableRuntimeAdapterError as e:
81+
print(f"Error: failed to load {e.adapter_path}: {e.error}")
8282
sys.exit(1)
8383
else:
8484
runtime_adapters = find_runtime_adapters(Path(__file__).parent / "adapters")

test-runner/wasi_test_runner/runtime_adapter.py

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import importlib.util
12
import subprocess
23
import sys
34
from pathlib import Path
4-
from typing import Dict, NamedTuple, List
5+
from typing import Dict, NamedTuple, List, Tuple, Any
56

67
from .test_case import Output
78

@@ -14,31 +15,88 @@ def __str__(self) -> str:
1415
return f"{self.name} {self.version}"
1516

1617

18+
class RuntimeAdapterError(Exception):
19+
adapter_path: str
20+
21+
def __init__(self, adapter_path: str) -> None:
22+
self.adapter_path = adapter_path
23+
24+
25+
class LegacyRuntimeAdapterError(RuntimeAdapterError):
26+
adapter_path: str
27+
28+
29+
class UnavailableRuntimeAdapterError(RuntimeAdapterError):
30+
error: Exception
31+
32+
def __init__(self, adapter_path: str, error: Exception) -> None:
33+
RuntimeAdapterError.__init__(self, adapter_path)
34+
self.error = error
35+
36+
37+
def _assert_not_legacy_adapter(adapter_path: str) -> None:
38+
"""
39+
Raise an exception if the python file at ADAPTER_PATH isn't
40+
loadable as a normal Python module.
41+
"""
42+
argv = [sys.executable, adapter_path, "--version"]
43+
try:
44+
result = subprocess.run(argv, encoding="UTF-8", check=True,
45+
capture_output=True)
46+
except subprocess.CalledProcessError as e:
47+
if 'FileNotFoundError' in e.stderr:
48+
# The adapter is valid Python. Running it tries to spawn
49+
# the engine subprocess, but couldn't find the binary. This
50+
# indicates a legacy adapter.py.
51+
raise LegacyRuntimeAdapterError(adapter_path) from e
52+
# Some other error running adapter.py; could be a legacy
53+
# adapter, could just be a typo. Propagate the error.
54+
raise UnavailableRuntimeAdapterError(adapter_path, e) from e
55+
assert result.stderr == "", result.stderr
56+
if result.stdout:
57+
# Running the adapter as a subprocess succeeded and produced
58+
# --version output: the engine is available but the adapter is
59+
# legacy.
60+
raise LegacyRuntimeAdapterError(adapter_path)
61+
# Otherwise if loading the file produces no output, then we assume
62+
# it's a module and not legacy.
63+
64+
65+
def _load_adapter_as_module(adapter_path: str) -> Any:
66+
path = Path(adapter_path)
67+
spec = importlib.util.spec_from_file_location(path.name, path)
68+
assert spec is not None
69+
assert spec.loader is not None
70+
module = importlib.util.module_from_spec(spec)
71+
spec.loader.exec_module(module)
72+
return module
73+
74+
1775
class RuntimeAdapter:
1876
def __init__(self, adapter_path: str) -> None:
19-
self._adapter_path = adapter_path
20-
self._cached_version: RuntimeVersion | None = None
77+
_assert_not_legacy_adapter(adapter_path)
78+
self._adapter = _load_adapter_as_module(adapter_path)
79+
try:
80+
name = self._adapter.get_name()
81+
version = self._adapter.get_version()
82+
except subprocess.CalledProcessError as e:
83+
raise UnavailableRuntimeAdapterError(adapter_path, e) from e
84+
except FileNotFoundError as e:
85+
raise UnavailableRuntimeAdapterError(adapter_path, e) from e
86+
self._version = RuntimeVersion(name, version)
2187

2288
def get_version(self) -> RuntimeVersion:
23-
if self._cached_version is None:
24-
argv = [sys.executable, self._adapter_path, "--version"]
25-
result = subprocess.run(argv, encoding="UTF-8", capture_output=True,
26-
check=True)
27-
output = result.stdout.strip().split(" ")
28-
self._cached_version = RuntimeVersion(output[0], output[1])
29-
return self._cached_version
89+
return self._version
3090

3191
def compute_argv(self, test_path: str, args: List[str],
3292
env_variables: Dict[str, str],
33-
dirs: List[str]) -> List[str]:
34-
argv = [sys.executable, self._adapter_path]
35-
argv += ["--test-file", test_path]
36-
for d in dirs:
37-
argv += ["--dir", f"{Path(test_path).parent / d}::{d}"] # noqa: E231
38-
for k, v in env_variables.items():
39-
argv += ["--env", f"{k}={v}"]
40-
for a in args:
41-
argv += ["--arg", a]
93+
dirs: List[Tuple[Path, str]]) -> List[str]:
94+
argv = self._adapter.compute_argv(test_path=test_path,
95+
args=args,
96+
env=env_variables,
97+
dirs=dirs)
98+
assert isinstance(argv, list)
99+
assert all(isinstance(arg, str) for arg in argv)
42100
return argv
43101

44102
def run_test(self, argv: List[str]) -> Output:

0 commit comments

Comments
 (0)