Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions scripts/harnesses/statictestreport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Static build test harness.

Runs the unit tests under tests/unit-tests/static_tests/ compiled with the
--static flag so that static WASM binaries (no dynamic linking) are tested.
This is a thin wrapper around wasmtestreport.py that pre-sets the static
compilation flags.
"""

from __future__ import annotations

import json
import subprocess
import tempfile
from pathlib import Path
from typing import Any, Callable

SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[1]
WASMTESTREPORT = SCRIPT_DIR / "wasmtestreport.py"

# Flags forwarded to wasmtestreport for static builds:
# --run static_tests : only run tests under static_tests/
# --static : pass --static before source file in lind_compile
# --allow-pre-compiled : use .cwasm AOT binaries (consistent with dynamic harness)
# --compile-flags -pthread -lpthread : link pthread for thread/TLS tests
_STATIC_HARNESS_ARGS = [
"--run", "static_tests",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this harness now owns static_tests, should we also exclude static_tests from the default wasmtestreport run? As written, I believe the normal wasm harness still discovers all tests/unit-tests/**/*.c, so these tests look like they’ll run once as dynamic-build tests and again here as static-build tests. Would it make sense to add a small guard near the existing if module_name == "wasmtestreport": block, perhaps appending a flag?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just resolved this, could you take a look again

"--static",
"--allow-pre-compiled",
"--compile-flags", "-pthread", "-lpthread",
]


def run_harness(
forward_args: list[str] | None = None,
execute_with_echo: Callable[[list[str], Path, str], tuple[int, str]] | None = None,
) -> dict[str, Any]:
"""Run static tests via wasmtestreport.py and return a harness result dict."""
with tempfile.TemporaryDirectory(prefix="harness_statictestreport_") as tmpdir:
tmp_path = Path(tmpdir)
json_out = tmp_path / "static.json"
html_out = tmp_path / "static.html"

args = [
"python3", str(WASMTESTREPORT),
*_STATIC_HARNESS_ARGS,
"--output", str(json_out),
"--report", str(html_out),
]
if forward_args:
args.extend(forward_args)

if execute_with_echo is not None:
return_code, combined_output = execute_with_echo(args, REPO_ROOT, "statictestreport")
if return_code != 0:
raise RuntimeError(
"statictestreport (wasmtestreport --static) failed "
f"with exit code {return_code}.\nCombined output:\n{combined_output}"
)
else:
proc = subprocess.run(
args,
capture_output=True,
text=True,
cwd=REPO_ROOT,
)
if proc.returncode != 0:
raise RuntimeError(
"statictestreport (wasmtestreport --static) failed "
f"with exit code {proc.returncode}.\n"
f"STDOUT:\n{proc.stdout}\nSTDERR:\n{proc.stderr}"
)

report_data = json.loads(json_out.read_text(encoding="utf-8"))
html_data = html_out.read_text(encoding="utf-8")

return {
"name": "static",
"json_filename": "static.json",
"report": report_data,
"html": html_data,
}
15 changes: 10 additions & 5 deletions scripts/harnesses/wasmtestreport.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this file to dynamic related?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test runner invokes lind_compile with no additional argument, so this is essentially doing the test with default configuration in lind_compile. If default configuration in lind_compile is changed, then what this test runner is testing is also changed. Therefore, I'd like to keep the name

Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
EXPECTED_DIRECTORY = Path("./expected")
SKIP_TESTS_FILE = "skip_test_cases.txt"
GLOBAL_COMPILE_FLAGS = []
LIND_PRE_FLAGS = []
DIR_FLAGS = []
MATH_TEST_DIR = os.environ.get("MATH_TEST_DIR")

Expand Down Expand Up @@ -461,7 +462,7 @@ def get_expected_output(source_file):
def compile_c_to_wasm(source_file, allow_precompiled=False):
source_file = Path(source_file)
testcase = str(source_file.with_suffix(''))
compile_cmd = [os.path.join(LIND_TOOL_PATH, "lind_compile"), source_file, *resolve_compile_flags(source_file, "lind")]
compile_cmd = [os.path.join(LIND_TOOL_PATH, "lind_compile"), *LIND_PRE_FLAGS, source_file, *resolve_compile_flags(source_file, "lind")]

logger.debug(f"Running command: {' '.join(map(str, compile_cmd))}")
if os.path.isfile(os.path.join(LIND_TOOL_PATH, "lind_compile")):
Expand Down Expand Up @@ -1183,6 +1184,7 @@ def parse_arguments(argv=None):
parser.add_argument("--artifacts-dir", type=Path, help="Directory to store build artifacts (default: temp dir)")
parser.add_argument("--keep-artifacts", action="store_true", help="Keep artifacts directory after run for troubleshooting")
parser.add_argument("--compile-flags", nargs="*", default=compile_flags, help="Extra flags passed to both lind_compile and the native compiler; values may start with '-' (e.g. --compile-flags -pthread -lpthread -O2 -g)")
parser.add_argument("--static", action="store_true", dest="static_build", help="Pass --static before the source file in lind_compile invocations (static WASM build, no dynamic linking)")
parser.add_argument("--dir-flags", type=Path, help="Path to JSON file mapping directories to lind/native flags")

args = parser.parse_args(argv)
Expand Down Expand Up @@ -1425,7 +1427,7 @@ def build_fail_message(case: str, native_output: str, wasm_output: str, native_r
)

def main():
global GLOBAL_COMPILE_FLAGS, DIR_FLAGS
global GLOBAL_COMPILE_FLAGS, LIND_PRE_FLAGS, DIR_FLAGS
os.chdir(LIND_WASM_BASE)
args = parse_arguments()
skip_folders = args.skip
Expand All @@ -1440,6 +1442,7 @@ def main():
artifacts_dir_arg = args.artifacts_dir
keep_artifacts = args.keep_artifacts
GLOBAL_COMPILE_FLAGS = [*args.compile_flags]
LIND_PRE_FLAGS = ["--static"] if args.static_build else []

if args.dir_flags:
try:
Expand Down Expand Up @@ -1498,8 +1501,10 @@ def main():
try:
shutil.rmtree(TESTFILES_DST)
logger.info(f"Testfiles at {TESTFILES_DST} deleted")
except FileNotFoundError as e:
logger.error(f"Testfiles not present at {TESTFILES_DST}")
except FileNotFoundError:
logger.info(f"Testfiles not present at {TESTFILES_DST}")
except PermissionError as e:
logger.warning(f"Could not remove testfiles at {TESTFILES_DST}: {e}")

if clean_testfiles:
return
Expand Down Expand Up @@ -1538,7 +1543,7 @@ def main():
# ALWAYS clean up, regardless of success/failure/interruption
try:
shutil.rmtree(TESTFILES_DST)
except FileNotFoundError:
except (FileNotFoundError, PermissionError):
pass

# Remove artifacts directory if it was temp and not requested to keep
Expand Down
19 changes: 14 additions & 5 deletions scripts/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,20 @@ def write_outputs(result: dict[str, Any], reports_dir: Path) -> dict[str, Any]:
json_path = reports_dir / json_filename
json_path.write_text(json.dumps(result["report"], indent=2), encoding="utf-8")

html_path: Path | None = None
html_payload = result.get("html")
if html_payload is not None:
html_filename = str(result.get("html_filename", f"{harness_name}.html"))
html_path = reports_dir / html_filename
html_path: Path | None = None
# Only write a separate HTML file when html_filename is explicitly provided by
# the harness. Harnesses that omit html_filename still contribute their HTML
# content to the combined report.html via the in-memory html_payload field.
if html_payload is not None and "html_filename" in result:
html_path = reports_dir / str(result["html_filename"])
html_path.write_text(str(html_payload), encoding="utf-8")

return {
"name": harness_name,
"json_path": json_path,
"html_path": html_path,
"html": html_payload,
"report": result["report"],
}

Expand All @@ -168,9 +171,12 @@ def generate_combined_report(harness_outputs: list[dict[str, Any]], reports_dir:
for output in harness_outputs:
name = output["name"]
json_path: Path = output["json_path"]
html_payload: str | None = output.get("html")
html_path: Path | None = output["html_path"]

if html_path and html_path.exists():
if html_payload is not None:
body = extract_html_body(html_payload)
elif html_path and html_path.exists():
body = extract_html_body(html_path.read_text(encoding="utf-8"))
else:
body = (
Expand Down Expand Up @@ -233,6 +239,9 @@ def main() -> None:

if module_name == "wasmtestreport":
harness_args.append("--allow-pre-compiled")
# static_tests are owned by the statictestreport harness; exclude them here
# so they don't also run as ordinary dynamic-build tests.
harness_args.extend(["--skip", "static_tests"])

result = run_harness(module_name, harness_args)

Expand Down
23 changes: 23 additions & 0 deletions tests/unit-tests/static_tests/deterministic/fork_simple.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <assert.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
pid_t cpid;
cpid = fork();
assert(cpid >= 0);

if (cpid == 0) {
exit(0);
} else {
int status;
pid_t waited_pid = waitpid(cpid, &status, 0);
assert(waited_pid >= 0);
assert(WIFEXITED(status));
assert(WEXITSTATUS(status) == 0);
}

return 0;
}
14 changes: 14 additions & 0 deletions tests/unit-tests/static_tests/deterministic/thread.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include <pthread.h>
#include <stdio.h>

void* thread_function(void* arg) {
printf("Hello from thread\n");
return NULL;
}

int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
pthread_join(thread, NULL);
return 0;
}
32 changes: 32 additions & 0 deletions tests/unit-tests/static_tests/deterministic/tls_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

__thread int tls_var = 233;

#define NUM_THREADS 5

void* thread_function(void* arg) {
int thread_id = *((int*)arg);
assert(tls_var == 233);
tls_var = thread_id * 10;
assert(tls_var == thread_id * 10);
return NULL;
}

int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];

for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i + 1;
pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
}

for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}

return 0;
}