diff --git a/api/src/main/java/net/neoforged/jst/api/FileEntries.java b/api/src/main/java/net/neoforged/jst/api/FileEntries.java new file mode 100644 index 0000000..88ec4b8 --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/FileEntries.java @@ -0,0 +1,32 @@ +package net.neoforged.jst.api; + +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public final class FileEntries { + private FileEntries() { + } + + /** + * Creates a file entry for a given NIO path. + * Since file entries need to know their path relative to the source root, the source root has to be + * given as an additional parameter. + */ + public static FileEntry ofPath(Path sourceRoot, Path path) { + if (path.equals(sourceRoot)) { + throw new IllegalStateException("path must not be the source root itself, since this results in an empty relative path"); + } + if (!path.startsWith(sourceRoot)) { + throw new IllegalStateException("path must be a child of sourceRoot"); + } + return new PathFileEntry(sourceRoot, path); + } + + /** + * Creates a file entry for an existing zip entry. Will source the content from the given zip file. + */ + public static FileEntry ofZipEntry(ZipFile zipFile, ZipEntry zipEntry) { + return new ZipFileEntry(zipFile, zipEntry); + } +} diff --git a/api/src/main/java/net/neoforged/jst/api/FileEntry.java b/api/src/main/java/net/neoforged/jst/api/FileEntry.java index bc7b891..cb619c9 100644 --- a/api/src/main/java/net/neoforged/jst/api/FileEntry.java +++ b/api/src/main/java/net/neoforged/jst/api/FileEntry.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.attribute.FileTime; import java.util.Locale; public interface FileEntry { @@ -18,7 +19,7 @@ public interface FileEntry { /** * @return Millis since epoch denoting when the file was last modified. 0 for directories. */ - long lastModified(); + FileTime lastModified(); /** * @return An input stream to read this content. diff --git a/api/src/main/java/net/neoforged/jst/api/FileSink.java b/api/src/main/java/net/neoforged/jst/api/FileSink.java index 3588f33..3f5cd1b 100644 --- a/api/src/main/java/net/neoforged/jst/api/FileSink.java +++ b/api/src/main/java/net/neoforged/jst/api/FileSink.java @@ -3,13 +3,18 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.attribute.FileTime; public interface FileSink extends AutoCloseable { @Override default void close() throws IOException { } + boolean canHaveMultipleEntries(); + boolean isOrdered(); - void put(FileEntry entry, byte[] content) throws IOException; + void putDirectory(String relativePath) throws IOException; + + void putFile(String relativePath, FileTime lastModified, byte[] content) throws IOException; } diff --git a/api/src/main/java/net/neoforged/jst/api/FileSource.java b/api/src/main/java/net/neoforged/jst/api/FileSource.java index a9fe38d..b84ef86 100644 --- a/api/src/main/java/net/neoforged/jst/api/FileSource.java +++ b/api/src/main/java/net/neoforged/jst/api/FileSource.java @@ -11,6 +11,8 @@ public interface FileSource extends AutoCloseable { Stream streamEntries() throws IOException; + boolean canHaveMultipleEntries(); + boolean isOrdered(); @Override diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/PathEntry.java b/api/src/main/java/net/neoforged/jst/api/PathFileEntry.java similarity index 70% rename from cli/src/main/java/net/neoforged/jst/cli/io/PathEntry.java rename to api/src/main/java/net/neoforged/jst/api/PathFileEntry.java index b273489..f57f306 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/PathEntry.java +++ b/api/src/main/java/net/neoforged/jst/api/PathFileEntry.java @@ -1,29 +1,26 @@ -package net.neoforged.jst.cli.io; - -import net.neoforged.jst.api.FileEntry; +package net.neoforged.jst.api; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; -final class PathEntry implements FileEntry { - private final Path relativeTo; +final class PathFileEntry implements FileEntry { private final Path path; private final String relativePath; private final boolean directory; - private final long lastModified; + private final FileTime lastModified; - public PathEntry(Path relativeTo, Path path) { + public PathFileEntry(Path relativeTo, Path path) { this.directory = Files.isDirectory(path); - this.relativeTo = relativeTo; this.path = path; var relativized = relativeTo.relativize(path).toString(); relativized = relativized.replace('\\', '/'); this.relativePath = relativized; try { - this.lastModified = Files.getLastModifiedTime(path).toMillis(); + this.lastModified = Files.getLastModifiedTime(path); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -40,7 +37,7 @@ public String relativePath() { } @Override - public long lastModified() { + public FileTime lastModified() { return lastModified; } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileEntry.java b/api/src/main/java/net/neoforged/jst/api/ZipFileEntry.java similarity index 82% rename from cli/src/main/java/net/neoforged/jst/cli/io/ZipFileEntry.java rename to api/src/main/java/net/neoforged/jst/api/ZipFileEntry.java index db771cc..44039c1 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileEntry.java +++ b/api/src/main/java/net/neoforged/jst/api/ZipFileEntry.java @@ -1,9 +1,10 @@ -package net.neoforged.jst.cli.io; +package net.neoforged.jst.api; import net.neoforged.jst.api.FileEntry; import java.io.IOException; import java.io.InputStream; +import java.nio.file.attribute.FileTime; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -27,8 +28,8 @@ public String relativePath() { } @Override - public long lastModified() { - return zipEntry.getLastModifiedTime().toMillis(); + public FileTime lastModified() { + return zipEntry.getLastModifiedTime(); } @Override diff --git a/cli/src/main/java/net/neoforged/jst/cli/Main.java b/cli/src/main/java/net/neoforged/jst/cli/Main.java index 16c51b6..d171580 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/Main.java +++ b/cli/src/main/java/net/neoforged/jst/cli/Main.java @@ -17,19 +17,22 @@ @CommandLine.Command(name = "jst", mixinStandardHelpOptions = true, usageHelpWidth = 100) public class Main implements Callable { @CommandLine.Parameters(index = "0", paramLabel = "INPUT", description = "Path to a single Java-file, a source-archive or a folder containing the source to transform.") - private Path inputPath; + Path inputPath; @CommandLine.Parameters(index = "1", paramLabel = "OUTPUT", description = "Path to where the resulting source should be placed.") - private Path outputPath; + Path outputPath; @CommandLine.Option(names = "--in-format", description = "Specify the format of INPUT explicitly. AUTO (the default) performs auto-detection. Other options are SINGLE_FILE for Java files, ARCHIVE for source jars or zips, and FOLDER for folders containing Java code.") - private PathType inputFormat = PathType.AUTO; + PathType inputFormat = PathType.AUTO; @CommandLine.Option(names = "--out-format", description = "Specify the format of OUTPUT explicitly. Allows the same options as --in-format.") - private PathType outputFormat = PathType.AUTO; + PathType outputFormat = PathType.AUTO; @CommandLine.Option(names = "--libraries-list", description = "Specifies a file that contains a path to an archive or directory to add to the classpath on each line.") - private Path librariesList; + Path librariesList; + + @CommandLine.Option(names = "--max-queue-depth", description = "When both input and output support ordering (archives), the transformer will try to maintain that order. To still process items in parallel, a queue is used. Larger queue depths lead to higher memory usage.") + int maxQueueDepth = 100; private final HashSet enabledTransformers = new HashSet<>(); @@ -44,6 +47,7 @@ public static int innerMain(String... args) { var main = new Main(); var commandLine = new CommandLine(main); + commandLine.setCaseInsensitiveEnumValuesAllowed(true); var spec = commandLine.getCommandSpec(); main.setupPluginCliOptions(plugins, spec); @@ -60,6 +64,8 @@ public Integer call() throws Exception { processor.addLibrariesList(librariesList); } + processor.setMaxQueueDepth(maxQueueDepth); + var orderedTransformers = new ArrayList<>(enabledTransformers); try (var sink = FileSinks.create(outputPath, outputFormat, source)) { @@ -106,73 +112,4 @@ public T set(T value) { spec.addArgGroup(builder.build()); } } - -// -// void poo() { -// String[] args = new String[0]; -// -// Path inputPath = null, outputPath = null, namesAndDocsPath = null, librariesPath = null; -// boolean enableJavadoc = true; -// int queueDepth = 50; -// -// for (int i = 0; i < args.length; i++) { -// var arg = args[i]; -// switch (arg) { -// case "--in": -// if (i + 1 >= args.length) { -// System.err.println("Missing argument for --in"); -// System.exit(1); -// } -// inputPath = Paths.get(args[++i]); -// break; -// case "--out": -// if (i + 1 >= args.length) { -// System.err.println("Missing argument for --out"); -// System.exit(1); -// } -// outputPath = Paths.get(args[++i]); -// break; -// case "--libraries": -// if (i + 1 >= args.length) { -// System.err.println("Missing argument for --libraries"); -// System.exit(1); -// } -// librariesPath = Paths.get(args[++i]); -// break; -// case "--names": -// if (i + 1 >= args.length) { -// System.err.println("Missing argument for --names"); -// System.exit(1); -// } -// namesAndDocsPath = Paths.get(args[++i]); -// break; -// case "--skip-javadoc": -// enableJavadoc = false; -// break; -// case "--queue-depth": -// if (i + 1 >= args.length) { -// System.err.println("Missing argument for --queue-depth"); -// System.exit(1); -// } -// queueDepth = Integer.parseUnsignedInt(args[++i]); -// break; -// case "--help": -// printUsage(System.out); -// System.exit(0); -// break; -// default: -// System.err.println("Unknown argument: " + arg); -// printUsage(System.err); -// System.exit(1); -// break; -// } -// } -// -// if (inputPath == null || outputPath == null || namesAndDocsPath == null) { -// System.err.println("Missing arguments"); -// printUsage(System.err); -// System.exit(1); -// } -// -// } } diff --git a/cli/src/main/java/net/neoforged/jst/cli/OrderedParallelWorkQueue.java b/cli/src/main/java/net/neoforged/jst/cli/OrderedParallelWorkQueue.java index 2d26509..544145a 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/OrderedParallelWorkQueue.java +++ b/cli/src/main/java/net/neoforged/jst/cli/OrderedParallelWorkQueue.java @@ -1,10 +1,10 @@ package net.neoforged.jst.cli; -import net.neoforged.jst.api.FileEntry; import net.neoforged.jst.api.FileSink; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.file.attribute.FileTime; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -61,17 +61,27 @@ public void submitAsync(Consumer producer) { } } - private static final class ParallelSink implements FileSink { + private final class ParallelSink implements FileSink { private final List workResults = new ArrayList<>(); + @Override + public boolean canHaveMultipleEntries() { + return sink.canHaveMultipleEntries(); + } + @Override public boolean isOrdered() { return false; } @Override - public void put(FileEntry entry, byte[] content) { - workResults.add(new WorkResult(entry, content)); + public void putDirectory(String relativePath) { + workResults.add(new WorkResult(true, relativePath, null, null)); + } + + @Override + public void putFile(String relativePath, FileTime lastModified, byte[] content) { + workResults.add(new WorkResult(false, relativePath, lastModified, content)); } } @@ -87,7 +97,11 @@ private void drainTo(int drainTo) throws InterruptedException, IOException { throw new RuntimeException(e.getCause()); } for (var workResult : workResults) { - sink.put(workResult.entry, workResult.content); + if (workResult.directory) { + sink.putDirectory(workResult.relativePath); + } else { + sink.putFile(workResult.relativePath, workResult.lastModified, workResult.content); + } } } } @@ -103,6 +117,6 @@ public void close() throws IOException { } } - private record WorkResult(FileEntry entry, byte[] content) { + private record WorkResult(boolean directory, String relativePath, FileTime lastModified, byte[] content) { } } diff --git a/cli/src/main/java/net/neoforged/jst/cli/PathType.java b/cli/src/main/java/net/neoforged/jst/cli/PathType.java index 992501d..2c450b8 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/PathType.java +++ b/cli/src/main/java/net/neoforged/jst/cli/PathType.java @@ -2,7 +2,7 @@ public enum PathType { AUTO, - SINGLE_FILE, + FILE, ARCHIVE, FOLDER } diff --git a/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java index 1d511f6..e4cbbc6 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java +++ b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java @@ -15,6 +15,8 @@ import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.List; /** @@ -24,13 +26,15 @@ class SourceFileProcessor implements AutoCloseable { private final IntelliJEnvironmentImpl ijEnv = new IntelliJEnvironmentImpl(); private int maxQueueDepth = 50; - private boolean enableJavadoc = true; public SourceFileProcessor() throws IOException { ijEnv.addCurrentJdkToClassPath(); } public void process(FileSource source, FileSink sink, List transformers) throws IOException { + if (source.canHaveMultipleEntries() && !sink.canHaveMultipleEntries()) { + throw new IllegalStateException("Cannot have an input with possibly more than one file when the output is a single file."); + } var context = new TransformContext(ijEnv, source, sink); @@ -44,14 +48,22 @@ public void process(FileSource source, FileSink sink, List tr if (sink.isOrdered()) { try (var stream = source.streamEntries()) { stream.forEach(entry -> { - processEntry(entry, sourceRoot, transformers, sink); + try { + processEntry(entry, sourceRoot, transformers, sink); + } catch (IOException e) { + throw new UncheckedIOException(e); + } }); } } else { try (var asyncOut = new OrderedParallelWorkQueue(sink, maxQueueDepth); var stream = source.streamEntries()) { stream.forEach(entry -> asyncOut.submitAsync(parallelSink -> { - processEntry(entry, sourceRoot, transformers, parallelSink); + try { + processEntry(entry, sourceRoot, transformers, parallelSink); + } catch (IOException e) { + throw new UncheckedIOException(e); + } })); } } @@ -61,15 +73,23 @@ public void process(FileSource source, FileSink sink, List tr } } - private void processEntry(FileEntry entry, VirtualFile sourceRoot, List transformers, FileSink sink) { + private void processEntry(FileEntry entry, VirtualFile sourceRoot, List transformers, FileSink sink) throws IOException { + if (entry.directory()) { + sink.putDirectory(entry.relativePath()); + return; + } + try (var in = entry.openInputStream()) { byte[] content = in.readAllBytes(); + var lastModified = entry.lastModified(); if (entry.hasExtension("java")) { + var orgContent = content; content = transformSource(sourceRoot, entry.relativePath(), transformers, content); + if (orgContent != content) { + lastModified = FileTime.from(Instant.now()); + } } - sink.put(entry, content); - } catch (IOException e) { - throw new UncheckedIOException(e); + sink.putFile(entry.relativePath(), lastModified, content); } } @@ -109,16 +129,12 @@ public void setMaxQueueDepth(int maxQueueDepth) { this.maxQueueDepth = maxQueueDepth; } - public void setEnableJavadoc(boolean enableJavadoc) { - this.enableJavadoc = enableJavadoc; + public void addLibrariesList(Path librariesList) throws IOException { + ClasspathSetup.addLibraries(librariesList, ijEnv); } @Override public void close() throws IOException { ijEnv.close(); } - - public void addLibrariesList(Path librariesList) throws IOException { - ClasspathSetup.addLibraries(librariesList, ijEnv); - } } diff --git a/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java b/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java index 4032b20..fbb9838 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java +++ b/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java @@ -23,6 +23,9 @@ private ClasspathSetup() { public static void addJdkModules(Path jdkHome, JavaCoreProjectEnvironment javaEnv) { var jrtFileSystem = javaEnv.getEnvironment().getJrtFileSystem(); + if (jrtFileSystem == null) { + throw new IllegalStateException("No JRT file system was configured"); + } VirtualFile jdkVfsRoot = jrtFileSystem.findFileByPath(jdkHome.toAbsolutePath() + URLUtil.JAR_SEPARATOR); if (jdkVfsRoot == null) { @@ -62,19 +65,25 @@ public static void addJdkModules(Path jdkHome, JavaCoreProjectEnvironment javaEn } public static void addLibraries(Path librariesPath, IntelliJEnvironmentImpl ijEnv) throws IOException { - var libraryFiles = Files.readAllLines(librariesPath) - .stream() - .filter(l -> l.startsWith("-e=")) - .map(l -> l.substring(3)) - .map(Paths::get) - .toList(); + for (String libraryLine : Files.readAllLines(librariesPath)) { + libraryLine = libraryLine.trim(); + + // Support the fucked up list-libraries format of Vineflower command-line options + if (libraryLine.startsWith("-e=")) { + libraryLine = libraryLine.substring("-e=".length()); + } + + if (libraryLine.isBlank()) { + continue; + } + + var libraryPath = Paths.get(libraryLine); - for (var libraryFile : libraryFiles) { - if (!Files.exists(libraryFile)) { - throw new UncheckedIOException(new FileNotFoundException(libraryFile.toString())); + if (!Files.exists(libraryPath)) { + throw new UncheckedIOException(new FileNotFoundException(libraryLine)); } - ijEnv.addJarToClassPath(libraryFile); - System.out.println("Added " + libraryFile); + ijEnv.addJarToClassPath(libraryPath); + System.out.println("Added " + libraryPath); } } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSink.java b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSink.java index 8d7b9cd..a99ea31 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSink.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSink.java @@ -1,18 +1,15 @@ package net.neoforged.jst.cli.io; import net.neoforged.jst.api.FileSink; -import net.neoforged.jst.api.FileEntry; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; -import java.time.Instant; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -public class ArchiveFileSink implements FileSink { +class ArchiveFileSink implements FileSink { private final ZipOutputStream zout; public ArchiveFileSink(Path path) throws IOException { @@ -25,9 +22,20 @@ public boolean isOrdered() { } @Override - public void put(FileEntry entry, byte[] content) throws IOException { - var ze = new ZipEntry(entry.relativePath()); - ze.setLastModifiedTime(FileTime.from(Instant.now())); + public void putDirectory(String relativePath) throws IOException { + if (!relativePath.endsWith("/")) { + relativePath += "/"; + } + + var ze = new ZipEntry(relativePath); + zout.putNextEntry(ze); + zout.closeEntry(); + } + + @Override + public void putFile(String relativePath, FileTime lastModified, byte[] content) throws IOException { + var ze = new ZipEntry(relativePath); + ze.setLastModifiedTime(lastModified); zout.putNextEntry(ze); zout.write(content); zout.closeEntry(); @@ -37,4 +45,9 @@ public void put(FileEntry entry, byte[] content) throws IOException { public void close() throws IOException { this.zout.close(); } + + @Override + public boolean canHaveMultipleEntries() { + return true; + } } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSource.java b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSource.java index 8bbbe44..04c6fa5 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSource.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSource.java @@ -3,15 +3,16 @@ import com.intellij.openapi.vfs.StandardFileSystems; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; -import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.api.FileEntries; import net.neoforged.jst.api.FileEntry; +import net.neoforged.jst.api.FileSource; import java.io.IOException; import java.nio.file.Path; import java.util.stream.Stream; import java.util.zip.ZipFile; -public class ArchiveFileSource implements FileSource { +class ArchiveFileSource implements FileSource { private final Path path; private final ZipFile zipFile; @@ -27,7 +28,12 @@ public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { @Override public Stream streamEntries() { - return zipFile.stream().map(ze -> new ZipFileEntry(zipFile, ze)); + return zipFile.stream().map(ze -> FileEntries.ofZipEntry(zipFile, ze)); + } + + @Override + public boolean canHaveMultipleEntries() { + return true; } @Override diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FileSinks.java b/cli/src/main/java/net/neoforged/jst/cli/io/FileSinks.java index 755a486..f759d3f 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/FileSinks.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FileSinks.java @@ -5,6 +5,7 @@ import net.neoforged.jst.cli.PathType; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; public final class FileSinks { @@ -14,9 +15,13 @@ private FileSinks() { public static FileSink create(Path path, PathType format, FileSource source) throws IOException { if (format == PathType.AUTO) { if (source instanceof SingleFileSource) { - format = PathType.SINGLE_FILE; + format = PathType.FILE; } else if (source instanceof ArchiveFileSource) { - format = PathType.ARCHIVE; + if (Files.isDirectory(path)) { + format = PathType.FOLDER; + } else { + format = PathType.ARCHIVE; + } } else if (source instanceof FolderFileSource) { format = PathType.FOLDER; } else { @@ -26,7 +31,7 @@ public static FileSink create(Path path, PathType format, FileSource source) thr return switch (format) { case AUTO -> throw new IllegalArgumentException("Do not support AUTO for output when input also was AUTO!"); - case SINGLE_FILE -> new SingleFileSink(path); + case FILE -> new SingleFileSink(path); case ARCHIVE -> new ArchiveFileSink(path); case FOLDER -> new FolderFileSink(path); }; diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FileSources.java b/cli/src/main/java/net/neoforged/jst/cli/io/FileSources.java index 7d837c1..e18a748 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/FileSources.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FileSources.java @@ -34,7 +34,7 @@ public static FileSource create(Path path, PathType format) throws IOException { throw new IOException("Cannot detect type of " + path + " it is neither file nor folder."); } } - case SINGLE_FILE -> { + case FILE -> { if (!Files.isRegularFile(path)) { throw new IOException("Expected " + path + " to be a file."); } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java index c7d6837..5c943cb 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java @@ -1,6 +1,5 @@ package net.neoforged.jst.cli.io; -import net.neoforged.jst.api.FileEntry; import net.neoforged.jst.api.FileSink; import java.io.IOException; @@ -8,12 +7,23 @@ import java.nio.file.Path; import java.nio.file.attribute.FileTime; -public record FolderFileSink(Path path) implements FileSink { +record FolderFileSink(Path path) implements FileSink { @Override - public void put(FileEntry entry, byte[] content) throws IOException { - var targetPath = path.resolve(entry.relativePath()); + public void putDirectory(String relativePath) throws IOException { + var targetPath = path.resolve(relativePath); + Files.createDirectories(targetPath); + } + + @Override + public void putFile(String relativePath, FileTime lastModified, byte[] content) throws IOException { + var targetPath = path.resolve(relativePath); Files.write(targetPath, content); - Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); + Files.setLastModifiedTime(targetPath, lastModified); + } + + @Override + public boolean canHaveMultipleEntries() { + return true; } @Override diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSource.java b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSource.java index a742984..b6e03e0 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSource.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSource.java @@ -2,15 +2,16 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; -import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.api.FileEntries; import net.neoforged.jst.api.FileEntry; +import net.neoforged.jst.api.FileSource; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Stream; -public record FolderFileSource(Path path) implements FileSource, AutoCloseable { +record FolderFileSource(Path path) implements FileSource, AutoCloseable { @Override public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { return vfsManager.findFileByNioPath(path); @@ -19,7 +20,13 @@ public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { @Override public Stream streamEntries() throws IOException { return Files.walk(path) - .map(child -> new PathEntry(path, child)); + .filter(p -> !p.equals(path)) + .map(child -> FileEntries.ofPath(path, child)); + } + + @Override + public boolean canHaveMultipleEntries() { + return true; } @Override diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSink.java b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSink.java index 7d59e08..b08305b 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSink.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSink.java @@ -1,25 +1,35 @@ package net.neoforged.jst.cli.io; -import net.neoforged.jst.api.FileEntry; import net.neoforged.jst.api.FileSink; +import net.neoforged.jst.api.SourceTransformer; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; -public record SingleFileSink(Path path) implements FileSink { +record SingleFileSink(Path path) implements FileSink { @Override - public void put(FileEntry entry, byte[] content) throws IOException { + public void putDirectory(String relativePath) { + throw new UnsupportedOperationException(); + } + + @Override + public void putFile(String relativePath, FileTime lastModified, byte[] content) throws IOException { Path targetPath; if (Files.isDirectory(path)) { - targetPath = path.resolve(entry.relativePath()); + targetPath = path.resolve(relativePath); } else { targetPath = path; } Files.write(targetPath, content); - Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); + Files.setLastModifiedTime(targetPath, lastModified); + } + + @Override + public boolean canHaveMultipleEntries() { + return false; } @Override diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSource.java b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSource.java index 6b7b7ac..db9a746 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSource.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSource.java @@ -2,13 +2,18 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; -import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.api.FileEntries; import net.neoforged.jst.api.FileEntry; +import net.neoforged.jst.api.FileSource; import java.nio.file.Path; import java.util.stream.Stream; -public record SingleFileSource(Path path) implements FileSource, AutoCloseable { +record SingleFileSource(Path path) implements FileSource, AutoCloseable { + SingleFileSource(Path path) { + this.path = path.toAbsolutePath(); + } + @Override public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { return vfsManager.findFileByNioPath(path.getParent()); @@ -16,7 +21,12 @@ public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { @Override public Stream streamEntries() { - return Stream.of(new PathEntry(path.getParent(), path)); + return Stream.of(FileEntries.ofPath(path.getParent(), path)); + } + + @Override + public boolean canHaveMultipleEntries() { + return false; } @Override diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java b/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java deleted file mode 100644 index 5c8d45d..0000000 --- a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.neoforged.jst.cli.io; - -import net.neoforged.jst.api.FileEntry; - -import java.io.InputStream; - -public record SourceEntryWithContent(FileEntry sourceEntry, InputStream contentStream) { -} diff --git a/tests/build.gradle b/tests/build.gradle index 94be220..05492c1 100644 --- a/tests/build.gradle +++ b/tests/build.gradle @@ -19,6 +19,9 @@ dependencies { testImplementation "org.assertj:assertj-core:$assertj_version" } +/** + * Delayed expansion for passing the path to the executable jar to the tests via system property. + */ abstract class ExecutableArgumentProvider implements CommandLineArgumentProvider { @InputFiles @PathSensitive(PathSensitivity.RELATIVE) @@ -38,8 +41,4 @@ test { } ) systemProperty("jst.testDataDir", "${project.projectDir}/data") - - if (Boolean.getBoolean("test.debug") || Boolean.getBoolean("jst.debug") || System.getProperty("idea.debugger.dispatch.port") != null) { - systemProperty("jst.debug", "true") - } } diff --git a/tests/data/single_file/Test.java b/tests/data/single_file/Test.java new file mode 100644 index 0000000..e24f4f9 --- /dev/null +++ b/tests/data/single_file/Test.java @@ -0,0 +1 @@ +class Test{} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java new file mode 100644 index 0000000..e32ea52 --- /dev/null +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -0,0 +1,422 @@ +package net.neoforged.jst.tests; + +import net.neoforged.jst.cli.Main; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test that references to external classes in method signatures are correctly resolved. + */ +public class EmbeddedTest { + private final Path testDataRoot = Paths.get(getRequiredSystemProperty("jst.testDataDir")); + + @TempDir + private Path tempDir; + + @Nested + class SingleFileSource { + + @Test + void singleFileOutput() throws Exception { + var singleFile = testDataRoot.resolve("single_file/Test.java"); + var outputFile = tempDir.resolve("Test.java"); + + runTool(singleFile.toString(), outputFile.toString()); + + assertThat(loadDirToMap(tempDir)).isEqualTo(loadDirToMap(singleFile.getParent())); + } + + @Test + void singleFileOutputThatAlreadyExists() throws Exception { + var singleFile = testDataRoot.resolve("single_file/Test.java"); + var outputFile = tempDir.resolve("Test.java"); + Files.write(outputFile, new byte[0]); + + runTool(singleFile.toString(), outputFile.toString()); + + assertThat(loadDirToMap(tempDir)).isEqualTo(loadDirToMap(singleFile.getParent())); + } + + @Test + void singleFileOutputThatIsAFolder() throws Exception { + var singleFile = testDataRoot.resolve("single_file/Test.java"); + // If the target exists and is a folder, it should create Test.java in it + var outputFile = tempDir; + + runTool(singleFile.toString(), outputFile.toString()); + + assertThat(loadDirToMap(tempDir)).isEqualTo(loadDirToMap(singleFile.getParent())); + } + + @Test + void folderOutput() throws Exception { + var singleFile = testDataRoot.resolve("single_file/Test.java"); + var outputFile = tempDir; + + runTool(singleFile.toString(), "--out-format", "folder", outputFile.toString()); + + var actualContent = loadDirToMap(tempDir); + var expectedContent = loadDirToMap(singleFile.getParent()); + assertThat(actualContent).isEqualTo(expectedContent); + } + + @Test + void archiveOutput() throws Exception { + var singleFile = testDataRoot.resolve("single_file/Test.java"); + var outputFile = tempDir.resolve("archive.zip"); + + runTool(singleFile.toString(), "--out-format", "archive", outputFile.toString()); + + var actualContent = loadZipToMap(outputFile); + var expectedCotent = truncateTimes(loadDirToMap(singleFile.getParent())); + assertThat(actualContent).isEqualTo(expectedCotent); + } + } + + @Nested + class FolderSource { + @Test + void singleFileOutput() { + var sourceFolder = testDataRoot.resolve("nested/source"); + var outputFile = tempDir.resolve("Output.java"); + + // We do not have a unified exception here + var e = assertThrows(Throwable.class, () -> { + runTool(sourceFolder.toString(), "--out-format", "file", outputFile.toString()); + }); + assertThat(e).hasMessageContaining("Cannot have an input with possibly more than one file when the output is a single file."); + } + + @Test + void folderOutput() throws Exception { + var sourceFolder = testDataRoot.resolve("nested/source"); + + runTool(sourceFolder.toString(), tempDir.toString()); + + assertThat(loadDirToMap(tempDir)).isEqualTo(loadDirToMap(sourceFolder)); + } + + @Test + void archiveOutput() throws Exception { + var sourceFolder = testDataRoot.resolve("nested/source"); + var outputFile = tempDir.resolve("archive.zip"); + + runTool(sourceFolder.toString(), "--out-format", "archive", outputFile.toString()); + + var actualContent = loadZipToMap(outputFile); + var expectedContent = truncateTimes(loadDirToMap(sourceFolder)); + assertThat(actualContent).isEqualTo(expectedContent); + } + } + + @Nested + class ArchiveSource { + Path inputFile; + Map expectedContent; + + @BeforeEach + void setUp() throws IOException { + inputFile = tempDir.resolve("input.zip"); + var sourceFolder = testDataRoot.resolve("nested/source"); + zipDirectory(sourceFolder, inputFile, p -> true); + expectedContent = truncateTimes(loadDirToMap(sourceFolder)); + } + + @Test + void singleFileOutput() { + var outputFile = tempDir.resolve("Test.java"); + + // We do not have a unified exception here + var e = assertThrows(Throwable.class, () -> { + runTool(inputFile.toString(), "--out-format", "file", outputFile.toString()); + }); + assertThat(e).hasMessageContaining("Cannot have an input with possibly more than one file when the output is a single file."); + } + + @Test + void folderOutput() throws Exception { + var outputFolder = tempDir.resolve("output"); + Files.createDirectories(outputFolder); + + runTool(inputFile.toString(), outputFolder.toString()); + + assertThat(loadDirToMap(outputFolder)).isEqualTo(expectedContent); + } + + @Test + void archiveOutput() throws Exception { + var sourceFolder = testDataRoot.resolve("nested/source"); + var outputFile = tempDir.resolve("archive.zip"); + + runTool(sourceFolder.toString(), "--out-format", "archive", outputFile.toString()); + + var actualContent = loadZipToMap(outputFile); + assertThat(actualContent).isEqualTo(expectedContent); + } + } + + @Test + void testInnerAndLocalClasses() throws Exception { + runTest("nested"); + } + + @Test + void testExternalReferences() throws Exception { + runTest("external_refs"); + } + + @Test + void testParamIndices() throws Exception { + runTest("param_indices"); + } + + @Test + void testJavadoc() throws Exception { + runTest("javadoc"); + } + + protected final void runTest(String testDirName) throws Exception { + var testDir = testDataRoot.resolve(testDirName); + var parchmentFile = testDir.resolve("parchment.json"); + var sourceDir = testDir.resolve("source"); + var expectedDir = testDir.resolve("expected"); + + var inputFile = tempDir.resolve("input.jar"); + zipDirectory(sourceDir, inputFile, path -> { + return Files.isDirectory(path) || path.getFileName().toString().endsWith(".java"); + }); + + var outputFile = tempDir.resolve("output.jar"); + + // For testing external references, add JUnit-API, so it can be referenced + var junitJarPath = Paths.get(Test.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + var librariesFile = tempDir.resolve("libraries.txt"); + Files.write(librariesFile, List.of("-e=" + junitJarPath)); + + runTool( + "--libraries-list", + librariesFile.toString(), + "--enable-parchment", + "--parchment-mappings", + parchmentFile.toString(), + inputFile.toString(), + outputFile.toString() + ); + + try (var zipFile = new ZipFile(outputFile.toFile())) { + var it = zipFile.entries().asIterator(); + while (it.hasNext()) { + var entry = it.next(); + if (entry.isDirectory()) { + continue; + } + + var actualFile = normalizeLines(new String(zipFile.getInputStream(entry).readAllBytes(), StandardCharsets.UTF_8)); + var expectedFile = normalizeLines(Files.readString(expectedDir.resolve(entry.getName()), StandardCharsets.UTF_8)); + assertEquals(expectedFile, actualFile); + } + } + } + + protected void runTool(String... args) throws Exception { + // This is thread hostile, but what can I do :-[ + var oldOut = System.out; + var oldErr = System.err; + var capturedOut = new ByteArrayOutputStream(); + int exitCode; + try { + System.setErr(new PrintStream(capturedOut)); + System.setOut(new PrintStream(capturedOut)); + // Run in-process for easier debugging + exitCode = Main.innerMain(args); + } finally { + System.setErr(oldErr); + System.setOut(oldOut); + } + + var capturedOutString = capturedOut.toString(StandardCharsets.UTF_8); + + if (exitCode != 0) { + throw new RuntimeException("Process failed with exit code 0: " + capturedOutString); + } + } + + protected static String getRequiredSystemProperty(String key) { + var value = System.getProperty(key); + if (value == null) { + throw new RuntimeException("Missing system property: " + key); + } + return value; + } + + private String normalizeLines(String s) { + return s.replaceAll("\r\n", "\n"); + } + + private static void zipDirectory(Path directory, Path destinationPath, Predicate filter) throws IOException { + try (var zOut = new ZipOutputStream(Files.newOutputStream(destinationPath)); + var files = Files.walk(directory)) { + files.filter(filter).forEach(path -> { + // Skip visiting the root directory itself + if (path.equals(directory)) { + return; + } + + var relativePath = directory.relativize(path).toString().replace('\\', '/'); + + try { + if (Files.isDirectory(path)) { + var entry = new ZipEntry(relativePath + "/"); + zOut.putNextEntry(entry); + zOut.closeEntry(); + } else { + var entry = new ZipEntry(relativePath); + entry.setLastModifiedTime(Files.getLastModifiedTime(path)); + + zOut.putNextEntry(entry); + Files.copy(path, zOut); + zOut.closeEntry(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } + + private static Map loadZipToMap(Path archive) throws IOException { + var result = new HashMap(); + + try (var zin = new ZipInputStream(Files.newInputStream(archive))) { + for (var entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) { + if (entry.isDirectory()) { + // strip trailing / + var name = entry.getName(); + if (name.endsWith("/")) { + name = name.substring(0, name.length() - 1); + } + result.put(name, new Directory()); + } else if (entry.getName().endsWith(".java")) { + result.put(entry.getName(), new TextFile( + entry.getLastModifiedTime(), + new String(zin.readAllBytes(), StandardCharsets.UTF_8) + )); + } else { + result.put(entry.getName(), new BinaryFile( + entry.getLastModifiedTime(), + zin.readAllBytes() + )); + } + } + } + + return result; + } + + /** + * Truncates times to the same precision as used by ZIP-files + */ + private static Map truncateTimes(Map tree) throws IOException { + return tree.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + v -> { + if (v.getValue() instanceof BinaryFile binaryFile) { + return new BinaryFile(truncateTime(binaryFile.fileTime), binaryFile.content()); + } else if (v.getValue() instanceof TextFile textFile) { + return new TextFile(truncateTime(textFile.fileTime), textFile.content()); + } else { + return v.getValue(); + } + } + )); + } + + private static FileTime truncateTime(FileTime fileTime) { + // Apparently it can only do millisecond precision, weird. + return FileTime.from(fileTime.toMillis() / 1000, TimeUnit.SECONDS); + } + + private static Map loadDirToMap(Path folder) throws IOException { + try (Stream stream = Files.walk(folder)) { + return stream + .filter(p -> !p.equals(folder)) + .collect(Collectors.toMap( + p -> folder.relativize(p).toString().replace('\\', '/'), + p -> { + try { + if (Files.isDirectory(p)) { + return new Directory(); + } else if (p.getFileName().toString().endsWith(".java")) { + return new TextFile( + Files.getLastModifiedTime(p), + Files.readString(p, StandardCharsets.UTF_8) + ); + } else { + return new BinaryFile( + Files.getLastModifiedTime(p), + Files.readAllBytes(p) + ); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + )); + } + } + + interface DirectoryTreeElement { + } + + record Directory() implements DirectoryTreeElement { + } + + record TextFile(FileTime fileTime, String content) implements DirectoryTreeElement { + } + + record BinaryFile(FileTime fileTime, byte[] content) implements DirectoryTreeElement { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BinaryFile that = (BinaryFile) o; + return Objects.equals(fileTime, that.fileTime) && Arrays.equals(content, that.content); + } + + @Override + public int hashCode() { + int result = Objects.hash(fileTime); + result = 31 * result + Arrays.hashCode(content); + return result; + } + } +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/ExecutableJarTest.java b/tests/src/test/java/net/neoforged/jst/tests/ExecutableJarTest.java new file mode 100644 index 0000000..dffdbca --- /dev/null +++ b/tests/src/test/java/net/neoforged/jst/tests/ExecutableJarTest.java @@ -0,0 +1,39 @@ +package net.neoforged.jst.tests; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Runs the same tests as {@link EmbeddedTest}, but runs them by actually running the executable jar + * in an external process. + */ +public class ExecutableJarTest extends EmbeddedTest { + @Override + protected void runTool(String... args) throws Exception { + var javaExecutablePath = ProcessHandle.current() + .info() + .command() + .orElseThrow(); + + List commandLine = new ArrayList<>(); + commandLine.add(javaExecutablePath); + commandLine.add("-jar"); + commandLine.add(getRequiredSystemProperty("jst.executableJar")); + Collections.addAll(commandLine, args); + + var process = new ProcessBuilder(commandLine) + .redirectErrorStream(true) + .start(); + + process.getOutputStream().close(); // Close stdin to java + + byte[] output = process.getInputStream().readAllBytes(); + System.out.println(new String(output)); + + int exitCode = process.waitFor(); + assertEquals(0, exitCode); + } +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/MainTest.java b/tests/src/test/java/net/neoforged/jst/tests/MainTest.java deleted file mode 100644 index 2425f08..0000000 --- a/tests/src/test/java/net/neoforged/jst/tests/MainTest.java +++ /dev/null @@ -1,168 +0,0 @@ -package net.neoforged.jst.tests; - -import net.neoforged.jst.cli.Main; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Predicate; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Test that references to external classes in method signatures are correctly resolved. - */ -public class MainTest { - @TempDir - private Path tempDir; - - @Test - void testInnerAndLocalClasses() throws Exception { - runTest("nested"); - } - - @Test - void testExternalReferences() throws Exception { - runTest("external_refs"); - } - - @Test - void testParamIndices() throws Exception { - runTest("param_indices"); - } - - @Test - void testJavadoc() throws Exception { - runTest("javadoc"); - } - - protected final void runTest(String testDir) throws Exception { - Path testDataRoot = Paths.get(getRequiredSystemProperty("jst.testDataDir")) - .resolve(testDir); - - var parchmentFile = testDataRoot.resolve("parchment.json"); - var sourceDir = testDataRoot.resolve("source"); - var expectedDir = testDataRoot.resolve("expected"); - - var inputFile = tempDir.resolve("input.jar"); - zipDirectory(sourceDir, inputFile, path -> { - return Files.isDirectory(path) || path.getFileName().toString().endsWith(".java"); - }); - - var outputFile = tempDir.resolve("output.jar"); - - // For testing external references, add JUnit-API, so it can be referenced - var junitJarPath = Paths.get(Test.class.getProtectionDomain().getCodeSource().getLocation().toURI()); - var librariesFile = tempDir.resolve("libraries.txt"); - Files.write(librariesFile, List.of("-e=" + junitJarPath)); - - runExecutableJar( - "--libraries-list", - librariesFile.toString(), - "--enable-parchment", - "--parchment-mappings", - parchmentFile.toString(), - inputFile.toString(), - outputFile.toString() - ); - - try (var zipFile = new ZipFile(outputFile.toFile())) { - var it = zipFile.entries().asIterator(); - while (it.hasNext()) { - var entry = it.next(); - if (entry.isDirectory()) { - continue; - } - - var actualFile = normalizeLines(new String(zipFile.getInputStream(entry).readAllBytes(), StandardCharsets.UTF_8)); - var expectedFile = normalizeLines(Files.readString(expectedDir.resolve(entry.getName()), StandardCharsets.UTF_8)); - assertEquals(expectedFile, actualFile); - } - } - } - - private static void runExecutableJar(String... args) throws IOException, InterruptedException { - // Run in-process for easier debugging - if (Boolean.getBoolean("jst.debug")) { - Main.innerMain(args); - return; - } - - var javaExecutablePath = ProcessHandle.current() - .info() - .command() - .orElseThrow(); - - List commandLine = new ArrayList<>(); - commandLine.add(javaExecutablePath); - commandLine.add("-jar"); - commandLine.add(getRequiredSystemProperty("jst.executableJar")); - Collections.addAll(commandLine, args); - - var process = new ProcessBuilder(commandLine) - .redirectErrorStream(true) - .start(); - - process.getOutputStream().close(); // Close stdin to java - - byte[] output = process.getInputStream().readAllBytes(); - System.out.println(new String(output)); - - int exitCode = process.waitFor(); - assertEquals(0, exitCode); - } - - private static String getRequiredSystemProperty(String key) { - var value = System.getProperty(key); - if (value == null) { - throw new RuntimeException("Missing system property: " + key); - } - return value; - } - - private String normalizeLines(String s) { - return s.replaceAll("\r\n", "\n"); - } - - private static void zipDirectory(Path directory, Path destinationPath, Predicate filter) throws IOException { - try (var zOut = new ZipOutputStream(Files.newOutputStream(destinationPath)); - var files = Files.walk(directory)) { - files.filter(filter).forEach(path -> { - // Skip visiting the root directory itself - if (path.equals(directory)) { - return; - } - - var relativePath = directory.relativize(path).toString().replace('\\', '/'); - - try { - if (Files.isDirectory(path)) { - var entry = new ZipEntry(relativePath + "/"); - zOut.putNextEntry(entry); - zOut.closeEntry(); - } else { - var entry = new ZipEntry(relativePath); - entry.setLastModifiedTime(Files.getLastModifiedTime(path)); - - zOut.putNextEntry(entry); - Files.copy(path, zOut); - zOut.closeEntry(); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } - } -}