Skip to content

Commit 3010ad2

Browse files
DyanBYaxuan-wstupendoussuperpowersrennergade
authored
Test Support for File/executable paths (#467)
* Add more unit tests * GHA: Retrigger tests * Add exit(1) * Fix broken tests. - Modified name for file open'd in open.c - Move fork.c to non-deterministic, remove it from skip list. - Print argv[0] in hello-arg - Run correct hello binary in forkexecv-arg.c * Merge main to resolve conflicts * Add execv-arg to skip list * Add open to skip list * Adding test support for exec, open and hello-arg tests * Ensure LIND_FS_ROOT exists in CI environment * Fix CI path resolution with symlink in Docker * Code enhacments, Reverting symlink in docker e2e and removed a bunch of tests in skip list * adding symlink back again due to CI failure * Removing all skip test cases * Finalized the skip list * Modified regex for analyzing wide range of dependencies * Adding readlinkat and creat_access to the skip list --------- Co-authored-by: Alice W <[email protected]> Co-authored-by: Sanchit Sahay <[email protected]> Co-authored-by: Nicholas Renner <[email protected]>
1 parent 42468ea commit 3010ad2

File tree

8 files changed

+242
-98
lines changed

8 files changed

+242
-98
lines changed

Docker/Dockerfile.e2e

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ RUN install -D -m 0755 /scripts/lind_compile /usr/local/bin/lind_compile \
112112
# Run all tests, print results, and exit with 1, if any test fails; 0 otherwise
113113
FROM base AS test
114114
COPY --parents scripts tests tools skip_test_cases.txt Makefile .
115+
# Create symlink so hardcoded paths in wasmtime match Docker environment
116+
RUN mkdir -p /home/lind && ln -sf / /home/lind/lind-wasm
115117
# NOTE: Build artifacts from prior stages are only mounted, to save COPY time
116118
# and cache layers. This means they are not preserved in the resulting image.
117119
RUN --mount=from=build-wasmtime,source=src/wasmtime/target,destination=src/wasmtime/target \

scripts/wasmtestreport.py

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os
1717
import subprocess
1818
from pathlib import Path
19+
from typing import Dict
1920
import argparse
2021
import shutil
2122
import logging
@@ -42,6 +43,7 @@
4243
REPO_ROOT = SCRIPT_DIR.parent
4344
LIND_WASM_BASE = Path(os.environ.get("LIND_WASM_BASE", REPO_ROOT)).resolve()
4445
LIND_FS_ROOT = Path(os.environ.get("LIND_FS_ROOT", LIND_WASM_BASE / "src/RawPOSIX/tmp")).resolve()
46+
CC = os.environ.get("CC", "gcc") # C compiler, defaults to gcc
4547

4648
LIND_TOOL_PATH = LIND_WASM_BASE / "scripts"
4749
TEST_FILE_BASE = LIND_WASM_BASE / "tests" / "unit-tests"
@@ -63,7 +65,7 @@
6365
"Lind_wasm_Segmentation_Fault": "Lind Wasm Segmentation Failure",
6466
"Lind_wasm_Timeout": "Timeout During Lind Wasm run",
6567
"Unknown_Failure": "Unknown Failure",
66-
"Output_mismatch": "GCC and Wasm Output mismatch"
68+
"Output_mismatch": "C Compiler and Wasm Output mismatch"
6769
}
6870

6971
# ----------------------------------------------------------------------
@@ -223,16 +225,42 @@ def compile_and_run_native(source_file, timeout_sec=DEFAULT_TIMEOUT):
223225
source_file = Path(source_file)
224226
native_output = source_file.parent / f"{source_file.stem}.o"
225227

228+
# Prepare any executable dependencies required by this test (like execv targets)
229+
executable_backups: Dict[Path, Path] = {}
230+
created_native_execs = set()
231+
native_dependencies = analyze_executable_dependencies([source_file])
232+
for exec_path, dependency_source in native_dependencies.items():
233+
dest_path = (LIND_FS_ROOT / exec_path).resolve()
234+
dest_path.parent.mkdir(parents=True, exist_ok=True)
235+
236+
if dest_path.exists():
237+
fd, backup_path = tempfile.mkstemp(prefix=f"{dest_path.name}_orig_", dir=str(dest_path.parent))
238+
os.close(fd)
239+
shutil.copy2(str(dest_path), backup_path)
240+
executable_backups[dest_path] = Path(backup_path)
241+
242+
dep_compile_cmd = [CC, str(dependency_source), "-o", str(dest_path)]
243+
try:
244+
dep_proc = run_subprocess(dep_compile_cmd, label="native dep compile", shell=False)
245+
except Exception as e:
246+
return False, f"Exception compiling dependency {dependency_source}: {e}", "compile_error", "Failure_native_compiling"
247+
248+
if dep_proc.returncode != 0:
249+
error_output = dep_proc.stdout + dep_proc.stderr
250+
return False, f"Failed to compile dependency {dependency_source}: {error_output}", "compile_error", "Failure_native_compiling"
251+
252+
created_native_execs.add(dest_path)
253+
226254
# Ensure paths are absolute to prevent cwd confusion
227255
if not source_file.is_absolute():
228256
raise ValueError(f"Source file must be absolute path, got: {source_file}")
229257
if not native_output.is_absolute():
230258
raise ValueError(f"Native output path must be absolute, got: {native_output}")
231259

232260
# Compile
233-
compile_cmd = ["gcc", str(source_file), "-o", str(native_output)]
261+
compile_cmd = [CC, str(source_file), "-o", str(native_output)]
234262
try:
235-
proc = run_subprocess(compile_cmd, label="gcc compile", cwd=LIND_FS_ROOT, shell=False)
263+
proc = run_subprocess(compile_cmd, label=f"{CC} compile", cwd=LIND_FS_ROOT, shell=False)
236264
if proc.returncode != 0:
237265
return False, proc.stdout + proc.stderr, "compile_error", "Failure_native_compiling"
238266
except Exception as e:
@@ -251,8 +279,21 @@ def compile_and_run_native(source_file, timeout_sec=DEFAULT_TIMEOUT):
251279
return False, f"Exception: {e}", "unknown_error", "Failure_native_running"
252280
finally:
253281
# Clean up native binary
254-
if native_output.exists():
255-
native_output.unlink()
282+
native_output.unlink(missing_ok=True)
283+
284+
# Restore any executable dependencies that were swapped out
285+
for dest_path in created_native_execs:
286+
try:
287+
dest_path.unlink(missing_ok=True)
288+
except (FileNotFoundError, PermissionError) as cleanup_err:
289+
logger.debug(f"Failed to remove native dependency {dest_path}: {cleanup_err}")
290+
291+
for dest_path, backup_path in executable_backups.items():
292+
try:
293+
if backup_path.exists():
294+
shutil.move(str(backup_path), str(dest_path))
295+
except (FileNotFoundError, PermissionError) as restore_err:
296+
logger.warning(f"Failed to restore dependency {dest_path} from backup: {restore_err}")
256297

257298
# ----------------------------------------------------------------------
258299
# Function: get_expected_output
@@ -474,8 +515,8 @@ def analyze_testfile_dependencies(tests_to_run):
474515
with open(test_file, 'r') as f:
475516
content = f.read()
476517

477-
# Look for testfiles references in open() calls
478-
testfile_pattern = r'open\s*\(\s*"testfiles/([^"]+)"'
518+
# Look for any "testfiles/..." string literals in the code
519+
testfile_pattern = r'"testfiles/([^"]+)"'
479520
matches = re.findall(testfile_pattern, content, re.IGNORECASE)
480521
all_dependencies.update(matches)
481522

@@ -487,6 +528,102 @@ def analyze_testfile_dependencies(tests_to_run):
487528

488529
return all_dependencies
489530

531+
# ----------------------------------------------------------------------
532+
# Function: analyze_executable_dependencies
533+
#
534+
# Purpose:
535+
# Analyzes test files to determine which executables they need
536+
#
537+
# Variables:
538+
# - Input:
539+
# tests_to_run: List of test files to analyze
540+
# - Output:
541+
# Dictionary mapping executable paths to their source file paths
542+
# e.g., {'automated_tests/hello-arg': Path('hello-arg.c')}
543+
# ----------------------------------------------------------------------
544+
def analyze_executable_dependencies(tests_to_run):
545+
import re
546+
547+
executable_deps: Dict[str, Path] = {}
548+
549+
for test_file in tests_to_run:
550+
try:
551+
with open(test_file, 'r') as f:
552+
content = f.read()
553+
554+
# Look for execv/execve/execl calls with string literal paths
555+
# NOTE: This intentionally only matches simple string literals like execv("path", ...)
556+
# It does NOT match variable references like execv(argv[0], ...) or macro expansions
557+
# This is a deliberate limitation to keep the dependency analysis simple and predictable
558+
# Pattern matches: execv("path/to/executable", ...), execl("/bin/ls", ...), etc.
559+
exec_pattern = r'exec[vle]+\s*\(\s*"([^"]+)"'
560+
matches = re.findall(exec_pattern, content, re.IGNORECASE)
561+
562+
for exec_path in matches:
563+
exec_name = Path(exec_path).name
564+
565+
candidate_sources = [
566+
test_file.parent / f"{exec_name}.c",
567+
test_file.resolve().parent / f"{exec_name}.c"
568+
]
569+
570+
selected_source = None
571+
for candidate in candidate_sources:
572+
if candidate.exists():
573+
selected_source = candidate
574+
break
575+
576+
if selected_source:
577+
executable_deps[exec_path] = selected_source
578+
logger.debug(f"Found executable dependency in {test_file.name}: {exec_path} -> {selected_source.name}")
579+
else:
580+
logger.debug(
581+
f"Executable {exec_path} referenced but no matching source found near {test_file}"
582+
)
583+
584+
except Exception as e:
585+
logger.debug(f"Could not analyze executable dependencies for {test_file}: {e}")
586+
587+
return executable_deps
588+
589+
# ----------------------------------------------------------------------
590+
# Function: create_required_executables
591+
#
592+
# Purpose:
593+
# Compiles required executables and places them in LIND_FS_ROOT
594+
#
595+
# Variables:
596+
# - Input:
597+
# executable_deps: Dictionary mapping executable paths to source files
598+
# - Output:
599+
# None (creates executables in LIND_FS_ROOT)
600+
# ----------------------------------------------------------------------
601+
def create_required_executables(executable_deps):
602+
if not executable_deps:
603+
return
604+
605+
logger.info(f"Creating {len(executable_deps)} required executable(s)")
606+
607+
for exec_path, source_file in executable_deps.items():
608+
try:
609+
# Compile the source file to WASM
610+
wasm_file, compile_err = compile_c_to_wasm(source_file)
611+
612+
if wasm_file is None:
613+
logger.error(f"Failed to compile {source_file}: {compile_err}")
614+
continue
615+
616+
# Create destination directory in LIND_FS_ROOT
617+
dest_path = LIND_FS_ROOT / exec_path
618+
dest_path.parent.mkdir(parents=True, exist_ok=True)
619+
620+
# Copy the compiled WASM to the destination (preserves source for potential reuse)
621+
shutil.copy2(str(wasm_file), str(dest_path))
622+
logger.info(f"Created executable: {dest_path}")
623+
624+
except Exception as e:
625+
logger.error(f"Failed to create executable {exec_path}: {e}")
626+
490627
# ----------------------------------------------------------------------
491628
# Function: pre_test
492629
#
@@ -502,6 +639,9 @@ def analyze_testfile_dependencies(tests_to_run):
502639
# None
503640
# ----------------------------------------------------------------------
504641
def pre_test(tests_to_run=None):
642+
# Ensure LIND_FS_ROOT exists (For CI Environment)
643+
os.makedirs(LIND_FS_ROOT, exist_ok=True)
644+
505645
# If tests_to_run is provided, use selective copying
506646
if tests_to_run:
507647
all_dependencies = analyze_testfile_dependencies(tests_to_run)
@@ -548,6 +688,11 @@ def pre_test(tests_to_run=None):
548688
except OSError:
549689
# Fallback to copying in case symlink creation fails
550690
shutil.copy2(readlinkfile_path, symlink_path)
691+
692+
# Create required executables
693+
if tests_to_run:
694+
executable_deps = analyze_executable_dependencies(tests_to_run)
695+
create_required_executables(executable_deps)
551696

552697
# ----------------------------------------------------------------------
553698
# Function: generate_html_report

skip_test_cases.txt

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,13 @@
11
file_tests/deterministic/dup2.c
22
file_tests/deterministic/dup3.c
3-
file_tests/deterministic/chmod.c
43
file_tests/deterministic/cloexec.c
5-
file_tests/deterministic/close.c
6-
file_tests/deterministic/creat_access.c
7-
file_tests/deterministic/fchmod.c
8-
file_tests/deterministic/fcntl.c
9-
file_tests/deterministic/fdatasync.c
104
file_tests/deterministic/flock.c
11-
file_tests/deterministic/fstat.c
12-
file_tests/deterministic/fsync.c
13-
file_tests/deterministic/mkdir_rmdir.c
145
file_tests/deterministic/popen.c
15-
file_tests/deterministic/pread_pwrite.c
16-
file_tests/deterministic/readbytes.c
17-
file_tests/deterministic/readlink.c
6+
file_tests/deterministic/creat_access.c
187
file_tests/deterministic/readlinkat.c
19-
file_tests/deterministic/rename.c
20-
file_tests/deterministic/stat.c
21-
file_tests/deterministic/sync_file_range.c
22-
file_tests/deterministic/truncate.c
23-
file_tests/deterministic/unlinkat.c
248
file_tests/non-deterministic/sc-writev.c
259
memory_tests/deterministic/mmap-negative1.c
2610
memory_tests/deterministic/mmap-negative2.c
27-
memory_tests/deterministic/mmap_complicated.c
28-
memory_tests/deterministic/mmap_file.c
2911
memory_tests/deterministic/mmaptest.c
3012
networking_tests/deterministic/uds-getsockname.c
3113
networking_tests/deterministic/uds-nb-select.c
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Before running this test:
3+
* 1. Create a file named "testfile.txt" in the $LIND_FS_ROOT directory.
4+
*/
5+
6+
#include <fcntl.h>
7+
#include <unistd.h>
8+
#include <stdio.h>
9+
10+
int main() {
11+
int fd = open("testfiles/filetestfile.txt", O_RDONLY);
12+
13+
if (fd == -1) {
14+
perror("open failed");
15+
return 1;
16+
}
17+
18+
printf("File opened successfully with fd = %d\n", fd);
19+
20+
close(fd);
21+
return 0;
22+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Before running this test:
3+
* 1. Make sure to compile the target program (hello) using your desired toolchain.
4+
* 2. Copy the compiled binary to $LIND_FS_ROOT.
5+
* 3. IMPORTANT: Rename the binary to "hello-arg" (no .wasm or other extensions).
6+
*
7+
* The executable must be accessible at: $LIND_FS_ROOT/hello-arg
8+
*/
9+
10+
#include <stdio.h>
11+
#include <unistd.h>
12+
#include <sys/wait.h>
13+
#include <stdlib.h>
14+
15+
int main() {
16+
pid_t pid = fork();
17+
18+
if (pid == 0) {
19+
// child process: call execv with argument
20+
char *args[] = {"hello-arg", "hello_from_parent", NULL};
21+
execv("automated_tests/hello-arg", args);
22+
perror("execv failed"); // only runs if execv fails
23+
exit(1);
24+
} else {
25+
// parent process
26+
wait(NULL); // wait for child to finish
27+
}
28+
29+
return 0;
30+
}

tests/unit-tests/process_tests/deterministic/forkexecv.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
/*
2+
* Before running this test:
3+
* 1. Make sure to compile the target program (hello) using your desired toolchain.
4+
* 2. Copy the compiled binary to $LIND_FS_ROOT.
5+
* 3. IMPORTANT: Rename the binary to "hello" (no .wasm or other extensions).
6+
*
7+
* The executable must be accessible at: $LIND_FS_ROOT/hello
8+
*/
9+
110
#include <sys/types.h>
211
#include <unistd.h>
312
#include <stdio.h>
4-
5-
#include <sys/types.h>
613
#include <sys/wait.h>
714

815
int main(void)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include <stdio.h>
2+
3+
int main(int argc, char *argv[]) {
4+
// Command line inputs arent currently supported by the test harness.
5+
/*
6+
if (argc < 2) {
7+
printf("Usage: %s <your_argument>\n", argv[0]);
8+
return 1;
9+
}
10+
*/
11+
argv[0] = "hello-arg";
12+
printf("Received argument: %s\n", argv[0]);
13+
return 0;
14+
}

0 commit comments

Comments
 (0)