diff --git a/pom.xml b/pom.xml
index 4b775a844..f9de08b34 100644
--- a/pom.xml
+++ b/pom.xml
@@ -109,7 +109,7 @@
0.24.0
2.7.1
3.0.0
- 0.1.0
+ 0.2.0
diff --git a/wasi-test-gen-plugin/src/main/java/run/endive/maven/WasiTestGenMojo.java b/wasi-test-gen-plugin/src/main/java/run/endive/maven/WasiTestGenMojo.java
index ac06bc024..5a41fbabd 100644
--- a/wasi-test-gen-plugin/src/main/java/run/endive/maven/WasiTestGenMojo.java
+++ b/wasi-test-gen-plugin/src/main/java/run/endive/maven/WasiTestGenMojo.java
@@ -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();
@@ -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.
*/
@@ -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());
@@ -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()
@@ -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(
diff --git a/wasi-tests/pom.xml b/wasi-tests/pom.xml
index 6f7a75be2..f4477ea82 100644
--- a/wasi-tests/pom.xml
+++ b/wasi-tests/pom.xml
@@ -119,4 +119,34 @@
+
+
+
+
+ host-file-system
+
+ [25,)
+
+
+
+
+ run.endive
+ wasi-test-gen-plugin
+ ${project.version}
+
+
+ host
+
+ wasi-test-gen
+
+
+ true
+
+
+
+
+
+
+
+
diff --git a/wasi-tests/src/test/java/run/endive/wasi/WasiPreview1Test.java b/wasi-tests/src/test/java/run/endive/wasi/WasiPreview1Test.java
index f180d48e4..ecb60386d 100644
--- a/wasi-tests/src/test/java/run/endive/wasi/WasiPreview1Test.java
+++ b/wasi-tests/src/test/java/run/endive/wasi/WasiPreview1Test.java
@@ -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 types = new HashMap<>();
- Map 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 types = new HashMap<>();
+ Map 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")));
}
}
diff --git a/wasi-tests/src/test/java/run/endive/wasi/WasiTestRunner.java b/wasi-tests/src/test/java/run/endive/wasi/WasiTestRunner.java
index bc08910a6..84813d888 100644
--- a/wasi-tests/src/test/java/run/endive/wasi/WasiTestRunner.java
+++ b/wasi-tests/src/test/java/run/endive/wasi/WasiTestRunner.java
@@ -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;
@@ -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;
@@ -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() {}
@@ -31,60 +34,90 @@ public static void execute(
Map env,
int exitCode,
Optional 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 args,
+ Optional root,
+ Map env,
+ int exitCode,
+ Optional stdout) {
+ Map 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 args,
+ Optional root,
+ Path target,
+ Map env,
+ int exitCode,
+ Optional stdout) {
+ var stdoutStream = new MockPrintStream();
+ var stderrStream = new MockPrintStream();
- List allArgs = new ArrayList<>();
- allArgs.add("test");
- allArgs.addAll(args);
+ List 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))
diff --git a/wasi/src/main/java/run/endive/wasi/WasiPreview1.java b/wasi/src/main/java/run/endive/wasi/WasiPreview1.java
index 44da8bf9f..2dc6aa9bc 100644
--- a/wasi/src/main/java/run/endive/wasi/WasiPreview1.java
+++ b/wasi/src/main/java/run/endive/wasi/WasiPreview1.java
@@ -1136,9 +1136,7 @@ private ResolvedSymlink resolveSymlinks(Path path, Path directory) {
try {
Path dirReal = directory.toRealPath();
- Path pathReal =
- Files.exists(path) ? path.toRealPath() : path.toAbsolutePath().normalize();
- if (!pathReal.startsWith(dirReal)) {
+ if (!realPath(path).startsWith(dirReal)) {
return new ResolvedSymlink(wasiResult(WasiErrno.EACCES));
}
} catch (IOException e) {
@@ -1148,6 +1146,21 @@ private ResolvedSymlink resolveSymlinks(Path path, Path directory) {
return new ResolvedSymlink(path);
}
+ // The real path of the nearest existing ancestor, followed by the missing names.
+ private static Path realPath(Path path) throws IOException {
+ Path existing = path.toAbsolutePath().normalize();
+ Path missing = existing.getFileSystem().getPath("");
+ while (!Files.exists(existing)) {
+ Path parent = existing.getParent();
+ if (parent == null) {
+ return path.toAbsolutePath().normalize();
+ }
+ missing = existing.getFileName().resolve(missing);
+ existing = parent;
+ }
+ return existing.toRealPath().resolve(missing);
+ }
+
@WasmExport
public int pathLink(
int oldFd,
@@ -1516,11 +1529,23 @@ public int pathRename(
} catch (DirectoryNotEmptyException e) {
return wasiResult(WasiErrno.ENOTEMPTY);
} catch (IOException e) {
- return wasiResult(WasiErrno.EIO);
+ // the default provider reports a non-empty target as a generic exception
+ return wasiResult(isNonEmptyDirectory(newPath) ? WasiErrno.ENOTEMPTY : WasiErrno.EIO);
}
return wasiResult(WasiErrno.ESUCCESS);
}
+ private static boolean isNonEmptyDirectory(Path path) {
+ if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
+ return false;
+ }
+ try (Stream entries = Files.list(path)) {
+ return entries.findAny().isPresent();
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
@WasmExport
public int pathSymlink(@Buffer String oldRawPath, int dirFd, @Buffer String newRawPath) {
logger.tracef("path_symlink: [\"%s\", %s, \"%s\"]", oldRawPath, dirFd, newRawPath);