Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ static List<String> detect(Path root) throws IOException {
return List.copyOf(detected);
}

private static List<Path> findBuildFiles(Path root) throws IOException {
static List<Path> findBuildFiles(Path root) throws IOException {
List<Path> buildFiles = new ArrayList<>();
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
Expand Down
62 changes: 58 additions & 4 deletions src/main/java/com/seleniumboot/migrator/Migrator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> EXCLUDED_DIRECTORIES = Set.of(".git", "target", "node_modules");
private static final Set<String> 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<String> applied, List<String> notes, Report remaining) { }

Expand Down Expand Up @@ -69,10 +78,27 @@ public Result migrate(Path project, Path output) throws IOException {
List<String> notes = new ArrayList<>();
Set<String> removedTypes = transformJava(destination, applied);
List<Finding> danglingReferences = findDanglingReferences(destination, removedTypes);
if (!migratePom(destination.resolve("pom.xml"))) {
List<Path> 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);
Report remaining = includeSourceManualFindings(sourceAnalysis, outputAnalysis, danglingReferences);
Expand All @@ -88,7 +114,7 @@ private static Report includeSourceManualFindings(Report source, Report output,
Set<String> 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());
source.detectedTechnologies(), source.recognizedTechnologies(), source.locatorCounts());
}

private static void copyProject(Path source, Path destination) throws IOException {
Expand Down Expand Up @@ -333,4 +359,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);
}
}
}
10 changes: 10 additions & 0 deletions src/test/java/com/seleniumboot/migrator/CliTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"), """
Expand Down
155 changes: 155 additions & 0 deletions src/test/java/com/seleniumboot/migrator/MigratorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,161 @@ 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<WebDriver> DRIVER = new ThreadLocal<>();
}
""");
write(project.resolve(".gradle/caches/cache.bin"), "gradle cache");
write(project.resolve("build/libs/app.jar"), "build jar");
Map<Path, byte[]> 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: 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)"));
}

@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<Path, byte[]> 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: 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)"));
}

@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);
Expand Down
Loading