Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
public class WasiTestGenMojo extends AbstractMojo {

private static final String JUNIT_TEST = "org.junit.jupiter.api.Test";
private static final String JUNIT_TEMP_DIR = "org.junit.jupiter.api.io.TempDir";
private static final String WASI_TEST_RUNNER = "run.endive.wasi.WasiTestRunner";

private final Log log = new SystemStreamLog();
Expand Down Expand Up @@ -85,6 +86,12 @@ public class WasiTestGenMojo extends AbstractMojo {
@Parameter(property = "wasi-test-gen.skip", defaultValue = "false")
private boolean skip;

/**
* Run the tests on a JUnit temporary directory of the host file system.
*/
@Parameter(defaultValue = "false")
private boolean hostFileSystem;

/**
* The current Maven project.
*/
Expand Down Expand Up @@ -158,9 +165,17 @@ public void execute() throws MojoExecutionException {
cu.addImport("java.util.Map");
cu.addImport("java.util.Optional");
cu.addImport(JUNIT_TEST);
if (hostFileSystem) {
cu.addImport("java.nio.file.Path");
cu.addImport(JUNIT_TEMP_DIR);
}

// generate test methods
var testClass = cu.addClass("Suite" + capitalize(testSuite) + "Test");
var testClass =
cu.addClass(
"Suite"
+ capitalize(testSuite)
+ (hostFileSystem ? "HostTest" : "Test"));
for (File file : files) {
String baseName =
file.getName().substring(0, file.getName().length() - ".wasm".length());
Expand All @@ -172,6 +187,9 @@ public void execute() throws MojoExecutionException {
testClass.addMethod(
"test" + escapedCamelCase(baseName), Modifier.Keyword.PUBLIC);
method.addAnnotation("Test");
if (hostFileSystem) {
method.addAndGetParameter("Path", "tempDir").addMarkerAnnotation("TempDir");
}

method.getBody()
.orElseThrow()
Expand Down Expand Up @@ -207,8 +225,11 @@ public void execute() throws MojoExecutionException {
AssignExpr.Operator.ASSIGN))
.addStatement(
new NameExpr(
"WasiTestRunner.execute("
+ "test, args, root, env, exitCode, stdout)"));
hostFileSystem
? "WasiTestRunner.executeOnHost(tempDir, "
+ "test, args, root, env, exitCode, stdout)"
: "WasiTestRunner.execute(test, args, root, env,"
+ " exitCode, stdout)"));
}

dest.add(
Expand Down
30 changes: 30 additions & 0 deletions wasi-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,34 @@
</plugin>
</plugins>
</build>

<profiles>
<profile>
<!-- older JDKs cannot set file times with nanosecond precision -->
<id>host-file-system</id>
<activation>
<jdk>[25,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>run.endive</groupId>
<artifactId>wasi-test-gen-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>host</id>
<goals>
<goal>wasi-test-gen</goal>
</goals>
<configuration>
<hostFileSystem>true</hostFileSystem>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
132 changes: 80 additions & 52 deletions wasi-tests/src/test/java/run/endive/wasi/WasiPreview1Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -443,61 +443,89 @@ public void filestatWithoutUnixAttributeView() throws IOException {
};
for (Configuration configuration : configurations) {
try (var fs = ZeroFs.newFileSystem(configuration)) {
Path root = fs.getPath("test");
createDirectory(root);
writeString(root.resolve("file"), "hello");
createDirectory(root.resolve("dir"));
try (var wasi = wasiWithDirectory("test", root)) {
var memory = new ByteBufferMemory(new MemoryLimits(1));
int buf = 0;

assertEquals(
WasiErrno.ESUCCESS.value(),
wasi.pathFilestatGet(memory, 3, 0, "file", buf));
assertEquals(WasiFileType.REGULAR_FILE.value(), memory.read(buf + 16));
assertEquals(5, memory.readLong(buf + 32));
long mtime = getLastModifiedTime(root.resolve("file")).to(NANOSECONDS);
assertEquals(mtime, memory.readLong(buf + 48));
long fileIno = memory.readLong(buf + 8);
assertNotEquals(0, fileIno);
assertFileStat(fs.getPath("test"));
}
}
}

assertEquals(
WasiErrno.ESUCCESS.value(),
wasi.pathFilestatGet(memory, 3, 0, "dir", buf));
long dirIno = memory.readLong(buf + 8);
assertNotEquals(0, dirIno);
assertNotEquals(fileIno, dirIno);
@Test
public void filestatOnHostFileSystem(@TempDir Path tempDir) throws IOException {
assertFileStat(tempDir.resolve("test"));
}

int fdPtr = 4096;
assertEquals(
WasiErrno.ESUCCESS.value(),
wasi.pathOpen(memory, 3, 0, "file", 0, 0, 0, 0, fdPtr));
int fd = memory.readInt(fdPtr);
assertEquals(WasiErrno.ESUCCESS.value(), wasi.fdFilestatGet(memory, fd, buf));
assertEquals(WasiFileType.REGULAR_FILE.value(), memory.read(buf + 16));
assertEquals(5, memory.readLong(buf + 32));
assertEquals(fileIno, memory.readLong(buf + 8));

int bufUsedPtr = 2048;
assertEquals(
WasiErrno.ESUCCESS.value(),
wasi.fdReaddir(memory, 3, buf, 1024, 0, bufUsedPtr));
Map<String, Integer> types = new HashMap<>();
Map<String, Long> inodes = new HashMap<>();
int used = memory.readInt(bufUsedPtr);
for (int pos = buf; pos < buf + used; ) {
int nameLen = memory.readInt(pos + 16);
String name = memory.readString(pos + 24, nameLen);
types.put(name, (int) memory.read(pos + 20));
inodes.put(name, memory.readLong(pos + 8));
pos += 24 + nameLen;
}
assertEquals(WasiFileType.REGULAR_FILE.value(), types.get("file"));
assertEquals(WasiFileType.DIRECTORY.value(), types.get("dir"));
assertEquals(fileIno, inodes.get("file"));
assertEquals(dirIno, inodes.get("dir"));
}
private static void assertFileStat(Path root) throws IOException {
createDirectory(root);
writeString(root.resolve("file"), "hello");
createDirectory(root.resolve("dir"));
try (var wasi = wasiWithDirectory("test", root)) {
var memory = new ByteBufferMemory(new MemoryLimits(1));
int buf = 0;

assertEquals(
WasiErrno.ESUCCESS.value(), wasi.pathFilestatGet(memory, 3, 0, "file", buf));
assertEquals(WasiFileType.REGULAR_FILE.value(), memory.read(buf + 16));
assertEquals(5, memory.readLong(buf + 32));
long mtime = getLastModifiedTime(root.resolve("file")).to(NANOSECONDS);
assertEquals(mtime, memory.readLong(buf + 48));
long fileIno = memory.readLong(buf + 8);
assertNotEquals(0, fileIno);

assertEquals(
WasiErrno.ESUCCESS.value(), wasi.pathFilestatGet(memory, 3, 0, "dir", buf));
long dirIno = memory.readLong(buf + 8);
assertNotEquals(0, dirIno);
assertNotEquals(fileIno, dirIno);

int fdPtr = 4096;
assertEquals(
WasiErrno.ESUCCESS.value(),
wasi.pathOpen(memory, 3, 0, "file", 0, 0, 0, 0, fdPtr));
int fd = memory.readInt(fdPtr);
assertEquals(WasiErrno.ESUCCESS.value(), wasi.fdFilestatGet(memory, fd, buf));
assertEquals(WasiFileType.REGULAR_FILE.value(), memory.read(buf + 16));
assertEquals(5, memory.readLong(buf + 32));
assertEquals(fileIno, memory.readLong(buf + 8));

int bufUsedPtr = 2048;
assertEquals(
WasiErrno.ESUCCESS.value(),
wasi.fdReaddir(memory, 3, buf, 1024, 0, bufUsedPtr));
Map<String, Integer> types = new HashMap<>();
Map<String, Long> inodes = new HashMap<>();
int used = memory.readInt(bufUsedPtr);
for (int pos = buf; pos < buf + used; ) {
int nameLen = memory.readInt(pos + 16);
String name = memory.readString(pos + 24, nameLen);
types.put(name, (int) memory.read(pos + 20));
inodes.put(name, memory.readLong(pos + 8));
pos += 24 + nameLen;
}
assertEquals(WasiFileType.REGULAR_FILE.value(), types.get("file"));
assertEquals(WasiFileType.DIRECTORY.value(), types.get("dir"));
assertEquals(fileIno, inodes.get("file"));
assertEquals(dirIno, inodes.get("dir"));
}
}

@Test
public void createFileInSymlinkedDirectory(@TempDir Path tempDir) throws IOException {
Path real = createDirectory(tempDir.resolve("real"));
Path link = createSymbolicLink(tempDir.resolve("link"), real);
try (var wasi = wasiWithDirectory("dir", link)) {
var memory = new ByteBufferMemory(new MemoryLimits(1));
assertEquals(
WasiErrno.ESUCCESS.value(),
wasi.pathOpen(
memory,
3,
WasiLookupFlags.SYMLINK_FOLLOW,
"new.txt",
WasiOpenFlags.CREAT,
WasiRights.FD_WRITE,
0,
0,
0));
assertTrue(exists(real.resolve("new.txt")));
}
}

Expand Down
111 changes: 72 additions & 39 deletions wasi-tests/src/test/java/run/endive/wasi/WasiTestRunner.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package run.endive.wasi;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static run.endive.wasi.Files.copyDirectory;

import io.roastedroot.zerofs.Configuration;
Expand All @@ -11,6 +12,7 @@
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -21,6 +23,7 @@

public final class WasiTestRunner {
private static final SystemLogger LOGGER = new SystemLogger();
private static final boolean WINDOWS = System.getProperty("os.name").startsWith("Windows");

private WasiTestRunner() {}

Expand All @@ -31,60 +34,90 @@ public static void execute(
Map<String, String> env,
int exitCode,
Optional<String> stdout) {

try (FileSystem fs =
ZeroFs.newFileSystem(
Configuration.unix().toBuilder().setAttributeViews("unix").build())) {
run(test, args, root, fs.getPath("/"), env, exitCode, stdout);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

var stdoutStream = new MockPrintStream();
var stderrStream = new MockPrintStream();
public static void executeOnHost(
Path tempDir,
File test,
List<String> args,
Optional<String> root,
Map<String, String> env,
int exitCode,
Optional<String> stdout) {
Map<String, String> hostEnv = env;
if (WINDOWS && root.isPresent()) {
// Java does not expose Windows file IDs, so hard links get different inode numbers
assumeFalse(test.getName().equals("path_link.wasm"), "hard link inode numbers");
hostEnv = new HashMap<>(env);
hostEnv.put("NO_RENAME_DIR_TO_EMPTY_DIR", "true");
}
run(test, args, root, tempDir.resolve("root"), hostEnv, exitCode, stdout);
}

private static void run(
File test,
List<String> args,
Optional<String> root,
Path target,
Map<String, String> env,
int exitCode,
Optional<String> stdout) {
var stdoutStream = new MockPrintStream();
var stderrStream = new MockPrintStream();

List<String> allArgs = new ArrayList<>();
allArgs.add("test");
allArgs.addAll(args);
List<String> allArgs = new ArrayList<>();
allArgs.add("test");
allArgs.addAll(args);

WasiOptions.Builder options =
WasiOptions.builder()
.withStdout(stdoutStream)
.withStderr(stderrStream)
.withArguments(allArgs);
WasiOptions.Builder options =
WasiOptions.builder()
.withStdout(stdoutStream)
.withStderr(stderrStream)
.withArguments(allArgs);

env.forEach(options::withEnvironment);
// TODO: dangling filesystem is not supported
if (!test.getName().contains("environ")) {
options.withEnvironment("NO_DANGLING_FILESYSTEM", "true");
}
env.forEach(options::withEnvironment);
// TODO: dangling filesystem is not supported
if (!test.getName().contains("environ")) {
options.withEnvironment("NO_DANGLING_FILESYSTEM", "true");
}

if (root.isPresent()) {
Path source = test.getParentFile().toPath().resolve(root.get());
Path target = fs.getPath("/");
if (root.isPresent()) {
Path source = test.getParentFile().toPath().resolve(root.get());
try {
copyDirectory(source, target);
options.withDirectory("/", target);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
options.withDirectory("/", target);
}

int actualExitCode;
try {
actualExitCode = execute(test, options.build());
} catch (WasiExitException e) {
actualExitCode = e.exitCode();
} catch (RuntimeException e) {
String message = "Failed to execute test: " + test;
if (!stdoutStream.output().isEmpty() || !stderrStream.output().isEmpty()) {
message += "\n<<<<<\n";
message += (stdoutStream.output() + stderrStream.output()).strip();
message += "\n>>>>>";
}
throw new RuntimeException(message, e);
int actualExitCode;
try {
actualExitCode = runModule(test, options.build());
} catch (WasiExitException e) {
actualExitCode = e.exitCode();
} catch (RuntimeException e) {
String message = "Failed to execute test: " + test;
if (!stdoutStream.output().isEmpty() || !stderrStream.output().isEmpty()) {
message += "\n<<<<<\n";
message += (stdoutStream.output() + stderrStream.output()).strip();
message += "\n>>>>>";
}

assertEquals(exitCode, actualExitCode, "exit code");
stdout.ifPresent(expected -> assertEquals(expected, stdoutStream.output(), "stdout"));
} catch (IOException e) {
throw new UncheckedIOException(e);
throw new RuntimeException(message, e);
}

assertEquals(exitCode, actualExitCode, "exit code");
stdout.ifPresent(expected -> assertEquals(expected, stdoutStream.output(), "stdout"));
}

private static int execute(File test, WasiOptions wasiOptions) {
private static int runModule(File test, WasiOptions wasiOptions) {
try (var wasi =
WasiPreview1.builder().withLogger(LOGGER).withOptions(wasiOptions).build()) {
Instance.builder(Parser.parse(test))
Expand Down
Loading
Loading