From 09e8b240a4f376e27848697b0138ed120c00f62f Mon Sep 17 00:00:00 2001 From: Pushpak-Jaiswal Date: Thu, 1 Oct 2026 15:56:20 +0530 Subject: [PATCH 1/3] feat: migrate supporting features --- README.md | 5 +- .../migrator/BuildFileAnalyzer.java | 2 +- .../com/seleniumboot/migrator/Migrator.java | 63 +++++++- .../com/seleniumboot/migrator/CliTest.java | 10 ++ .../seleniumboot/migrator/MigratorTest.java | 153 ++++++++++++++++++ 5 files changed, 226 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ebcfd78..3b634cb 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,13 @@ java -jar target/selenium-boot-migrator.jar migrate ./my-selenium-project --out The output directory must not already exist and cannot be the source directory or one of its children. Migration output lists applied changes, compatibility notes, and any findings still -requiring manual review. The POM rewrite uses the published Selenium Boot `3.5.0` release. +requiring manual review. The build descriptor rewrite (`pom.xml`, `build.gradle`, `build.gradle.kts`) +uses the published Selenium Boot `3.5.0` release. `analyze` reports counts per rule, what maps cleanly vs. needs review, and an *estimated* confidence. The estimate is a guide, not a guarantee. The report also lists dependencies found in Maven `pom.xml` files and Gradle `build.gradle` / `build.gradle.kts` files. Gradle files are inspected -as text; a Gradle installation is not required. +and migrated as text; a Gradle installation is not required. ### CLI Options diff --git a/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java b/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java index 4cbf125..aeaf12d 100644 --- a/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java +++ b/src/main/java/com/seleniumboot/migrator/BuildFileAnalyzer.java @@ -54,7 +54,7 @@ static List detect(Path root) throws IOException { return List.copyOf(detected); } - private static List findBuildFiles(Path root) throws IOException { + static List findBuildFiles(Path root) throws IOException { List buildFiles = new ArrayList<>(); Files.walkFileTree(root, new SimpleFileVisitor<>() { @Override diff --git a/src/main/java/com/seleniumboot/migrator/Migrator.java b/src/main/java/com/seleniumboot/migrator/Migrator.java index b783efe..48202ee 100644 --- a/src/main/java/com/seleniumboot/migrator/Migrator.java +++ b/src/main/java/com/seleniumboot/migrator/Migrator.java @@ -34,13 +34,22 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Stream; /** Copies a project and applies only transformations that can be performed mechanically. */ public final class Migrator { private static final String SELENIUM_BOOT_VERSION = "3.5.0"; - private static final Set EXCLUDED_DIRECTORIES = Set.of(".git", "target", "node_modules"); + private static final Set EXCLUDED_DIRECTORIES = Set.of(".git", "target", "build", ".gradle", "node_modules"); + private static final Pattern GRADLE_STRING_DEPENDENCY = Pattern.compile( + "(?m)^(?!(?:\\s*//|\\s*/\\*|\\s*\\*))(.*?)(?:(\"\"\"|['\"]))\\s*org\\.seleniumhq\\.selenium:selenium-java(?::[^'\"\\r\\n]+)?\\s*\\2"); + private static final Pattern GRADLE_MAP_GROUP_FIRST = Pattern.compile( + "(?m)^(?!(?:\\s*//|\\s*/\\*|\\s*\\*))(.*?)\\bgroup\\s*(:|=>|=)\\s*(['\"])org\\.seleniumhq\\.selenium\\3\\s*,\\s*name\\s*\\2\\s*(['\"])selenium-java\\4(?:\\s*,\\s*version\\s*\\2\\s*['\"][^'\"]*['\"])?"); + private static final Pattern GRADLE_MAP_NAME_FIRST = Pattern.compile( + "(?m)^(?!(?:\\s*//|\\s*/\\*|\\s*\\*))(.*?)\\bname\\s*(:|=>|=)\\s*(['\"])selenium-java\\3\\s*,\\s*group\\s*\\2\\s*(['\"])org\\.seleniumhq\\.selenium\\4(?:\\s*,\\s*version\\s*\\2\\s*['\"][^'\"]*['\"])?"); + private static final Pattern GRADLE_MULTI_ARG = Pattern.compile( + "(?m)^(?!(?:\\s*//|\\s*/\\*|\\s*\\*))(.*?)(['\"])org\\.seleniumhq\\.selenium\\2\\s*,\\s*(['\"])selenium-java\\3(?:\\s*,\\s*['\"][^'\"]*['\"])?"); public record Result(Path output, List applied, List notes, Report remaining) { } @@ -69,10 +78,27 @@ public Result migrate(Path project, Path output) throws IOException { List notes = new ArrayList<>(); Set removedTypes = transformJava(destination, applied); List danglingReferences = findDanglingReferences(destination, removedTypes); - if (!migratePom(destination.resolve("pom.xml"))) { + List buildFiles = BuildFileAnalyzer.findBuildFiles(destination); + if (buildFiles.isEmpty()) { notes.add("pom.xml: no org.seleniumhq.selenium:selenium-java dependency found to replace."); } else { - applied.add("pom.xml: replaced selenium-java with io.github.seleniumboot:selenium-boot:" + SELENIUM_BOOT_VERSION); + for (Path buildFile : buildFiles) { + String relPath = destination.relativize(buildFile).toString().replace('\\', '/'); + String name = buildFile.getFileName().toString(); + if (name.equals("pom.xml")) { + if (!migratePom(buildFile)) { + notes.add(relPath + ": no org.seleniumhq.selenium:selenium-java dependency found to replace."); + } else { + applied.add(relPath + ": replaced selenium-java with io.github.seleniumboot:selenium-boot:" + SELENIUM_BOOT_VERSION); + } + } else if (name.equals("build.gradle") || name.equals("build.gradle.kts")) { + if (!migrateGradle(buildFile)) { + notes.add(relPath + ": no org.seleniumhq.selenium:selenium-java dependency found to replace."); + } else { + applied.add(relPath + ": 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), @@ -86,7 +112,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), + output.detectedTechnologies(), output.recognizedTechnologies(), output.locatorCounts()); } private static void copyProject(Path source, Path destination) throws IOException { @@ -331,4 +358,32 @@ private static void setChildText(Element parent, String name, String value) { child.setTextContent(value); parent.appendChild(child); } + + private static boolean migrateGradle(Path buildFile) throws IOException { + if (!Files.isRegularFile(buildFile)) return false; + try { + String original = Files.readString(buildFile); + String updated = GRADLE_STRING_DEPENDENCY.matcher(original).replaceAll( + mr -> mr.group(1) + mr.group(2) + "io.github.seleniumboot:selenium-boot:" + SELENIUM_BOOT_VERSION + mr.group(2)); + updated = GRADLE_MAP_GROUP_FIRST.matcher(updated).replaceAll( + mr -> mr.group(1) + "group" + mr.group(2) + " " + mr.group(3) + "io.github.seleniumboot" + mr.group(3) + + ", name" + mr.group(2) + " " + mr.group(4) + "selenium-boot" + mr.group(4) + + ", version" + mr.group(2) + " " + mr.group(3) + SELENIUM_BOOT_VERSION + mr.group(3)); + updated = GRADLE_MAP_NAME_FIRST.matcher(updated).replaceAll( + mr -> mr.group(1) + "name" + mr.group(2) + " " + mr.group(3) + "selenium-boot" + mr.group(3) + + ", group" + mr.group(2) + " " + mr.group(4) + "io.github.seleniumboot" + mr.group(4) + + ", version" + mr.group(2) + " " + mr.group(4) + SELENIUM_BOOT_VERSION + mr.group(4)); + updated = GRADLE_MULTI_ARG.matcher(updated).replaceAll( + mr -> mr.group(1) + mr.group(2) + "io.github.seleniumboot" + mr.group(2) + + ", " + mr.group(3) + "selenium-boot" + mr.group(3) + + ", " + mr.group(2) + SELENIUM_BOOT_VERSION + mr.group(2)); + + if (original.equals(updated)) return false; + Files.writeString(buildFile, updated); + return true; + } catch (Exception exception) { + if (exception instanceof IOException ioException) throw ioException; + throw new IOException("could not update " + buildFile + ": " + exception.getMessage(), exception); + } + } } \ No newline at end of file diff --git a/src/test/java/com/seleniumboot/migrator/CliTest.java b/src/test/java/com/seleniumboot/migrator/CliTest.java index d84f12d..94e4f06 100644 --- a/src/test/java/com/seleniumboot/migrator/CliTest.java +++ b/src/test/java/com/seleniumboot/migrator/CliTest.java @@ -210,6 +210,16 @@ void returnsExitCodeThreeOnRuntimeError(@TempDir Path temp) throws Exception { assertTrue(result.err().contains("migration failed:")); } + @Test + void migratesGradleProjectViaCli(@TempDir Path temp) throws Exception { + Path output = temp.resolve("migrated-gradle"); + RunResult result = runCli("migrate", fixture("gradle-groovy").toString(), "--out", output.toString()); + assertEquals(0, result.exitCode()); + assertTrue(result.out().contains("Migrated copy:")); + assertTrue(result.out().contains("[applied] build.gradle: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(Files.readString(output.resolve("build.gradle")).contains("io.github.seleniumboot:selenium-boot:3.5.0")); + } + @Test void processExitCodes(@TempDir Path temp) throws Exception { Files.writeString(temp.resolve("Wait.java"), """ diff --git a/src/test/java/com/seleniumboot/migrator/MigratorTest.java b/src/test/java/com/seleniumboot/migrator/MigratorTest.java index 4ce1129..a83103e 100644 --- a/src/test/java/com/seleniumboot/migrator/MigratorTest.java +++ b/src/test/java/com/seleniumboot/migrator/MigratorTest.java @@ -169,6 +169,159 @@ void refusesToWriteIntoSourceOrOverwriteExistingOutput() throws Exception { assertEquals("unchanged", Files.readString(project.resolve("source.txt"))); } + @Test + void migratesGradleGroovyProjectWithoutChangingOriginal() throws Exception { + Path project = temp.resolve("gradle-groovy-project"); + Path output = temp.resolve("gradle-groovy-migrated"); + write(project.resolve("build.gradle"), """ + plugins { + id 'java' + } + + dependencies { + implementation 'org.seleniumhq.selenium:selenium-java:4.21.0' + testImplementation 'org.testng:testng:7.10.2' + } + """); + write(project.resolve("src/main/java/fixture/DriverFactory.java"), """ + package fixture; + import org.openqa.selenium.WebDriver; + public class DriverFactory { + private static final ThreadLocal DRIVER = new ThreadLocal<>(); + } + """); + write(project.resolve(".gradle/caches/cache.bin"), "gradle cache"); + write(project.resolve("build/libs/app.jar"), "build jar"); + Map original = snapshot(project); + + Migrator.Result result = new Migrator().migrate(project, output); + + assertEquals(original.keySet(), snapshot(project).keySet()); + original.forEach((path, bytes) -> assertArrayEquals(bytes, read(project.resolve(path)))); + assertFalse(Files.exists(output.resolve(".gradle"))); + assertFalse(Files.exists(output.resolve("build"))); + assertFalse(Files.exists(output.resolve("src/main/java/fixture/DriverFactory.java"))); + + String gradle = Files.readString(output.resolve("build.gradle")); + assertTrue(gradle.contains("implementation 'io.github.seleniumboot:selenium-boot:3.5.0'")); + assertTrue(gradle.contains("testImplementation 'org.testng:testng:7.10.2'")); + assertFalse(gradle.contains("org.seleniumhq.selenium:selenium-java")); + + assertTrue(result.applied().contains("build.gradle: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(result.remaining().detectedTechnologies().contains("Dependency: io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(result.remaining().detectedTechnologies().contains("Dependency: org.testng:testng:7.10.2")); + assertTrue(result.remaining().detectedTechnologies().contains("Build system: Gradle (Groovy DSL)")); + } + + @Test + void migratesGradleKotlinProjectWithoutChangingOriginal() throws Exception { + Path project = temp.resolve("gradle-kotlin-project"); + Path output = temp.resolve("gradle-kotlin-migrated"); + write(project.resolve("build.gradle.kts"), """ + plugins { + java + } + + dependencies { + implementation("org.seleniumhq.selenium:selenium-java:4.21.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + } + """); + Map original = snapshot(project); + + Migrator.Result result = new Migrator().migrate(project, output); + + assertEquals(original.keySet(), snapshot(project).keySet()); + original.forEach((path, bytes) -> assertArrayEquals(bytes, read(project.resolve(path)))); + + String gradleKts = Files.readString(output.resolve("build.gradle.kts")); + assertTrue(gradleKts.contains("implementation(\"io.github.seleniumboot:selenium-boot:3.5.0\")")); + assertTrue(gradleKts.contains("testImplementation(\"org.junit.jupiter:junit-jupiter:5.10.2\")")); + assertFalse(gradleKts.contains("org.seleniumhq.selenium:selenium-java")); + + assertTrue(result.applied().contains("build.gradle.kts: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(result.remaining().detectedTechnologies().contains("Dependency: io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(result.remaining().detectedTechnologies().contains("Dependency: org.junit.jupiter:junit-jupiter:5.10.2")); + assertTrue(result.remaining().detectedTechnologies().contains("Build system: Gradle (Kotlin DSL)")); + } + + @Test + void migratesFromGradleFixtures() throws Exception { + Path groovyFixture = Path.of(MigratorTest.class.getResource("/gradle-groovy").toURI()); + Path groovyOutput = temp.resolve("fixture-groovy-migrated"); + Migrator.Result groovyResult = new Migrator().migrate(groovyFixture, groovyOutput); + assertTrue(groovyResult.applied().contains("build.gradle: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(Files.readString(groovyOutput.resolve("build.gradle")).contains("io.github.seleniumboot:selenium-boot:3.5.0")); + + Path kotlinFixture = Path.of(MigratorTest.class.getResource("/gradle-kotlin").toURI()); + Path kotlinOutput = temp.resolve("fixture-kotlin-migrated"); + Migrator.Result kotlinResult = new Migrator().migrate(kotlinFixture, kotlinOutput); + assertTrue(kotlinResult.applied().contains("build.gradle.kts: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(Files.readString(kotlinOutput.resolve("build.gradle.kts")).contains("io.github.seleniumboot:selenium-boot:3.5.0")); + } + + @Test + void reportsNoteWhenGradleBuildHasNoSeleniumJava() throws Exception { + Path project = temp.resolve("gradle-no-selenium"); + Path output = temp.resolve("gradle-no-selenium-migrated"); + write(project.resolve("build.gradle"), """ + plugins { + id 'java' + } + dependencies { + testImplementation 'org.testng:testng:7.10.2' + } + """); + Migrator.Result result = new Migrator().migrate(project, output); + assertTrue(result.notes().contains("build.gradle: no org.seleniumhq.selenium:selenium-java dependency found to replace.")); + assertFalse(result.applied().stream().anyMatch(applied -> applied.contains("build.gradle"))); + } + + @Test + void supportsVariousGradleSyntaxStyles() throws Exception { + Path project = temp.resolve("gradle-syntax"); + Path output = temp.resolve("gradle-syntax-migrated"); + write(project.resolve("build.gradle"), """ + dependencies { + // Comments should not be touched: + // implementation 'org.seleniumhq.selenium:selenium-java:4.21.0' + /* testImplementation 'org.seleniumhq.selenium:selenium-java:4.21.0' */ + * implementation 'org.seleniumhq.selenium:selenium-java:4.21.0' + + // Double quotes with and without version: + implementation "org.seleniumhq.selenium:selenium-java:4.21.0" + api "org.seleniumhq.selenium:selenium-java" + + // Map notation: + testImplementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '4.21.0' + compileOnly name: 'selenium-java', group: 'org.seleniumhq.selenium' + + // Multi-arg notation: + runtimeOnly 'org.seleniumhq.selenium', 'selenium-java', '4.21.0' + + // Parentheses with exclude block: + implementation('org.seleniumhq.selenium:selenium-java:4.21.0') { + exclude group: 'org.hamcrest' + } + } + """); + Migrator.Result result = new Migrator().migrate(project, output); + assertTrue(result.applied().contains("build.gradle: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); + + String migrated = Files.readString(output.resolve("build.gradle")); + assertTrue(migrated.contains("// implementation 'org.seleniumhq.selenium:selenium-java:4.21.0'")); + assertTrue(migrated.contains("/* testImplementation 'org.seleniumhq.selenium:selenium-java:4.21.0' */")); + assertTrue(migrated.contains("* implementation 'org.seleniumhq.selenium:selenium-java:4.21.0'")); + + assertTrue(migrated.contains("implementation \"io.github.seleniumboot:selenium-boot:3.5.0\"")); + assertTrue(migrated.contains("api \"io.github.seleniumboot:selenium-boot:3.5.0\"")); + assertTrue(migrated.contains("testImplementation group: 'io.github.seleniumboot', name: 'selenium-boot', version: '3.5.0'")); + assertTrue(migrated.contains("compileOnly name: 'selenium-boot', group: 'io.github.seleniumboot', version: '3.5.0'")); + assertTrue(migrated.contains("runtimeOnly 'io.github.seleniumboot', 'selenium-boot', '3.5.0'")); + assertTrue(migrated.contains("implementation('io.github.seleniumboot:selenium-boot:3.5.0') {")); + assertTrue(migrated.contains("exclude group: 'org.hamcrest'")); + } + private static void write(Path path, String content) throws Exception { Files.createDirectories(path.getParent()); Files.writeString(path, content); From e3462ae6254015d147532a7bdd759e3060efb97a Mon Sep 17 00:00:00 2001 From: Pushpak-Jaiswal Date: Fri, 2 Oct 2026 15:49:48 +0530 Subject: [PATCH 2/3] fixed failing test issue --- src/main/java/com/seleniumboot/migrator/Migrator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/seleniumboot/migrator/Migrator.java b/src/main/java/com/seleniumboot/migrator/Migrator.java index 73077dd..83844ab 100644 --- a/src/main/java/com/seleniumboot/migrator/Migrator.java +++ b/src/main/java/com/seleniumboot/migrator/Migrator.java @@ -114,7 +114,7 @@ private static Report includeSourceManualFindings(Report source, Report output, Set unparsable = new LinkedHashSet<>(source.unparsable()); unparsable.addAll(output.unparsable()); return new Report(output.filesFound(), output.filesParsed(), List.copyOf(unparsable), List.copyOf(findings), - source.detectedTechnologies(), source.recognizedTechnologies(), source.locatorCounts()); + output.detectedTechnologies(), output.recognizedTechnologies(), output.locatorCounts()); } private static void copyProject(Path source, Path destination) throws IOException { From 2abded46ebb7019613a65cd9749d3afef1713d88 Mon Sep 17 00:00:00 2001 From: Pushpak-Jaiswal Date: Fri, 2 Oct 2026 19:40:19 +0530 Subject: [PATCH 3/3] fixes --- src/main/java/com/seleniumboot/migrator/Migrator.java | 2 +- src/test/java/com/seleniumboot/migrator/MigratorTest.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/seleniumboot/migrator/Migrator.java b/src/main/java/com/seleniumboot/migrator/Migrator.java index 83844ab..53689d1 100644 --- a/src/main/java/com/seleniumboot/migrator/Migrator.java +++ b/src/main/java/com/seleniumboot/migrator/Migrator.java @@ -114,7 +114,7 @@ private static Report includeSourceManualFindings(Report source, Report output, Set unparsable = new LinkedHashSet<>(source.unparsable()); unparsable.addAll(output.unparsable()); return new Report(output.filesFound(), output.filesParsed(), List.copyOf(unparsable), List.copyOf(findings), - output.detectedTechnologies(), output.recognizedTechnologies(), output.locatorCounts()); + 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/MigratorTest.java b/src/test/java/com/seleniumboot/migrator/MigratorTest.java index da8fdf1..00d1ab4 100644 --- a/src/test/java/com/seleniumboot/migrator/MigratorTest.java +++ b/src/test/java/com/seleniumboot/migrator/MigratorTest.java @@ -222,7 +222,8 @@ public class DriverFactory { assertFalse(gradle.contains("org.seleniumhq.selenium:selenium-java")); assertTrue(result.applied().contains("build.gradle: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); - assertTrue(result.remaining().detectedTechnologies().contains("Dependency: io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(result.remaining().detectedTechnologies().contains("Dependency: org.seleniumhq.selenium:selenium-java:4.21.0")); + assertFalse(result.remaining().detectedTechnologies().contains("Dependency: io.github.seleniumboot:selenium-boot:3.5.0")); assertTrue(result.remaining().detectedTechnologies().contains("Dependency: org.testng:testng:7.10.2")); assertTrue(result.remaining().detectedTechnologies().contains("Build system: Gradle (Groovy DSL)")); } @@ -254,7 +255,8 @@ void migratesGradleKotlinProjectWithoutChangingOriginal() throws Exception { assertFalse(gradleKts.contains("org.seleniumhq.selenium:selenium-java")); assertTrue(result.applied().contains("build.gradle.kts: replaced selenium-java with io.github.seleniumboot:selenium-boot:3.5.0")); - assertTrue(result.remaining().detectedTechnologies().contains("Dependency: io.github.seleniumboot:selenium-boot:3.5.0")); + assertTrue(result.remaining().detectedTechnologies().contains("Dependency: org.seleniumhq.selenium:selenium-java:4.21.0")); + assertFalse(result.remaining().detectedTechnologies().contains("Dependency: io.github.seleniumboot:selenium-boot:3.5.0")); assertTrue(result.remaining().detectedTechnologies().contains("Dependency: org.junit.jupiter:junit-jupiter:5.10.2")); assertTrue(result.remaining().detectedTechnologies().contains("Build system: Gradle (Kotlin DSL)")); }