diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 158e68a8..7975c73d 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -142,8 +142,8 @@ val fcovTestReport = tasks.register("fcovTestReport") { mainClass = "dev.ionfusion.fusion.cli.Cli" args = listOf("report_coverage", "--configFile", fcovConfig.asFile.path, - fcovDataDir.get().asFile.path, - fcovReportDir.get().asFile.path) + "--htmlDir", fcovReportDir.get().asFile.path, + fcovDataDir.get().asFile.path) enableAssertions = true } diff --git a/runtime/src/main/java/dev/ionfusion/fusion/cli/Cover.java b/runtime/src/main/java/dev/ionfusion/fusion/cli/Cover.java index 9dcd0f3b..b7ba64fe 100644 --- a/runtime/src/main/java/dev/ionfusion/fusion/cli/Cover.java +++ b/runtime/src/main/java/dev/ionfusion/fusion/cli/Cover.java @@ -13,6 +13,8 @@ import java.io.PrintWriter; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; /** * @@ -24,10 +26,12 @@ class Cover private static final String HELP_ONE_LINER = "Generate a code coverage report."; private static final String HELP_USAGE = - "report_coverage [--configFile FILE] COVERAGE_DATA_DIR REPORT_DIR"; + "report_coverage [--configFile FILE] --htmlDir REPORT_DIR DATA_DIR ..."; private static final String HELP_BODY = - "Reads Fusion code-coverage data from the COVERAGE_DATA_DIR, then writes an\n" + - "HTML report to the REPORT_DIR."; + "Reads Fusion code-coverage data from the DATA_DIRs, then writes an\n" + + "HTML report to the REPORT_DIR.\n" + + "\n" + + "Multiple data directories can be given, generating an aggregate report."; Cover() @@ -45,6 +49,7 @@ Object makeOptions(GlobalOptions globals) private class Options { private Path myConfigFile; + private Path myHtmlDir; public void setConfigFile(Path configFile) throws UsageException @@ -55,6 +60,16 @@ public void setConfigFile(Path configFile) } myConfigFile = configFile; } + + public void setHtmlDir(Path dir) + throws UsageException + { + if (exists(dir) && !isDirectory(dir)) + { + throw usage("--htmlDir is not a directory: " + dir); + } + myHtmlDir = dir; + } } @@ -65,45 +80,51 @@ public void setConfigFile(Path configFile) Executor makeExecutor(GlobalOptions globals, Object locals, String[] args) throws UsageException { - if (args.length != 2) return null; + if (args.length == 0) return null; - String dataPath = args[0]; - if (dataPath.isEmpty()) return null; - - // TODO Support multiple data directories, to generate an aggregate - // report from a few test suites, Gradle subprojects, etc. - Path dataDir = Paths.get(dataPath); - if (!isDirectory(dataDir) || !isReadable(dataDir)) + List dataDirs = new ArrayList<>(); + for (String dataPath : args) { - throw usage("Coverage data directory is not a readable directory: " + dataPath); + // Avoid resolving to the current directory. + if (dataPath.isEmpty()) return null; + + Path dataDir = Paths.get(dataPath); + if (!isDirectory(dataDir) || !isReadable(dataDir)) + { + throw usage("Coverage data directory is not a readable directory: " + dataDir); + } + dataDirs.add(dataDir); } - String reportPath = args[1]; - if (reportPath.isEmpty()) return null; + Options options = (Options) locals; - Path reportDir = Paths.get(reportPath); - if (exists(reportDir) && !isDirectory(reportDir)) + if (options.myHtmlDir == null) { - throw usage("Report directory is not a directory: " + reportPath); + throw usage("No HTML output directory given; provide with --htmlDir"); } - return new Executor(globals, (Options) locals, dataDir, reportDir); + if (options.myConfigFile == null && dataDirs.size() > 1) + { + throw usage("Must provide --configFile when generating an aggregate report"); + } + + return new Executor(globals, options, dataDirs); } static class Executor extends StdioExecutor { - private final Options myLocals; - private final Path myDataDir; - private final Path myReportDir; + private final Options myLocals; + private final List myDataDirs; + private final Path myReportDir; - private Executor(GlobalOptions globals, Options locals, Path dataDir, Path reportDir) + private Executor(GlobalOptions globals, Options locals, List dataDirs) { super(globals); - myLocals = locals; - myDataDir = dataDir; - myReportDir = reportDir; + myLocals = locals; + myDataDirs = dataDirs; + myReportDir = locals.myHtmlDir; } @Override @@ -117,10 +138,15 @@ public int execute(PrintWriter out, PrintWriter err) } else { - config = CoverageConfiguration.forDataDir(myDataDir); + assert myDataDirs.size() == 1; // Checked in makeExecutor() + config = CoverageConfiguration.forDataDir(myDataDirs.get(0)); } - CoverageDatabase database = CoverageDatabase.loadSessions(myDataDir); + CoverageDatabase database = new CoverageDatabase(); + for (Path dataDir : myDataDirs) + { + database.loadSessions(dataDir); + } CoverageReportWriter renderer = new CoverageReportWriter(config, database); diff --git a/runtime/src/main/java/dev/ionfusion/runtime/_private/cover/CoverageDatabase.java b/runtime/src/main/java/dev/ionfusion/runtime/_private/cover/CoverageDatabase.java index a42d64e3..8da18e7c 100644 --- a/runtime/src/main/java/dev/ionfusion/runtime/_private/cover/CoverageDatabase.java +++ b/runtime/src/main/java/dev/ionfusion/runtime/_private/cover/CoverageDatabase.java @@ -84,16 +84,14 @@ public int compare(SourceLocation a, SourceLocation b) private final Map myLocations = new HashMap<>(); - CoverageDatabase() + public CoverageDatabase() { } - public static CoverageDatabase loadSessions(Path dataDir) + public void loadSessions(Path dataDir) throws IOException { - CoverageDatabase db = new CoverageDatabase(); - Path sessionsDir = dataDir.resolve("sessions"); if (Files.exists(sessionsDir)) { @@ -103,12 +101,11 @@ public static CoverageDatabase loadSessions(Path dataDir) { if (isRegularFile(p)) { - db.loadSession(p); + loadSession(p); } } } } - return db; } diff --git a/runtime/src/test/java/dev/ionfusion/fusion/cli/CoverTest.java b/runtime/src/test/java/dev/ionfusion/fusion/cli/CoverTest.java index da8dabe1..0b3f74df 100644 --- a/runtime/src/test/java/dev/ionfusion/fusion/cli/CoverTest.java +++ b/runtime/src/test/java/dev/ionfusion/fusion/cli/CoverTest.java @@ -53,6 +53,24 @@ public void testNoArgs() } + @Test + public void testNoDataDirArg() + throws Exception + { + run(1, "report_coverage", "--htmlDir", reportDir().getPath()); + assertThat(stderrText, containsString("Usage:")); + } + + + @Test + public void testDataDirArgIsEmpty() + throws Exception + { + run(1, "report_coverage", "--htmlDir", reportDir().getPath(), ""); + assertThat(stderrText, containsString("Usage:")); + } + + @Test public void testDataDirIsMissing() throws Exception @@ -60,7 +78,7 @@ public void testDataDirIsMissing() File f = new File(myFolder, "no file"); assertFalse(f.exists()); - run(1, "report_coverage", f.getPath(), reportDir().getPath()); + run(1, "report_coverage", "--htmlDir", reportDir().getPath(), f.getPath()); assertThat(stderrText, containsString("not a readable directory")); assertThat(stderrText, containsString(f.getPath())); @@ -72,7 +90,7 @@ public void testDataDirIsFile() { String f = plainFile().getPath(); - run(1, "report_coverage", f, reportDir().getPath()); + run(1, "report_coverage", "--htmlDir", reportDir().getPath(), f); assertThat(stderrText, containsString("not a readable directory")); assertThat(stderrText, containsString(f)); @@ -84,7 +102,7 @@ public void testReportDirIsFile() { String f = plainFile().getPath(); - run(1, "report_coverage", dataDir().getPath(), f); + run(1, "report_coverage", "--htmlDir", f, dataDir().getPath()); assertThat(stderrText, containsString("not a directory")); assertThat(stderrText, containsString(f)); @@ -99,7 +117,7 @@ public void testCoverCompletionMessage() String reportDir = reportDir().getPath(); // I'm surprised this works without any coverage data! - run(0, "report_coverage", dataDir, reportDir); + run(0, "report_coverage", "--htmlDir", reportDir, dataDir); assertThat(stdoutText, allOf(containsString("Wrote Fusion coverage report to "), containsString(reportDir))); @@ -108,6 +126,33 @@ public void testCoverCompletionMessage() assertTrue(new File(reportDir, "index.html").isFile()); } + @Test + public void testMultipleDataDirs() + throws Exception + { + File dataDir1 = dataDir(); + File dataDir2 = newFolder(myFolder, "dataDir2"); + run(0, "report_coverage", + "--htmlDir", reportDir().getPath(), + "--configFile", plainFile().getPath(), + dataDir1.getPath(), + dataDir2.getPath()); + } + + @Test + public void testMultipleDataDirsNoConfig() + throws Exception + { + File dataDir1 = dataDir(); + File dataDir2 = newFolder(myFolder, "dataDir2"); + run(1, "report_coverage", + "--htmlDir", reportDir().getPath(), + dataDir1.getPath(), + dataDir2.getPath()); + assertThat(stderrText, containsString("Must provide --configFile")); + } + + private static File newFolder(File root, String... subDirs) throws IOException { String subFolder = String.join("/", subDirs); File result = new File(root, subFolder);