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() {}
}