java/de/jflex/migration/testcase/Migrator.java (234 lines of code) (raw):

/* * Copyright (C) 2019-2021 Google, LLC. * SPDX-License-Identifier: BSD-3-Clause */ package de.jflex.migration.testcase; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; import static de.jflex.migration.util.JavaResources.readResource; import com.google.common.base.CaseFormat; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Streams; import com.google.common.flogger.FluentLogger; import com.google.common.flogger.LoggerConfig; import com.google.common.io.CharSink; import com.google.common.io.CharSource; import com.google.common.io.Files; import de.jflex.testing.testsuite.golden.GoldenInOutFilePair; import de.jflex.util.javac.JavaPackageUtils; import de.jflex.velocity.Velocity; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Comparator; import java.util.logging.Level; import org.apache.velocity.runtime.parser.ParseException; /** * Tool to migrate a test case from {@code //testsuite/testcases/src/test/cases} (as executed by the * jflex-testsuite-amven-plugin) to {@code //ajvatests/jflex/testcase} (as executed by bazel). * * <p>See <a href="README.md">README</a> for usage. */ public class Migrator { private static final String PATH = JavaPackageUtils.getPathForClass(Migrator.class); private static final String TEST_CASE_TEMPLATE = PATH + "/TestCase.java.vm"; private static final String BUILD_HEADER = "java/" + PATH + "/BUILD-header.bzl"; private static final String BUILD_TEMPLATE = PATH + "/BUILD.vm"; private static final String GOLDEN_INPUT_EXT = ".input"; private static final String GOLDEN_OUTPUT_EXT = ".output"; private static final FluentLogger logger = FluentLogger.forEnclosingClass(); public static final String TESTING_PACKAGE = "de.jflex.testcase."; private static final String TESTING_PACKAGE_DIR = TESTING_PACKAGE.replace('.', File.separatorChar); public static void main(String[] args) { LoggerConfig.of(logger).setLevel(Level.FINEST); checkArgument(args.length > 0, "Syntax error: migrator TESTCASE_DIRS_ABS_PATH"); try { for (String testCaseDir : args) { migrateCase(testCaseDir); } } catch (MigrationException e) { logger.atSevere().withCause(e).log("Migration failed"); } } /** Migrates one given test-case directory. */ private static void migrateCase(String testCase) throws MigrationException { File dir = new File(testCase); if (!dir.exists()) { logger.atWarning().log("Directory doesn't exist: %s", dir.getName()); throw new MigrationException( "Could not migrate " + testCase, new FileNotFoundException(dir.getAbsolutePath())); } migrateCase(dir); } /** * Migrates one given test-case directory. * * <ol> * <li>Creates a target folder in {@code /tmp} based on the original directory name (replaces * '-' by '_') * <li>Initializes a BUILD file * <li>Finds all test specs, and migrate them. * </ol> */ private static void migrateCase(File testCaseDir) throws MigrationException { logger.atInfo().log("Migrating %s...", testCaseDir.getName()); logger.atFine().log("location: %s", testCaseDir.getAbsolutePath()); File outputDir = initTargetDir(testCaseDir); File buildFile = initBuildFile(outputDir); Iterable<File> originalDirectoryContent = Files.fileTraverser().breadthFirst(testCaseDir); ImmutableList<File> testSpecFiles = Streams.stream(originalDirectoryContent) .filter(f -> Files.getFileExtension(f.getName()).equals("test")) .sorted(Comparator.comparing(File::getName)) .collect(toImmutableList()); for (File testSpec : testSpecFiles) { migrateTestCase(testCaseDir, testSpec, buildFile); } } private static File initTargetDir(File testCaseDir) { String lowerUnderscoreTestDir = CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_UNDERSCORE, testCaseDir.getName()); File outputDir = new File(new File("/tmp"), lowerUnderscoreTestDir); if (!outputDir.isDirectory()) { //noinspection ResultOfMethodCallIgnored outputDir.mkdirs(); } return outputDir; } /** Creates a new BUILD file in the given directory. If there is one, it is replaced. */ private static File initBuildFile(File outputDir) throws MigrationException { File outFile = new File(outputDir, "BUILD.bazel"); outFile.delete(); try { Files.copy(new File(BUILD_HEADER), outFile); return outFile; } catch (IOException e) { throw new MigrationException("Could not create BUILD file", e); } } /** * Migrates one given test case (such as {@code test-0}) within the test case directory. * * <p>Scans the grammar specification and creates a {@link TestCase} model. */ private static void migrateTestCase(File testCaseDir, File testSpecFile, File buildFile) throws MigrationException { try (BufferedReader reader = Files.newReader(testSpecFile, StandardCharsets.UTF_8)) { TestSpecScanner scanner = new TestSpecScanner(reader); TestCase test = scanner.load(); if (test.isExpectJavacFail() || test.isExpectJFlexFail()) { logger.atWarning().log("Test %s must be migrated with JflexTestRunner", test.getTestName()); } migrateTestCase(testCaseDir, test, buildFile); } catch (IOException e) { throw new MigrationException("Failed reading the test spec " + testSpecFile.getName(), e); } } /** * Migrates one given test case (such as {@code test-0}), represented by a {@link TestCase} data * model, within the test case directory. * * <p>Creates all velocity {@link MigrationTemplateVars} for this case. */ private static void migrateTestCase(File testCaseDir, TestCase test, File buildFile) throws MigrationException { File flexFile = findFlexFile(testCaseDir, test); ImmutableList<GoldenInOutFilePair> goldenFiles = findGoldenFiles(testCaseDir, test); String lowerUnderscoreTestDir = buildFile.getParentFile().getName(); MigrationTemplateVars templateVars = createTemplateVars(lowerUnderscoreTestDir, test, flexFile, goldenFiles); migrateTestCase(buildFile, templateVars); } /** * Migrates one given test case. * * <ol> * <li>Add targets to BUILD file * <li>Renders the java test class * <li>Modifies and copies the flex grammar in the target folder. See {@link * #copyGrammarFile(File, String, File)} * <li>Copies the golden files * </ol> */ private static void migrateTestCase(File buildFile, MigrationTemplateVars templateVars) throws MigrationException { File outputDir = buildFile.getParentFile(); logger.atInfo().log("Generating into %s", outputDir); renderBuildFile(templateVars, buildFile); renderTestCase(templateVars, outputDir); copyGrammarFile(templateVars.flexGrammar, templateVars.javaPackage, outputDir); copyGoldenFiles(templateVars.goldens, outputDir); logger.atInfo().log("Import the files in your workspace"); logger.atInfo().log( " cp -r %s $(bazel info workspace)/javatests/de/jflex/testcase", outputDir); } /** Generates the BUILD file for this test case. */ // FIXME This should be done once for the whole directory, but with many java_test() private static void renderBuildFile(MigrationTemplateVars templateVars, File buildFile) throws MigrationException { logger.atInfo().log("Generating %s", buildFile); try { Velocity.render(readResource(BUILD_TEMPLATE), "BuildBazel", templateVars, buildFile); } catch (ParseException | IOException e) { throw new MigrationException("Failed to parse Velocity template " + BUILD_TEMPLATE, e); } } /** Generates the Java test class. */ private static void renderTestCase(MigrationTemplateVars templateVars, File outputDir) throws MigrationException { File outFile = new File(outputDir, templateVars.testClassName + ".java"); try { logger.atInfo().log("Generating %s", outFile); velocityRenderTestCase(templateVars, outFile); } catch (IOException e) { throw new MigrationException("Couldn't write java test case", e); } } /** * Copy the grammar file. * * <p>The old grammars were defined in the default (empty) java package. The copied file be * prepended with a {@code package} declaration matches its directory location. */ private static void copyGrammarFile(File flexFile, String javaPackage, File outputDir) throws MigrationException { try { logger.atInfo().log("Copy grammar %s", flexFile.getName()); logger.atFine().log("location: %s", flexFile.getAbsolutePath()); // The grammars are defined in the default package. This is so bad practice that I'm not // sure that bazel allows compilation. Don't simply copy the original: // copyFile(fixedFlexFile, outputDir); // But instead: File copiedWithPatch = new File(outputDir, flexFile.getName()); CharSink out = Files.asCharSink(copiedWithPatch, StandardCharsets.UTF_8); CharSource fixedContent = CharSource.concat( CharSource.wrap(String.format("package %s;\n", javaPackage)), Files.asCharSource(flexFile, StandardCharsets.UTF_8)); fixedContent.copyTo(out); } catch (IOException e) { throw new MigrationException("Could not copy .flex file", e); } } /** Copy the list of golden files. */ private static void copyGoldenFiles(ImmutableList<GoldenInOutFilePair> goldens, File outputDir) throws MigrationException { logger.atInfo().log("Copy %d pairs of golden files", goldens.size()); try { for (GoldenInOutFilePair golden : goldens) { copyFile(golden.inputFile, outputDir); copyFile(golden.outputFile, outputDir); } } catch (IOException e) { throw new MigrationException("Could not copy golden files", e); } } /** Creates the template variables for velocity. */ private static MigrationTemplateVars createTemplateVars( String lowerUnderscoreTestDir, TestCase test, File flexGrammar, ImmutableList<GoldenInOutFilePair> goldenFiles) { MigrationTemplateVars vars = new MigrationTemplateVars(); vars.templateName = ""; vars.flexGrammar = flexGrammar; vars.javaPackage = TESTING_PACKAGE + lowerUnderscoreTestDir; vars.javaPackageDir = TESTING_PACKAGE_DIR + lowerUnderscoreTestDir; vars.testClassName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, test.getTestName()) + "GoldenTest"; vars.testName = test.getTestName(); vars.testDescription = Strings.nullToEmpty(test.getDescription()).trim(); vars.goldens = goldenFiles; // TODO(regisd). We should use the real JFLex generator to read the `%class` value from the // grammar. For now, we rely on the convention that the name of the scanner is the name of // the test... vars.scannerClassName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, test.getTestName()); return vars; } /** Lists all golden files for a given test. */ private static ImmutableList<GoldenInOutFilePair> findGoldenFiles( File testCaseDir, TestCase test) { Iterable<File> dirContent = Files.fileTraverser().breadthFirst(testCaseDir); return Streams.stream(dirContent) .filter(f -> isGoldenInputFile(test, f)) .sorted(Comparator.comparing(File::getName)) .map(f -> new GoldenInOutFilePair(f, getGoldenOutputFile(f))) .filter(g -> g.outputFile.isFile()) .collect(toImmutableList()); } /** Finds the grammar file for a test. */ private static File findFlexFile(File testCaseDir, TestCase test) { return new File(testCaseDir, test.getTestName() + ".flex"); } private static boolean isGoldenInputFile(TestCase test, File f) { return f.getName().startsWith(test.getTestName() + "-") && f.getName().endsWith(".input"); } /** Returns the output file for the given input file. */ private static File getGoldenOutputFile(File goldenInputFIle) { checkArgument(goldenInputFIle.getName().endsWith(GOLDEN_INPUT_EXT)); return new File( goldenInputFIle.getParentFile(), goldenInputFIle .getName() .substring(0, goldenInputFIle.getName().length() - ".input".length()) + GOLDEN_OUTPUT_EXT); } /** Invokes velocity to generate the Test file. */ private static void velocityRenderTestCase(MigrationTemplateVars templateVars, File output) throws IOException, MigrationException { try { Velocity.render(readResource(TEST_CASE_TEMPLATE), "TestCase", templateVars, output); } catch (ParseException e) { throw new MigrationException("Failed to parse Velocity template " + TEST_CASE_TEMPLATE, e); } } /** Copies file to the target directory. */ private static void copyFile(File file, File targetDir) throws IOException { checkArgument(file.isFile(), "Input %s should be a file: %s", file, file.getAbsoluteFile()); checkArgument( targetDir.isDirectory(), "Target %s should be a directory: %s", targetDir, targetDir.getAbsoluteFile()); logger.atFine().log("Copying %s...", file.getName()); File copiedFile = new File(targetDir, file.getName()); Files.copy(file, copiedFile); } private Migrator() {} }