package org.zwobble.hobgoblin.compiler; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import org.zwobble.hobgoblin.compiler.testing.TestProcessRunner; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.zwobble.precisely.AssertThat.assertThat; import static org.zwobble.precisely.Matchers.equalTo; public class ExampleTests { @TestFactory public Stream tests() { return ExampleSet.exampleSets() .map(exampleSet -> DynamicTest.dynamicTest( exampleSet.name(), () -> runTest(exampleSet) )); } private void runTest(ExampleSet exampleSet) throws IOException, InterruptedException, NoSuchAlgorithmException { HobgoblinCompiler.compile(exampleSet.path()); var javaOutputPath = verifyJavaOutput(exampleSet); var rustOutputPath = verifyRustOutput(exampleSet); verifySqlite3Output(exampleSet); verifyTypescriptOutput(exampleSet); var transient0OutputDirs = Stream.of(javaOutputPath, rustOutputPath) .flatMap(outputPath -> outputPath.stream()) .map(outputPath -> outputPath.resolve("transient-0")) .filter(transient0OutputPath -> transient0OutputPath.toFile().exists()) .toList(); var names = transient0OutputDirs.stream() .flatMap(outputPath -> Arrays.stream(outputPath.toFile().listFiles()).map(file -> file.getName())) .collect(Collectors.toSet()) .stream() .sorted() .toList(); for (var name : names) { var expectedBytes = Optional.empty(); for (var transient0OutputDir : transient0OutputDirs) { var actualBytes = Files.readAllBytes(transient0OutputDir.resolve(name)); if (expectedBytes.isEmpty()) { expectedBytes = Optional.of(actualBytes); } else { assertThat(Arrays.equals(actualBytes, expectedBytes.get()), equalTo(true)); } } } } private Optional verifyJavaOutput(ExampleSet exampleSet) throws NoSuchAlgorithmException, IOException, InterruptedException { return verifyOutput(exampleSet, "java", List.of(List.of("mvn", "test"))); } private Optional verifyRustOutput(ExampleSet exampleSet) throws NoSuchAlgorithmException, IOException, InterruptedException { return verifyOutput(exampleSet, "rust", List.of(List.of("cargo", "test"))); } private void verifySqlite3Output( ExampleSet exampleSet ) throws NoSuchAlgorithmException, IOException, InterruptedException { verifyOutput( exampleSet, "sqlite3", List.of( List.of("python3", "-m", "unittest", "test.py") ) ); } private Optional verifyTypescriptOutput(ExampleSet exampleSet) throws NoSuchAlgorithmException, IOException, InterruptedException { return verifyOutput(exampleSet, "typescript", List.of( List.of("pnpm", "install"), List.of("./node_modules/.bin/tsc", "--noEmit"), List.of("pnpm", "test")) ); } private Optional verifyOutput( ExampleSet exampleSet, String name, List> commands ) throws NoSuchAlgorithmException, IOException, InterruptedException { var outputPath = exampleSet.path().resolve("output").resolve(name); if (!outputPath.toFile().exists()) { return Optional.empty(); } var currentHash = hashJavaProject(outputPath); var lastTestRunPath = outputPath.resolve("last-test-run.properties"); var lastTestRun = readLastTestRun(lastTestRunPath); if (lastTestRun.isPresent() && lastTestRun.get().pass && lastTestRun.get().hash.equals(currentHash)) { return Optional.of(outputPath); } try { for (var command : commands) { TestProcessRunner.command(command) .cwd(outputPath) .addEnvVar("HOBGOBLIN_OUTPUT_DIR", outputPath.toAbsolutePath().toString()) .runCaptureOutput() .throwOnError(); } writeLastTestRun(lastTestRunPath, new LastTestRun(currentHash, true)); } catch (Throwable exception) { writeLastTestRun(lastTestRunPath, new LastTestRun(currentHash, false)); throw exception; } return Optional.of(outputPath); } private record LastTestRun(String hash, boolean pass) {} private Optional readLastTestRun(Path path) throws IOException { var properties = new Properties(); try (var reader = Files.newBufferedReader(path)) { properties.load(reader); var hash = properties.getProperty("hash"); var pass = properties.getProperty("result").equals("PASS"); return Optional.of(new LastTestRun(hash, pass)); } catch (NoSuchFileException exception) { return Optional.empty(); } } private void writeLastTestRun(Path path, LastTestRun lastTestRun) throws IOException { var properties = new Properties(); properties.setProperty("hash", lastTestRun.hash); properties.setProperty("result", lastTestRun.pass ? "PASS" : "FAIL"); try (var writer = Files.newBufferedWriter(path)) { properties.store(writer, null); } } private String hashJavaProject(Path javaProjectPath) throws NoSuchAlgorithmException, IOException { var paths = collectJavaProjectSourcePaths(javaProjectPath); var digest = MessageDigest.getInstance("sha256"); for (var path : paths) { digest.update(path.toString().getBytes(StandardCharsets.UTF_8)); digest.update(new byte[]{0}); var contents = Files.readAllBytes(path); digest.update(contents); digest.update(new byte[]{0}); } return bytesToHex(digest.digest()); } private static String bytesToHex(byte[] bytes) { var hexString = new StringBuilder(2 * bytes.length); for (var b : bytes) { var hex = Integer.toHexString(0xff & b); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); } private static List collectJavaProjectSourcePaths(Path javaProjectPath) throws IOException { var paths = new ArrayList(); Files.walkFileTree( javaProjectPath, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) { if (!javaProjectPath.relativize(file).toString().equals("last-test-run.properties")) { paths.add(file); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (javaProjectPath.relativize(dir).startsWith("target")) { return FileVisitResult.SKIP_SUBTREE; } else { return FileVisitResult.CONTINUE; } } } ); paths.sort(Comparator.naturalOrder()); return paths; } }