diff --git a/src/main/java/com/seleniumboot/migrator/MigrationReport.java b/src/main/java/com/seleniumboot/migrator/MigrationReport.java new file mode 100644 index 0000000..d0b88ec --- /dev/null +++ b/src/main/java/com/seleniumboot/migrator/MigrationReport.java @@ -0,0 +1,117 @@ +package com.seleniumboot.migrator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.TreeMap; + +final class MigrationReport { + + private MigrationReport() { + } + + static Path write(Path directory, Report report) throws IOException { + Path reportPath = directory.resolve("MIGRATION_REPORT.md"); + Files.writeString(reportPath, render(report)); + return reportPath; + } + + static String render(Report report) { + StringBuilder output = new StringBuilder("# Selenium Boot Migration Report\n\n"); + + output.append("## Summary\n\n"); + output.append("- Files found: ").append(report.filesFound()).append('\n'); + output.append("- Files parsed: ").append(report.filesParsed()).append('\n'); + output.append("- Automatic findings: ") + .append(report.count(Finding.Status.AUTO)).append('\n'); + output.append("- Manual findings: ") + .append(report.count(Finding.Status.MANUAL)).append('\n'); + output.append("- Unparsable files: ") + .append(report.unparsable().size()).append("\n\n"); + + output.append("## Detected technologies\n\n"); + if (report.detectedTechnologies().isEmpty()) { + output.append("- No supported build descriptor found\n"); + } else { + report.detectedTechnologies() + .forEach(technology -> output.append("- ").append(technology).append('\n')); + } + + output.append("\n## Recognized technologies\n\n"); + if (report.recognizedTechnologies().isEmpty()) { + output.append("- No supported test technologies detected\n"); + } else { + report.recognizedTechnologies() + .forEach(technology -> output.append("- ").append(technology).append('\n')); + } + + output.append("\n## Rules applied per file\n\n"); + Map findingsByFile = new TreeMap<>(); + report.findings().forEach(finding -> findingsByFile + .computeIfAbsent(finding.file(), file -> new StringBuilder()) + .append("- ") + .append(finding.ruleId()) + .append(" at line ") + .append(finding.line()) + .append(": ") + .append(finding.detected()) + .append('\n')); + + if (findingsByFile.isEmpty()) { + output.append("- No migration rules detected\n\n"); + } else { + findingsByFile.forEach((file, findings) -> output + .append("### `") + .append(file) + .append("`\n\n") + .append(findings) + .append('\n')); + } + + output.append("## Warnings\n\n"); + var manualFindings = report.findings().stream() + .filter(finding -> finding.status() == Finding.Status.MANUAL) + .toList(); + + if (manualFindings.isEmpty() && report.unparsable().isEmpty()) { + output.append("- None\n"); + } else { + manualFindings.forEach(finding -> output + .append("- `") + .append(finding.file()) + .append(':') + .append(finding.line()) + .append("`: Detected: ") + .append(finding.detected()) + .append(". Why not automatic: manual review is required. Suggested approach: ") + .append(finding.advice()) + .append('\n')); + + report.unparsable().forEach(file -> output + .append("- `") + .append(file) + .append("` could not be parsed\n")); + } + + output.append("\n## Manual actions\n\n"); + if (manualFindings.isEmpty()) { + output.append("- None\n"); + } else { + manualFindings.forEach(finding -> output + .append("- `") + .append(finding.file()) + .append(':') + .append(finding.line()) + .append("`: ") + .append(finding.advice()) + .append('\n')); + } + + output.append("\n## Estimated migration confidence\n\n"); + output.append(report.estimatedConfidence()) + .append("% estimated migration confidence. This is an estimate, not a guarantee.\n"); + + return output.toString(); + } +} diff --git a/src/main/java/com/seleniumboot/migrator/Migrator.java b/src/main/java/com/seleniumboot/migrator/Migrator.java index b783efe..0bca348 100644 --- a/src/main/java/com/seleniumboot/migrator/Migrator.java +++ b/src/main/java/com/seleniumboot/migrator/Migrator.java @@ -75,8 +75,9 @@ public Result migrate(Path project, Path output) throws IOException { applied.add("pom.xml: replaced selenium-java with io.github.seleniumboot:selenium-boot:" + SELENIUM_BOOT_VERSION); } Report outputAnalysis = new Analyzer().analyze(destination); - return new Result(destination, List.copyOf(applied), List.copyOf(notes), - includeSourceManualFindings(sourceAnalysis, outputAnalysis, danglingReferences)); + Report remaining = includeSourceManualFindings(sourceAnalysis, outputAnalysis, danglingReferences); + MigrationReport.write(destination, remaining); + return new Result(destination, List.copyOf(applied), List.copyOf(notes), remaining); } private static Report includeSourceManualFindings(Report source, Report output, List additionalFindings) { @@ -86,7 +87,8 @@ private static Report includeSourceManualFindings(Report source, Report output, additionalFindings.stream().filter(finding -> !findings.contains(finding)).forEach(findings::add); Set unparsable = new LinkedHashSet<>(source.unparsable()); unparsable.addAll(output.unparsable()); - return new Report(output.filesFound(), output.filesParsed(), List.copyOf(unparsable), List.copyOf(findings)); + return new Report(output.filesFound(), output.filesParsed(), List.copyOf(unparsable), List.copyOf(findings), + source.detectedTechnologies(), source.recognizedTechnologies(), source.locatorCounts()); } private static void copyProject(Path source, Path destination) throws IOException { diff --git a/src/test/java/com/seleniumboot/migrator/MigrationReportTest.java b/src/test/java/com/seleniumboot/migrator/MigrationReportTest.java new file mode 100644 index 0000000..f2c0c14 --- /dev/null +++ b/src/test/java/com/seleniumboot/migrator/MigrationReportTest.java @@ -0,0 +1,109 @@ +package com.seleniumboot.migrator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MigrationReportTest { + + @Test + void matchesSnapshot() throws Exception { + var report = fixtureReport(); + + String expected = Files.readString( + Path.of("src/test/resources/migration-report.md") + ).replace("\r\n", "\n"); + + assertEquals(expected, MigrationReport.render(report)); + } + + @Test + void rendersReportWithZeroFindings() { + var report = new Report(0, 0, List.of(), List.of()); + + String expected = """ + # Selenium Boot Migration Report + + ## Summary + + - Files found: 0 + - Files parsed: 0 + - Automatic findings: 0 + - Manual findings: 0 + - Unparsable files: 0 + + ## Detected technologies + + - No supported build descriptor found + + ## Recognized technologies + + - No supported test technologies detected + + ## Rules applied per file + + - No migration rules detected + + ## Warnings + + - None + + ## Manual actions + + - None + + ## Estimated migration confidence + + 100% estimated migration confidence. This is an estimate, not a guarantee. + """; + + assertEquals(expected, MigrationReport.render(report)); + } + + @Test + void writesMigrationReport(@TempDir Path directory) throws Exception { + var report = fixtureReport(); + + Path reportPath = MigrationReport.write(directory, report); + + assertEquals(directory.resolve("MIGRATION_REPORT.md"), reportPath); + assertTrue(Files.exists(reportPath)); + assertEquals(MigrationReport.render(report), Files.readString(reportPath)); + } + + private static Report fixtureReport() { + return new Report( + 3, + 2, + List.of("Broken.java"), + List.of( + new Finding( + "MIG-002", + Finding.Status.AUTO, + "Auto.java", + 4, + "WebDriverManager", + "Delete; Selenium Manager fetches drivers automatically." + ), + new Finding( + "MIG-003", + Finding.Status.MANUAL, + "Page.java", + 8, + "WebDriverWait / ExpectedConditions", + "Use $(locator) auto-wait or getWait(); review the condition by hand." + ) + ), + List.of("Build system: Maven"), + List.of("Selenium", "JUnit 5"), + Map.of("By.id", 2L) + ); + } +} diff --git a/src/test/java/com/seleniumboot/migrator/MigratorTest.java b/src/test/java/com/seleniumboot/migrator/MigratorTest.java index 4ce1129..824e006 100644 --- a/src/test/java/com/seleniumboot/migrator/MigratorTest.java +++ b/src/test/java/com/seleniumboot/migrator/MigratorTest.java @@ -80,6 +80,12 @@ class Utility { int value; } Migrator.Result result = new Migrator().migrate(project, output); + assertTrue(Files.isRegularFile(output.resolve("MIGRATION_REPORT.md"))); + String migrationReport = Files.readString(output.resolve("MIGRATION_REPORT.md")); + assertEquals(MigrationReport.render(result.remaining()), migrationReport); + assertTrue(migrationReport.contains("# Selenium Boot Migration Report")); + assertTrue(migrationReport.contains("## Rules applied per file")); + assertTrue(migrationReport.contains("Estimated migration confidence")); assertEquals(original.keySet(), snapshot(project).keySet()); original.forEach((path, bytes) -> assertArrayEquals(bytes, read(project.resolve(path)))); assertFalse(Files.exists(output.resolve("src/main/java/fixture/DriverFactory.java"))); @@ -152,7 +158,15 @@ class Caller { && finding.file().endsWith("Caller.java")) .count()); assertTrue(Files.readString(output.resolve("pom.xml")).contains("3.5.0")); - } + + assertTrue(Files.isRegularFile(output.resolve("MIGRATION_REPORT.md"))); + String migrationReport = Files.readString(output.resolve("MIGRATION_REPORT.md")); + assertEquals(MigrationReport.render(result.remaining()), migrationReport); + assertTrue(migrationReport.contains("MIG-017")); + assertTrue(migrationReport.contains("Caller.java")); + assertFalse(migrationReport.contains("MIG-004")); + assertFalse(migrationReport.contains("MIG-001")); + } @Test void refusesToWriteIntoSourceOrOverwriteExistingOutput() throws Exception { diff --git a/src/test/resources/migration-report.md b/src/test/resources/migration-report.md new file mode 100644 index 0000000..e4f902c --- /dev/null +++ b/src/test/resources/migration-report.md @@ -0,0 +1,41 @@ +# Selenium Boot Migration Report + +## Summary + +- Files found: 3 +- Files parsed: 2 +- Automatic findings: 1 +- Manual findings: 1 +- Unparsable files: 1 + +## Detected technologies + +- Build system: Maven + +## Recognized technologies + +- Selenium +- JUnit 5 + +## Rules applied per file + +### `Auto.java` + +- MIG-002 at line 4: WebDriverManager + +### `Page.java` + +- MIG-003 at line 8: WebDriverWait / ExpectedConditions + +## Warnings + +- `Page.java:8`: Detected: WebDriverWait / ExpectedConditions. Why not automatic: manual review is required. Suggested approach: Use $(locator) auto-wait or getWait(); review the condition by hand. +- `Broken.java` could not be parsed + +## Manual actions + +- `Page.java:8`: Use $(locator) auto-wait or getWait(); review the condition by hand. + +## Estimated migration confidence + +50% estimated migration confidence. This is an estimate, not a guarantee.