diff --git a/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTest.java b/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTest.java index 056dc53cb2..ab4d890b5d 100644 --- a/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTest.java +++ b/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTest.java @@ -19,8 +19,10 @@ import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeGroups; import org.testng.annotations.Test; +import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -35,40 +37,46 @@ import java.util.List; import java.util.Map; import java.util.Objects; - +import java.util.Optional; import static org.ballerina.projectapi.MavenCustomRepoTestUtils.createSettingToml; import static org.ballerina.projectapi.MavenCustomRepoTestUtils.deleteArtifacts; import static org.ballerina.projectapi.MavenCustomRepoTestUtils.deleteFiles; +import static org.ballerina.projectapi.MavenCustomRepoTestUtils.editVersionBallerinaToml; import static org.ballerina.projectapi.MavenCustomRepoTestUtils.getEnvVariables; import static org.ballerina.projectapi.MavenCustomRepoTestUtils.getString; +import static org.ballerina.projectapi.MavenCustomRepoTestUtils.packTrigger; +import static org.ballerina.projectapi.MavenCustomRepoTestUtils.pushTrigger; +import static org.ballerina.projectapi.MavenCustomRepoTestUtils.pasteStaticMainBalWithAllPkgs; +import static org.ballerina.projectapi.MavenCustomRepoTestUtils.pasteStaticMainBalWithPkg1AndPkg2; +import static org.ballerina.projectapi.MavenCustomRepoTestUtils.updateVersionForPackage; import static org.ballerina.projectapi.TestUtils.DISTRIBUTION_FILE_NAME; import static org.ballerina.projectapi.TestUtils.OUTPUT_CONTAIN_ERRORS; import static org.ballerina.projectapi.TestUtils.executeBuildCommand; +import static org.ballerina.projectapi.TestUtils.executeCleanCommand; +import static org.ballerina.projectapi.TestUtils.executePackCommand; import static org.ballerina.projectapi.TestUtils.executePullCommand; import static org.ballerina.projectapi.TestUtils.executePushCommand; -import static org.ballerina.projectapi.TestUtils.executePackCommand; -/** - * Tests related to Maven repositories. - */ public class MavenCustomRepoTest { private static final String org = "bctestorg"; private static final String platform = "any"; - private static final String packagename = "pact"; - private static final String version = "0.2.0"; - private static final String GITHUB_REPO_ID = "github1"; + private static final String PACKAGE_NAME = "pkg1"; + static final String GITHUB_REPO_ID = "github1"; + private static final String VERSION = "0.1.0"; private Path actualHomeDirectory; private Path tempWorkspaceDirectory; private Path actualHomeDirectoryClone; private Map envVariables; @BeforeClass() - public void setUp() throws IOException { + public void setUp() throws IOException, InterruptedException { TestUtils.setupDistributions(); - deleteArtifacts(org, packagename); - actualHomeDirectory = Paths.get(System.getProperty("user.home")).resolve(".ballerina"); + deleteArtifacts(org, "pkg1"); + deleteArtifacts(org, "pkg2"); + deleteArtifacts(org, "pkg3"); + actualHomeDirectory = Paths.get(System.getProperty("user.home")).resolve(".ballerina"); actualHomeDirectoryClone = Files.createTempDirectory("bal-test-integration-packaging-home-") .resolve(".ballerina"); Files.walkFileTree(actualHomeDirectory, @@ -78,7 +86,7 @@ public void setUp() throws IOException { createSettingToml(actualHomeDirectory); System.setProperty("user.home", actualHomeDirectory.getParent().toString()); - envVariables = getEnvVariables(); + envVariables = TestUtils.addEnvVariables(getEnvVariables(), actualHomeDirectory); // Copy test resources to temp workspace directory try { @@ -89,24 +97,30 @@ public void setUp() throws IOException { } catch (URISyntaxException e) { Assert.fail("error loading resources"); } + + publishBalaPackagesBeforeTests(); } @Test(description = "Push package to Github packages", enabled = false) public void testPushBalaGithub() throws IOException, InterruptedException { List args = new ArrayList<>(); - Process build = executePackCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve(packagename), + Process build = executePackCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve(PACKAGE_NAME), args, this.envVariables); String buildErrors = getString(build.getErrorStream()); if (!buildErrors.isEmpty()) { Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); } String buildOutput = getString(build.getInputStream()); - Assert.assertTrue(buildOutput.contains("Creating bala\n" + "\ttarget/bala/" + org + "-" - + packagename + "-" + platform + "-" + version + ".bala")); + String normalizedOutput = buildOutput.replace("\\", "/").replace("\r\n", "\n").replace("\r", "\n"); + String collapsed = normalizedOutput.replaceAll("\\s+", " "); + String expectedBalaPath = "target/bala/" + org + "-" + PACKAGE_NAME + "-" + platform + "-" + VERSION + ".bala"; + Assert.assertTrue(collapsed.contains("Creating bala") && collapsed.contains(expectedBalaPath), + "Expected creation message with path: " + expectedBalaPath + System.lineSeparator() + + "Actual output: " + buildOutput); args = new ArrayList<>(); args.add("--repository=" + GITHUB_REPO_ID); - build = executePushCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve(packagename), + build = executePushCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve(PACKAGE_NAME), args, this.envVariables); buildErrors = getString(build.getErrorStream()); if (!buildErrors.isEmpty()) { @@ -114,16 +128,20 @@ public void testPushBalaGithub() throws IOException, InterruptedException { } buildOutput = getString(build.getInputStream()); - Assert.assertTrue(buildOutput.contains("Successfully pushed target/bala/" + org + "-" - + packagename + "-any-" + version + ".bala to " + "'" + GITHUB_REPO_ID + "' repository.")); + String normalizedPushOutput = buildOutput.replace("\\", "/").replace("\r\n", "\n").replace("\r", "\n"); + String collapsedPush = normalizedPushOutput.replaceAll("\\s+", " "); + String expectedPushMsg = "Successfully pushed target/bala/" + org + "-" + PACKAGE_NAME + "-any-" + VERSION + + ".bala to '" + GITHUB_REPO_ID + "' repository."; + Assert.assertTrue(collapsedPush.contains(expectedPushMsg), + "Expected push success message. Actual output: " + buildOutput); } @Test(description = "Pull package from Github packages", dependsOnMethods = "testPushBalaGithub", enabled = false) public void testPullBalaGithub() throws IOException, InterruptedException { List args = new ArrayList<>(); - args.add(org + "/" + packagename + ":" + version); + args.add(org + "/" + PACKAGE_NAME + ":" + VERSION); args.add("--repository=" + GITHUB_REPO_ID); - Process build = executePullCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve(packagename), + Process build = executePullCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve(PACKAGE_NAME), args, this.envVariables); String buildErrors = getString(build.getErrorStream()); if (!buildErrors.isEmpty()) { @@ -134,7 +152,7 @@ public void testPullBalaGithub() throws IOException, InterruptedException { Assert.assertTrue(buildOutput.contains("Successfully pulled the package from the custom repository")); Path packagePath = this.actualHomeDirectory.resolve("repositories") - .resolve(GITHUB_REPO_ID).resolve("bala").resolve(org).resolve(packagename).resolve(version); + .resolve(GITHUB_REPO_ID).resolve("bala").resolve(org).resolve(PACKAGE_NAME).resolve(VERSION); Assert.assertTrue(Files.exists(packagePath.resolve("any"))); deleteFiles(this.actualHomeDirectory.resolve("repositories").resolve(GITHUB_REPO_ID), false); } @@ -165,21 +183,615 @@ public void testBuildBalaGithubOnline() throws IOException, InterruptedException } String buildOutput = getString(build.getInputStream()); - Assert.assertTrue(buildOutput.contains("Generating executable\n\ttarget/bin/test.jar")); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n").replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && collapsed.contains("target/bin/test.jar"), + "Expected generating executable + target/bin/test.jar in output. Actual output: " + buildOutput); + } + + private void publishBalaPackagesBeforeTests() throws IOException, InterruptedException { + // Iteratively publish the requested versions: pkg1 -> 0.1.0, pkg2 -> 1.0.0 then pkg 3 -> 1.0.0 + String[][] seq = { + {"pkg1", "0.1.0"}, + {"pkg2", "1.0.0"}, + {"pkg3", "1.0.0"} + }; + + for (String[] entry : seq) { + String pkg = entry[0]; + String ver = entry[1]; + // packTrigger returns a Process; we don't use the process object, so don't assign it + packTrigger(pkg, this.tempWorkspaceDirectory, this.envVariables); + Process push = pushTrigger(pkg, this.tempWorkspaceDirectory, this.envVariables); + String pushOutput = getString(push.getInputStream()); + String normalizedPushOutput = pushOutput.replace("\\", "/").replace("\r\n", "\n").replace("\r", "\n"); + String collapsedPush = normalizedPushOutput.replaceAll("\\s+", " "); + Assert.assertTrue(collapsedPush.contains("Successfully pushed target/bala/" + org + "-" + pkg + "-any-" + ver + ".bala to '" + GITHUB_REPO_ID + "' repository."), + "Expected push success message. Actual output: " + pushOutput); + } + } + + @Test(description = "Build a package with multiple dependencies from Github packages") + public void testCase1_buildMyProject_assertDependenciesToml() throws IOException, InterruptedException { + List args = new ArrayList<>(); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory + .resolve("myproject1"), + args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n") + .replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + + buildOutput); + + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + Optional pkg1Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg1"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + + Assert.assertTrue(pkg1Version.isPresent(), "Expected pkg1 to be present in Dependencies.toml"); + Assert.assertEquals(pkg1Version.get(), "0.1.0", "Package version is not matching with the " + + "pushed package version"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.0.0", "Package version is not matching with the" + + "pushed package version"); + } + + @BeforeGroups("testCase2") + public void beforeGroupTestCase2() throws IOException { + Path projectDir = this.tempWorkspaceDirectory.resolve("myproject1"); + try { + Assert.assertTrue(updateVersionForPackage(projectDir, "pkg1", "0.1.0"), + "pkg1 not found in Ballerina.toml"); + Assert.assertTrue(updateVersionForPackage(projectDir, "pkg2", "1.0.0"), + "pkg2 not found in Ballerina.toml"); + } catch (IOException e) { + Assert.fail("Error updating package versions in Ballerina.toml before Test Case 2. " + e.getMessage()); + } + pasteStaticMainBalWithPkg1AndPkg2(projectDir); + } + + @Test(description = "Push more packages to Github to test locking mode results", + dependsOnMethods = "testCase1_buildMyProject_assertDependenciesToml") + public void testCase2_publishAdditionalVersionsForDeps() throws IOException, InterruptedException { + + // Publish multiple versions for pkg1 (Dep1) + String[] pkg1Versions = {"0.1.1", "0.2.0", "1.0.0"}; + for (String ver : pkg1Versions) { + // Update package version, pack and push + editVersionBallerinaToml(this.tempWorkspaceDirectory.resolve("pkg1"), ver); + // packTrigger returns a Process we don't need; call directly + packTrigger("pkg1", this.tempWorkspaceDirectory, this.envVariables); + Process p = pushTrigger("pkg1", this.tempWorkspaceDirectory, this.envVariables); + String pushOutput = getString(p.getInputStream()); + // Normalize output so Windows backslashes don't break the assertion + String normalizedPushOutput = pushOutput.replace("\\", "/") + .replace("\r\n", "\n").replace("\r", "\n"); + String collapsedPush = normalizedPushOutput.replaceAll("\\s+", " "); + String expectedPushMsg1 = "Successfully pushed target/bala/" + org + "-pkg1-any-" + ver + + ".bala to '" + GITHUB_REPO_ID + "' repository."; + Assert.assertTrue(collapsedPush.contains(expectedPushMsg1), + "Expected push success message. Actual output: " + pushOutput); + } + + // Publish multiple versions for pkg2 (Dep2) + String[] pkg2Versions = {"1.1.1", "1.1.0", "2.0.0"}; + for (String ver : pkg2Versions) { + // Update package version, pack and push + editVersionBallerinaToml(this.tempWorkspaceDirectory.resolve("pkg2"), ver); + Process p = packTrigger("pkg2", this.tempWorkspaceDirectory, this.envVariables); + String ignoredPackOut = getString(p.getInputStream()); + p = pushTrigger("pkg2", this.tempWorkspaceDirectory, this.envVariables); + String pushOutput = getString(p.getInputStream()); + // Normalize output so Windows backslashes don't break the assertion + String normalizedPushOutput = pushOutput.replace("\\", "/") + .replace("\r\n", "\n").replace("\r", "\n"); + String collapsedPush = normalizedPushOutput.replaceAll("\\s+", " "); + String expectedPushMsg2 = "Successfully pushed target/bala/" + org + "-pkg2-any-" + ver + + ".bala to '" + GITHUB_REPO_ID + "' repository."; + Assert.assertTrue(collapsedPush.contains(expectedPushMsg2), + "Expected push success message. Actual output: " + pushOutput); + } + + } + + @Test(description = "Build package with soft locking mode", + dependsOnMethods = "testCase2_publishAdditionalVersionsForDeps", groups = "testCase2") + public void testCase2_1_softLockingMode_resolvesLatestCompatible() throws IOException, InterruptedException { + List args = new ArrayList<>(); + + File dependencyPathBefore = this.tempWorkspaceDirectory.resolve("myproject1") + .resolve("Dependencies.toml").toFile(); + if (dependencyPathBefore.exists()) { + if (!dependencyPathBefore.delete()) { + Assert.fail("Could not delete Dependencies.toml at " + dependencyPathBefore.getAbsolutePath()); + } + List cleanArgs = new ArrayList<>(); + Process clean = executeCleanCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), cleanArgs, this.envVariables); + String cleanErrors = getString(clean.getErrorStream()); + if (!cleanErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + cleanErrors); + } + } + args.add("--locking-mode=soft"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve("myproject1"), + args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/") + .replace("\r\n", "\n").replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + buildOutput); + + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + Optional pkg1Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg1"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Assert.assertTrue(pkg1Version.isPresent(), "Expected pkg1 to be present in Dependencies.toml"); + Assert.assertEquals(pkg1Version.get(), "0.1.1", "Package version is not matching with " + + "the expected package version"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.1.1", "Package version is not matching with " + + "the expected package version"); + } + + @Test(description = "Build package with medium locking mode", + dependsOnMethods = "testCase2_publishAdditionalVersionsForDeps", groups = "testCase2") + public void testCase2_2_mediumLockingMode_resolvesConservative() throws IOException, InterruptedException { + List args = new ArrayList<>(); + + File dependencyPathBefore = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml").toFile(); + if (dependencyPathBefore.exists()) { + if (!dependencyPathBefore.delete()) { + Assert.fail("Could not delete Dependencies.toml at " + dependencyPathBefore.getAbsolutePath()); + } + List cleanArgs = new ArrayList<>(); + Process clean = executeCleanCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), cleanArgs, this.envVariables); + String cleanErrors = getString(clean.getErrorStream()); + if (!cleanErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + cleanErrors); + } + } + args.add("--locking-mode=medium"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve("myproject1"), + args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/") + .replace("\r\n", "\n").replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), "Expected generating executable + " + + "target/bin/myproject1.jar in output. Actual output: " + buildOutput); + + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + Optional pkg1Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg1"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Assert.assertTrue(pkg1Version.isPresent(), "Expected pkg1 to be present in Dependencies.toml"); + Assert.assertEquals(pkg1Version.get(), "0.1.1", "Package version is not matching with the " + + "expected package version"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.0.0", "Package version is not matching with the " + + "expected package version"); + } + + // HARD mode enforces exact compiler update reproducibility. + // Build is expected to fail if the project was built using a different Swan Lake update. + @Test(description = "Build package with hard locking mode", + dependsOnMethods = "testCase2_publishAdditionalVersionsForDeps", groups = "testCase2") + public void testCase2_3_hardLockingMode_enforcesExact() throws IOException, InterruptedException { + List args = new ArrayList<>(); + File dependencyPathBefore = this.tempWorkspaceDirectory.resolve("myproject1") + .resolve("Dependencies.toml").toFile(); + if (dependencyPathBefore.exists()) { + if (!dependencyPathBefore.delete()) { + Assert.fail("Could not delete Dependencies.toml at " + dependencyPathBefore.getAbsolutePath()); + } + List cleanArgs = new ArrayList<>(); + Process clean = executeCleanCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), cleanArgs, this.envVariables); + String cleanErrors = getString(clean.getErrorStream()); + if (!cleanErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + cleanErrors); + } + } + + args.add("--locking-mode=hard"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve("myproject1"), + args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/") + .replace("\r\n", "\n").replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); - @AfterClass - private void cleanup() throws IOException { + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + + buildOutput); + + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + Optional pkg1Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg1"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Assert.assertTrue(pkg1Version.isPresent(), "Expected pkg1 to be present in Dependencies.toml"); + Assert.assertEquals(pkg1Version.get(), "0.1.0", "Package version is not matching with the " + + "expected package version"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.0.0", "Package version is not matching with the " + + "expected package version"); + + } + + @Test(description = "Build package with locked mode", dependsOnMethods = {"testCase2_publishAdditionalVersionsForDeps", "testCase2_3_hardLockingMode_enforcesExact"}, groups = "testCase2") + public void testCase2_4_lockedMode_usesLockedVersions() throws IOException, InterruptedException { + List args = new ArrayList<>(); + args.add("--locking-mode=locked"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve("myproject1"), + args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n") + .replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + + buildOutput); + + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + Optional pkg1Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg1"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Assert.assertTrue(pkg1Version.isPresent(), "Expected pkg1 to be present in Dependencies.toml"); + Assert.assertEquals(pkg1Version.get(), "0.1.0", "Package version is not matching with the " + + "expected package version"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.0.0", "Package version is not matching with the " + + "expected package version"); + } + + @Test(description = "Build package with backwards compatibility", dependsOnGroups = "testCase2") + public void testCase3_backwardCompatibility_legacyBallerinaToml() throws IOException, InterruptedException { + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + if (Files.deleteIfExists(dependencyPath)) { + // Use separate argument lists for clean and build to avoid reusing/mutating the same list + List cleanArgs = new ArrayList<>(); + Process clean = executeCleanCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), cleanArgs, this.envVariables); + String cleanErrors = getString(clean.getErrorStream()); + if (!cleanErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + cleanErrors); + } + List buildArgs = new ArrayList<>(); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), + buildArgs, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/") + .replace("\r\n", "\n").replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + + buildOutput); + Path dependencyPath2 = this.tempWorkspaceDirectory.resolve("myproject1") + .resolve("Dependencies.toml"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath2, + "pkg2"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.1.1", "Package version is not matching with " + + "the expected package version"); + } else { + Assert.fail("Could not delete Dependencies.toml file to test backwards compatibility"); + } + } + + private void pinPkg1AndPkg2To100() throws IOException, InterruptedException { + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + if (Files.exists(dependencyPath)) { + boolean deleted = Files.deleteIfExists(dependencyPath); + if (!deleted) { + Assert.fail("Could not delete Dependencies.toml at " + dependencyPath); + } + List cleanArgs = new ArrayList<>(); + Process clean = executeCleanCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), cleanArgs, this.envVariables); + String cleanErrors = getString(clean.getErrorStream()); + if (!cleanErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + cleanErrors); + } + } + + Path ballerinaTOML = this.tempWorkspaceDirectory.resolve("myproject1"); + Assert.assertTrue(updateVersionForPackage(ballerinaTOML, "pkg1", "1.0.0"), + "pkg1 not found in Ballerina.toml"); + Assert.assertTrue(updateVersionForPackage(ballerinaTOML, "pkg2", "1.0.0"), + "pkg2 not found in Ballerina.toml"); + List args = new ArrayList<>(); + args.add("--locking-mode=hard"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + } + + // New: run this setup only before the Test Case 4 group + @BeforeGroups("testCase4") + public void beforeGroupTestCase4() throws IOException, InterruptedException { + pinPkg1AndPkg2To100(); + } + + @Test(description = "Test case 4.1: Add dep3 and build with SOFT locking mode", + dependsOnGroups = "testCase2", groups = "testCase4") + public void testCase4_1_addDep3_softLocking() throws IOException, InterruptedException { + Path projectDir = this.tempWorkspaceDirectory.resolve("myproject1"); + // Ensure legacy dependency for pkg3 is present (idempotent) + MavenCustomRepoTestUtils.ensureLegacyDependency(projectDir, "bctestorg", "pkg3", "1.0.0", GITHUB_REPO_ID); + List args = new ArrayList<>(); + args.add("--locking-mode=soft"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, projectDir, args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n").replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + buildOutput); + + Path dependencyPath = projectDir.resolve("Dependencies.toml"); + Optional pkg3Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg3"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.1.1", "pkg2 version should be 1.1.1"); + Assert.assertTrue(pkg3Version.isPresent(), "Expected pkg3 to be present in Dependencies.toml"); + Assert.assertEquals(pkg3Version.get(), "1.0.0", "pkg3 version should be 1.0.0"); + } + + @Test(description = "Test case 4.2: Add dep3 and build with MEDIUM (default) locking mode", + dependsOnGroups = "testCase2", groups = "testCase4") + public void testCase4_2_addDep3_mediumLocking() throws IOException, InterruptedException { + Path projectDir = this.tempWorkspaceDirectory.resolve("myproject1"); + // Ensure legacy dependency for pkg3 is present (idempotent) + MavenCustomRepoTestUtils.pasteStaticMainBalWithPkg1AndPkg2(projectDir); + // Remove existing Dependencies.toml in an idempotent way + Files.deleteIfExists(projectDir.resolve("Dependencies.toml")); + List args = new ArrayList<>(); + args.add("--locking-mode=hard"); + Process buildCleanup = executeBuildCommand(DISTRIBUTION_FILE_NAME, projectDir, + args, this.envVariables); + String buildCleanupErrors = getString(buildCleanup.getErrorStream()); + if (!buildCleanupErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildCleanupErrors); + } + pasteStaticMainBalWithAllPkgs(projectDir); + args = new ArrayList<>(); + args.add("--locking-mode=medium"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, projectDir, args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n") + .replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + + buildOutput); + + Path dependencyPath = projectDir.resolve("Dependencies.toml"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Optional pkg3Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg3"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.0.0", "Pkg2 version should be 1.0.0"); + Assert.assertTrue(pkg3Version.isPresent(), "Expected pkg3 to be present in Dependencies.toml"); + Assert.assertEquals(pkg3Version.get(), "1.0.0", "pkg3 version should be 1.0.0"); + } + + @Test(description = "Test case 4.3: Add dep3 and build with HARD locking mode", + dependsOnGroups = "testCase2", groups = "testCase4") + public void testCase4_3_addDep3_hardLocking() throws IOException, InterruptedException { + Path projectDir = this.tempWorkspaceDirectory.resolve("myproject1"); + // Ensure legacy dependency for pkg3 is present (idempotent) + MavenCustomRepoTestUtils.pasteStaticMainBalWithPkg1AndPkg2(projectDir); + // Remove existing Dependencies.toml in an idempotent way + Files.deleteIfExists(projectDir.resolve("Dependencies.toml")); + List args = new ArrayList<>(); + args.add("--locking-mode=hard"); + Process buildCleanup = executeBuildCommand(DISTRIBUTION_FILE_NAME, projectDir, + args, this.envVariables); + String buildCleanupErrors = getString(buildCleanup.getErrorStream()); + if (!buildCleanupErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildCleanupErrors); + } + pasteStaticMainBalWithAllPkgs(projectDir); + args = new ArrayList<>(); + args.add("--locking-mode=hard"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, projectDir, args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n") + .replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + buildOutput); + + Path dependencyPath = projectDir.resolve("Dependencies.toml"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Optional pkg3Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg3"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertTrue(pkg3Version.isPresent(), "Expected pkg3 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.0.0", "pkg2 version should be 1.0.0"); + Assert.assertEquals(pkg3Version.get(), "1.0.0", "pkg3 version should be 1.0.0"); + } + + @Test(description = "Test case 4.4: Add dep3 and build with LOCKED locking mode", + dependsOnGroups = "testCase2", groups = "testCase4") + public void testCase4_4_addDep3_lockedLocking() throws IOException, InterruptedException { + Path projectDir = this.tempWorkspaceDirectory.resolve("myproject1"); + MavenCustomRepoTestUtils.pasteStaticMainBalWithPkg1AndPkg2(projectDir); + Files.deleteIfExists(projectDir.resolve("Dependencies.toml")); + + List args = new ArrayList<>(); + args.add("--locking-mode=hard"); + Process buildCleanup = executeBuildCommand(DISTRIBUTION_FILE_NAME, projectDir, + args, this.envVariables); + String buildCleanupErrors = getString(buildCleanup.getErrorStream()); + if (!buildCleanupErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildCleanupErrors); + } + + pasteStaticMainBalWithAllPkgs(projectDir); + + args = new ArrayList<>(); + args.add("--locking-mode=locked"); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, projectDir, args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n") + .replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + + // Expect a locked-mode error when attempting to add new imports. + boolean stderrHasLockedMsg = buildErrors != null && (buildErrors.contains("cannot add new imports with" + + " --locking-mode=locked") + || buildErrors.contains("package resolution contains errors")); + boolean stdoutHasLockedMsg = collapsed.contains("cannot add new imports with --locking-mode=locked") + || collapsed.contains("package resolution contains errors"); + + Assert.assertTrue(stderrHasLockedMsg || stdoutHasLockedMsg, + "Expected locked-mode error when adding new imports. stdout: " + collapsed + "\nstderr: " + + buildErrors); + + // Build should not have generated an executable in locked mode when adding new imports. + Assert.assertFalse(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Build unexpectedly succeeded in locked locking mode when adding new imports. stdout: " + + collapsed + "\nstderr: " + buildErrors); + } + + @Test(description = "Add a new version for an existing dependency and build", + dependsOnMethods = "testCase2_publishAdditionalVersionsForDeps") + public void testCase5_addVersionInBallerinaToml_buildSucceeds() throws IOException, InterruptedException { + // Update the package version in Ballerina.toml first, then build to pick up the change. + Assert.assertTrue(updateVersionForPackage(this.tempWorkspaceDirectory.resolve("myproject1"), + "pkg2", "1.1.0"), "pkg2 not found in Ballerina.toml"); + List args = new ArrayList<>(); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, this.tempWorkspaceDirectory.resolve("myproject1"), + args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + String buildOutput = getString(build.getInputStream()); + String normalized = buildOutput.replace("\\", "/").replace("\r\n", "\n") + .replace("\r", "\n"); + String collapsed = normalized.replaceAll("\\s+", " "); + Assert.assertTrue(collapsed.contains("Generating executable") && + collapsed.contains("target/bin/myproject1.jar"), + "Expected generating executable + target/bin/myproject1.jar in output. Actual output: " + buildOutput); + + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + Optional pkg2Version = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2"); + Assert.assertTrue(pkg2Version.isPresent(), "Expected pkg2 to be present in Dependencies.toml"); + Assert.assertEquals(pkg2Version.get(), "1.1.1", "Package version is not matching with the " + + "expected package version"); + } + + @Test(description = "Add an incompatible version for an existing dependency and build should fail", + dependsOnMethods = "testCase2_publishAdditionalVersionsForDeps") + public void testCase5_addIncompatibleVersion_buildFails() throws IOException, InterruptedException { + Path dependencyPath = this.tempWorkspaceDirectory.resolve("myproject1").resolve("Dependencies.toml"); + if (!Files.exists(dependencyPath)) { + // If the locked Dependencies.toml was removed by an earlier test, recreate a pinned state. + // pinPkg1AndPkg2To100 is idempotent and will create a hard-locked Dependencies.toml. + pinPkg1AndPkg2To100(); + } + + // Read the existing locked version so we can restore it after the test + String existingVersion = MavenCustomRepoTestUtils.getPackageVersionFromDependencies(dependencyPath, + "pkg2").orElse(""); + + // Update Ballerina.toml to an incompatible version first, then run the build and assert failure + try { + Assert.assertTrue(updateVersionForPackage(this.tempWorkspaceDirectory.resolve("myproject1"), "pkg2", "2.0.0"), + "pkg2 not found in Ballerina.toml"); + List args = new ArrayList<>(); + Process build = executeBuildCommand(DISTRIBUTION_FILE_NAME, + this.tempWorkspaceDirectory.resolve("myproject1"), args, this.envVariables); + String buildErrors = getString(build.getErrorStream()); + Assert.assertTrue(buildErrors.contains("incompatible with the version locked in Dependencies.toml"), + "Incompatible version error message not found in the build errors. Actual stderr: " + buildErrors); + } finally { + if (!existingVersion.isEmpty()) { + Assert.assertTrue(updateVersionForPackage(this.tempWorkspaceDirectory.resolve("myproject1"), "pkg2", existingVersion), + "pkg2 not found when restoring version in Ballerina.toml"); + } + } + } + + @AfterClass(alwaysRun = true) + public void cleanup() throws IOException { deleteFiles(actualHomeDirectory, true); Files.walkFileTree(actualHomeDirectoryClone, new MavenCustomRepoTest.Copy(actualHomeDirectoryClone, actualHomeDirectory)); deleteFiles(actualHomeDirectoryClone, false); deleteFiles(tempWorkspaceDirectory, false); - deleteArtifacts(org, packagename); + deleteArtifacts(org, "pkg1"); + deleteArtifacts(org, "pkg2"); + deleteArtifacts(org, "pkg3"); } - - /** * Copy test resources to temp directory. */ diff --git a/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTestUtils.java b/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTestUtils.java index b4aa7dfdd2..a4542cb0c6 100644 --- a/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTestUtils.java +++ b/project-api-tests/src/test/java/org/ballerina/projectapi/MavenCustomRepoTestUtils.java @@ -16,8 +16,10 @@ package org.ballerina.projectapi; +import io.ballerina.toml.api.Toml; import okhttp3.OkHttpClient; import okhttp3.Request; +import org.testng.Assert; import java.io.BufferedReader; import java.io.File; @@ -27,12 +29,22 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.ballerina.projectapi.TestUtils.DISTRIBUTION_FILE_NAME; +import static org.ballerina.projectapi.TestUtils.OUTPUT_CONTAIN_ERRORS; +import static org.ballerina.projectapi.TestUtils.executePackCommand; +import static org.ballerina.projectapi.TestUtils.executePushCommand; + /** * Utility class for maven repository tests. @@ -51,7 +63,7 @@ static void createSettingToml(Path dirPath) throws IOException { "username = \"ballerina-platform\"\n " + "accesstoken = \"" + getGithubToken() + "\"\n"; Files.write(dirPath.resolve("Settings.toml"), content.getBytes(), StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING); + StandardOpenOption.TRUNCATE_EXISTING); } /** @@ -94,8 +106,8 @@ static String getString(InputStream outputs) throws IOException { /** * Delete files inside directories. * - * @param dirPath directory path - * @param deleteDirContentOnly delete only the content inside the directory + * @param dirPath directory path + * @param deleteDirContentOnly delete only the content inside the directory * @throws IOException throw an exception if an issue occurs */ static void deleteFiles(Path dirPath, boolean deleteDirContentOnly) throws IOException { @@ -124,4 +136,301 @@ static void deleteArtifacts(String org, String packagename) throws IOException { .build(); client.newCall(request).execute(); } + + /** + * Run the `ballerina pack` command for the given package and return the spawned Process. + * + * @param packageName the package directory name under the sourceDirectory + * @param sourceDirectory the path that contains the package folders + * @param envVariable environment variables to pass to the process + * @return the Process for the pack command + * @throws IOException if an I/O error occurs when starting the process + * @throws InterruptedException if the current thread is interrupted while waiting for the process + */ + static Process packTrigger(String packageName, Path sourceDirectory, Map envVariable) + throws IOException, InterruptedException { + Process process = executePackCommand(DISTRIBUTION_FILE_NAME, + sourceDirectory.resolve(packageName), new ArrayList<>(), + envVariable); + + String buildErrors = getString(process.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + return process; + } + + /** + * Run the `ballerina push` command for the given package and return the spawned Process. + * + * @param packageName the package directory name under the sourceDirectory + * @param sourceDirectory the path that contains the package folders + * @param envVariable environment variables to pass to the process + * @return the Process for the push command + * @throws IOException if an I/O error occurs when starting the process + * @throws InterruptedException if the current thread is interrupted while waiting for the process + */ + static Process pushTrigger(String packageName, Path sourceDirectory, Map envVariable) + throws IOException, InterruptedException { + List args = new ArrayList<>(); + args.add("--repository=" + MavenCustomRepoTest.GITHUB_REPO_ID); + + Process process = executePushCommand(DISTRIBUTION_FILE_NAME, sourceDirectory.resolve(packageName), args, + envVariable); + String buildErrors = getString(process.getErrorStream()); + if (!buildErrors.isEmpty()) { + Assert.fail(OUTPUT_CONTAIN_ERRORS + buildErrors); + } + return process; + } + /** + * Replace the `version = ...` line inside the [package] section of a Ballerina.toml. + * + *

The method performs a simple text substitution: any line starting with + * {@code version =} is replaced with the provided version string. The file is + * overwritten with the updated contents.

+ * + * @param sourceDirectory path to the project containing Ballerina.toml + * @param version the version string to set (e.g. "1.0.0") + * @throws IOException if reading or writing the Ballerina.toml fails + */ + static void editVersionBallerinaToml(Path sourceDirectory, String version) throws IOException { + Path ballerinaTomlPath = sourceDirectory.resolve("Ballerina.toml"); + String toml = Files.readString(ballerinaTomlPath); + + int pkgStart = toml.indexOf("[package]"); + if (pkgStart == -1) { + // No [package] block - append one + String appended = toml + System.lineSeparator() + "[package]" + System.lineSeparator() + + "version = \"" + version + "\""; + Files.writeString(ballerinaTomlPath, appended, StandardOpenOption.TRUNCATE_EXISTING); + return; + } + + int nextHeader = toml.indexOf("\n[", pkgStart + 1); + int blockEnd = nextHeader == -1 ? toml.length() : nextHeader; + String block = toml.substring(pkgStart, blockEnd); + + Pattern versionLine = Pattern.compile("(?m)^(\\s*)version\\s*=.*$"); + Matcher vm = versionLine.matcher(block); + String newBlock; + if (vm.find()) { + // replace existing version line, preserve indentation + newBlock = vm.replaceFirst("$1version = \"" + version + "\""); + } else { + // insert version after header line + int headerEnd = block.indexOf('\n'); + if (headerEnd == -1) { + newBlock = block + System.lineSeparator() + "version = \"" + version + "\""; + } else { + newBlock = block.substring(0, headerEnd + 1) + "version = \"" + version + "\"" + + (headerEnd + 1 < block.length() ? "\n" + block.substring(headerEnd + 1) : ""); + } + } + + String updated = toml.substring(0, pkgStart) + newBlock + toml.substring(blockEnd); + Files.writeString(ballerinaTomlPath, updated, StandardOpenOption.TRUNCATE_EXISTING); + } + + /** + * Update (or insert) a `version = "..."` entry for the given package name inside a + * Ballerina.toml located in the project directory. + * + *

The helper searches for a line containing {@code name = ""} and then + * replaces an existing {@code version =} line following it, or inserts one if missing. + * The operation is persisted to disk and the method returns whether a change was made.

+ * + * @param sourceDirectory the project directory that contains Ballerina.toml + * @param packageName the package name to adjust (e.g. "pkg1") + * @param version the version string to set (e.g. "1.1.0") + * @return true if the file was modified, false if the package name was not found + * @throws IOException if reading or writing the file fails + **/ + static boolean updateVersionForPackage(Path sourceDirectory, String packageName, String version) + throws IOException { + Path ballerinaTomlPath = sourceDirectory.resolve("Ballerina.toml"); + List lines = Files.readAllLines(ballerinaTomlPath); + + for (int i = 0; i < lines.size(); i++) { + String trimmed = lines.get(i).trim(); + // look for name = "pkg" + if (trimmed.startsWith("name") && trimmed.contains("\"" + packageName + "\"")) { + // search forward for version line until next section header + for (int j = i + 1; j < lines.size(); j++) { + String t = lines.get(j).trim(); + if (t.startsWith("[")) { + // reached next section, insert version right after the name line + lines.add(i + 1, "version = \"" + version + "\""); + Files.write(ballerinaTomlPath, lines); + return true; + } + if (t.startsWith("version")) { + // replace existing version line + lines.set(j, "version = \"" + version + "\""); + Files.write(ballerinaTomlPath, lines); + return true; + } + } + // reached EOF without finding version or next section: append version after name + lines.add(i + 1, "version = \"" + version + "\""); + Files.write(ballerinaTomlPath, lines); + return true; + } + } + return false; + } + + /** + * Read a Dependencies.toml and return the version string for the given package name if present. + * + * @param dependencyTomlPath path to Dependencies.toml + * @param packageName package name to look up + * @return Optional version string + * @throws IOException when reading the toml fails + */ + static Optional getPackageVersionFromDependencies(Path dependencyTomlPath, String packageName) + throws IOException { + Toml toml = Toml.read(dependencyTomlPath); + List packages = toml.getTables("package"); + return packages.stream() + .filter(pkg -> pkg.get("name").map(n -> n.toString()).orElse("").equals(packageName)) + .map(pkg -> pkg.get("version").map(v -> v.toString())) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + + /** + * Add or update a [[dependency]] in Ballerina.toml for the given org and package, then update the source. + * + * @param projectDir the project directory containing Ballerina.toml + * @param org the organization/group id of the dependency (e.g., "bctestorg") + * @param name the package name of the dependency (e.g., "pkg1") + * @param version the version to pin for the dependency (e.g., "1.0.0") + * @param repository the repository id to use for the dependency (e.g., "github1") + * @throws IOException if reading or writing the Ballerina.toml fails + */ + public static void ensureLegacyDependency(Path projectDir, String org, String name, String version, + String repository) throws IOException { + Path ballerinaTomlPath = projectDir.resolve("Ballerina.toml"); + if (!Files.exists(ballerinaTomlPath)) { + throw new IOException("Missing Ballerina.toml in project: " + projectDir); + } + String toml = Files.readString(ballerinaTomlPath); + String desiredDepBlock = "\n[[dependency]]\norg=\"" + org + "\"\nname=\"" + name + "\"\nversion=\"" + version + + "\"\nrepository=\"" + repository + "\"\n"; + + // Find an existing [[dependency]] block for the given org+name + Pattern depPattern = Pattern.compile("(?is)\\[\\[dependency\\]\\].*?org\\s*=\\s*\"" + + Pattern.quote(org) + "\".*?name\\s*=\\s*\"" + Pattern.quote(name) + "\"" + + ".*?(?=\\z|\\[\\[dependency\\]\\])"); + Matcher matcher = depPattern.matcher(toml); + if (matcher.find()) { + String existingBlock = matcher.group(0); + // Try to extract version if present + Pattern verPat = Pattern.compile("version\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); + Matcher vm = verPat.matcher(existingBlock); + if (vm.find()) { + String existingVersion = vm.group(1); + if (existingVersion.equals(version)) { + // already the desired version + // Ensure source-level usage and return + if ("pkg1".equals(name) || "pkg2".equals(name) || "pkg3".equals(name)) { + pasteStaticMainBalWithAllPkgs(projectDir); + } + return; + } + } + // Version differs or missing -> replace the whole dependency block with the desired block + String updatedToml = toml.substring(0, matcher.start()) + desiredDepBlock + toml.substring(matcher.end()); + Files.writeString(ballerinaTomlPath, updatedToml); + } else { + // No existing dependency block -> append + Files.writeString(ballerinaTomlPath, toml + desiredDepBlock); + } + + // Ensure source-level usage so the package becomes a compile-time dependency + if ("pkg1".equals(name) || "pkg2".equals(name) || "pkg3".equals(name)) { + pasteStaticMainBalWithAllPkgs(projectDir); + } + } + + /** + * Writes a deterministic `main.bal` that imports pkg2 and pkg1 and calls pkg2:main() then pkg1:main(). + * it returns early if the file already contains the imports and calls. + * + * @param projectDir the project directory where main.bal should be written + * @throws IOException if an I/O error occurs while creating or writing the file + */ + public static void pasteStaticMainBalWithPkg1AndPkg2(Path projectDir) throws IOException { + Path mainBal = projectDir.resolve("main.bal"); + if (!Files.exists(mainBal)) { + mainBal = projectDir.resolve("src").resolve("main.bal"); + } + if (mainBal.getParent() != null && !Files.exists(mainBal.getParent())) { + Files.createDirectories(mainBal.getParent()); + } + + String import1 = "import bctestorg/pkg1;"; + String import2 = "import bctestorg/pkg2;"; + String call1 = "pkg1:main();"; + String call2 = "pkg2:main();"; + + + + String snippet = import2 + "\n" + import1 + "\n" + + "\n" + + "public function main(string... args) {\n" + + " // Ensure pkg2 then pkg1 are used so they become compile-time dependencies\n" + + " " + call2 + "\n" + + " " + call1 + "\n" + + "}\n"; + + Files.writeString(mainBal, snippet, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + + /** + * Writes a single deterministic `main.bal` that uses pkg2, pkg1 and pkg3. + * it returns early if the file already contains the imports and calls. + * + * @param projectDir the project directory where main.bal should be written + * @throws IOException if an I/O error occurs while creating or writing the file + */ + public static void pasteStaticMainBalWithAllPkgs(Path projectDir) throws IOException { + Path mainBal = projectDir.resolve("main.bal"); + if (!Files.exists(mainBal)) { + mainBal = projectDir.resolve("src").resolve("main.bal"); + } + if (mainBal.getParent() != null && !Files.exists(mainBal.getParent())) { + Files.createDirectories(mainBal.getParent()); + } + + String import1 = "import bctestorg/pkg1;"; + String import2 = "import bctestorg/pkg2;"; + String import3 = "import bctestorg/pkg3;"; + String call1 = "pkg1:main();"; + String call2 = "pkg2:main();"; + String call3 = "pkg3:main();"; + + if (Files.exists(mainBal)) { + String existing = Files.readString(mainBal); + if (existing.contains(import1) && existing.contains(import2) && existing.contains(import3) + && existing.contains(call1) && existing.contains(call2) && existing.contains(call3)) { + return; // already contains desired snippet + } + } + + String snippet = import2 + "\n" + import1 + "\n" + import3 + "\n" + + "\n" + + "public function main(string... args) {\n" + + " // Ensure pkg2, pkg1 and pkg3 are used so they become compile-time dependencies\n" + + " " + call2 + "\n" + + " " + call1 + "\n" + + " " + call3 + "\n" + + "}\n"; + + Files.writeString(mainBal, snippet, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } diff --git a/project-api-tests/src/test/java/org/ballerina/projectapi/TestUtils.java b/project-api-tests/src/test/java/org/ballerina/projectapi/TestUtils.java index 67cc0ef516..ed5acdf040 100644 --- a/project-api-tests/src/test/java/org/ballerina/projectapi/TestUtils.java +++ b/project-api-tests/src/test/java/org/ballerina/projectapi/TestUtils.java @@ -63,7 +63,13 @@ private TestUtils() { public static Process executeCommand(String command, String distributionName, Path sourceDirectory, List args, Map envProperties) throws IOException, InterruptedException { args.add(0, command); - args.add(0, TEST_DISTRIBUTION_PATH.resolve(distributionName).resolve("bin").resolve("bal").toString()); + // Use platform-specific bal executable: on Windows use bal.bat, otherwise use bal + String osName = System.getProperty("os.name"); + String balExecutable = "bal"; + if (osName != null && osName.toLowerCase().contains("win")) { + balExecutable = "bal.bat"; + } + args.add(0, TEST_DISTRIBUTION_PATH.resolve(distributionName).resolve("bin").resolve(balExecutable).toString()); OUT.println("Executing: " + StringUtils.join(args, ' ')); @@ -118,6 +124,11 @@ public static Process executeHelpCommand(String distributionName, Path sourceDir return executeCommand("help", distributionName, sourceDirectory, args, envProperties); } + public static Process executeCleanCommand(String distributionName, Path sourceDirectory, + List args, Map envProperties) throws IOException, InterruptedException { + return executeCommand("clean", distributionName, sourceDirectory, args, envProperties); + } + public static Process executeNewCommand(String distributionName, Path sourceDirectory, List args, Map envProperties) throws IOException, InterruptedException { diff --git a/project-api-tests/src/test/resources/maven-repos/myproject1/.devcontainer.json b/project-api-tests/src/test/resources/maven-repos/myproject1/.devcontainer.json new file mode 100644 index 0000000000..1b2b1ce159 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/myproject1/.devcontainer.json @@ -0,0 +1,8 @@ +{ + "image": "ballerina/ballerina-devcontainer:2201.12.10", + "customizations": { + "vscode": { + "extensions": ["WSO2.ballerina"] + } + } +} diff --git a/project-api-tests/src/test/resources/maven-repos/myproject1/.gitignore b/project-api-tests/src/test/resources/maven-repos/myproject1/.gitignore new file mode 100644 index 0000000000..2d54267fa8 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/myproject1/.gitignore @@ -0,0 +1,11 @@ +# Ballerina generates this directory during the compilation of a package. +# It contains compiler-generated artifacts and the final executable if this is an application package. +target/ + +# Ballerina maintains the compiler-generated source code here. +# Remove this if you want to commit generated sources. +generated/ + +# Contains configuration values used during development time. +# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. +Config.toml diff --git a/project-api-tests/src/test/resources/maven-repos/myproject1/Ballerina.toml b/project-api-tests/src/test/resources/maven-repos/myproject1/Ballerina.toml new file mode 100644 index 0000000000..5c465c260b --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/myproject1/Ballerina.toml @@ -0,0 +1,23 @@ +[package] +org = "bctestorg" +name = "myproject1" +version = "0.1.0" +distribution = "2201.12.10" +readme = "Package.md" + +[build-options] +observabilityIncluded = true + + +[[dependency]] +org="bctestorg" +name="pkg1" +version="0.1.0" +repository="github1" + +[[dependency]] +org="bctestorg" +name="pkg2" +version="1.0.0" +repository="github1" + diff --git a/project-api-tests/src/test/resources/maven-repos/myproject1/Package.md b/project-api-tests/src/test/resources/maven-repos/myproject1/Package.md new file mode 100644 index 0000000000..202f1a015b --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/myproject1/Package.md @@ -0,0 +1,2 @@ +Package documentation for myproject1. + diff --git a/project-api-tests/src/test/resources/maven-repos/myproject1/main.bal b/project-api-tests/src/test/resources/maven-repos/myproject1/main.bal new file mode 100644 index 0000000000..06bd6dec47 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/myproject1/main.bal @@ -0,0 +1,7 @@ +import bctestorg/pkg1; +import bctestorg/pkg2; + +public function main() { + pkg1:main(); + pkg2:main(); +} diff --git a/project-api-tests/src/test/resources/maven-repos/pact/Ballerina.toml b/project-api-tests/src/test/resources/maven-repos/pact/Ballerina.toml index 7e22b99cfe..09d2803129 100644 --- a/project-api-tests/src/test/resources/maven-repos/pact/Ballerina.toml +++ b/project-api-tests/src/test/resources/maven-repos/pact/Ballerina.toml @@ -1,8 +1,9 @@ [package] org = "bctestorg" name = "pact" -version = "0.2.0" +version = "0.1.0" readme = "Package.md" [build-options] observabilityIncluded = true + diff --git a/project-api-tests/src/test/resources/maven-repos/pact/Package.md b/project-api-tests/src/test/resources/maven-repos/pact/Package.md index 3806222f94..f0c4342aa0 100644 --- a/project-api-tests/src/test/resources/maven-repos/pact/Package.md +++ b/project-api-tests/src/test/resources/maven-repos/pact/Package.md @@ -1 +1,2 @@ -jsjj +jsjj + diff --git a/project-api-tests/src/test/resources/maven-repos/pkg1/.devcontainer.json b/project-api-tests/src/test/resources/maven-repos/pkg1/.devcontainer.json new file mode 100644 index 0000000000..86079a3084 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg1/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "image": "ballerina/ballerina-devcontainer:2201.6.0", + "extensions": ["WSO2.ballerina"], +} diff --git a/project-api-tests/src/test/resources/maven-repos/pkg1/.gitignore b/project-api-tests/src/test/resources/maven-repos/pkg1/.gitignore new file mode 100644 index 0000000000..7512ebe232 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg1/.gitignore @@ -0,0 +1,3 @@ +target +generated +Config.toml diff --git a/project-api-tests/src/test/resources/maven-repos/pkg1/Ballerina.toml b/project-api-tests/src/test/resources/maven-repos/pkg1/Ballerina.toml new file mode 100644 index 0000000000..e9c0a005ec --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg1/Ballerina.toml @@ -0,0 +1,10 @@ +[package] +org = "bctestorg" +name = "pkg1" +version = "0.1.0" +readme = "Package.md" + + +[build-options] +observabilityIncluded = true + diff --git a/project-api-tests/src/test/resources/maven-repos/pkg1/Package.md b/project-api-tests/src/test/resources/maven-repos/pkg1/Package.md new file mode 100644 index 0000000000..d6930bd3a3 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg1/Package.md @@ -0,0 +1,2 @@ +Package documentation for pkg1. + diff --git a/project-api-tests/src/test/resources/maven-repos/pkg1/main.bal b/project-api-tests/src/test/resources/maven-repos/pkg1/main.bal new file mode 100644 index 0000000000..6b71ef415c --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg1/main.bal @@ -0,0 +1,2 @@ +public function main() { +} diff --git a/project-api-tests/src/test/resources/maven-repos/pkg2/.devcontainer.json b/project-api-tests/src/test/resources/maven-repos/pkg2/.devcontainer.json new file mode 100644 index 0000000000..86079a3084 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg2/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "image": "ballerina/ballerina-devcontainer:2201.6.0", + "extensions": ["WSO2.ballerina"], +} diff --git a/project-api-tests/src/test/resources/maven-repos/pkg2/.gitignore b/project-api-tests/src/test/resources/maven-repos/pkg2/.gitignore new file mode 100644 index 0000000000..7512ebe232 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg2/.gitignore @@ -0,0 +1,3 @@ +target +generated +Config.toml diff --git a/project-api-tests/src/test/resources/maven-repos/pkg2/Ballerina.toml b/project-api-tests/src/test/resources/maven-repos/pkg2/Ballerina.toml new file mode 100644 index 0000000000..3fb3842e6b --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg2/Ballerina.toml @@ -0,0 +1,10 @@ +[package] +org = "bctestorg" +name = "pkg2" +version = "1.0.0" +readme = "Package.md" + + +[build-options] +observabilityIncluded = true + diff --git a/project-api-tests/src/test/resources/maven-repos/pkg2/Package.md b/project-api-tests/src/test/resources/maven-repos/pkg2/Package.md new file mode 100644 index 0000000000..ea8a435438 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg2/Package.md @@ -0,0 +1,2 @@ +Package documentation for pkg2. + diff --git a/project-api-tests/src/test/resources/maven-repos/pkg2/main.bal b/project-api-tests/src/test/resources/maven-repos/pkg2/main.bal new file mode 100644 index 0000000000..6b71ef415c --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg2/main.bal @@ -0,0 +1,2 @@ +public function main() { +} diff --git a/project-api-tests/src/test/resources/maven-repos/pkg3/.devcontainer.json b/project-api-tests/src/test/resources/maven-repos/pkg3/.devcontainer.json new file mode 100644 index 0000000000..86079a3084 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg3/.devcontainer.json @@ -0,0 +1,4 @@ +{ + "image": "ballerina/ballerina-devcontainer:2201.6.0", + "extensions": ["WSO2.ballerina"], +} diff --git a/project-api-tests/src/test/resources/maven-repos/pkg3/.gitignore b/project-api-tests/src/test/resources/maven-repos/pkg3/.gitignore new file mode 100644 index 0000000000..7512ebe232 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg3/.gitignore @@ -0,0 +1,3 @@ +target +generated +Config.toml diff --git a/project-api-tests/src/test/resources/maven-repos/pkg3/Ballerina.toml b/project-api-tests/src/test/resources/maven-repos/pkg3/Ballerina.toml new file mode 100644 index 0000000000..162ab868a5 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg3/Ballerina.toml @@ -0,0 +1,16 @@ +[package] +org = "bctestorg" +name = "pkg3" +version = "1.0.0" +readme = "Package.md" + + +[[dependency]] +org = "bctestorg" +name = "pkg2" +version = "1.0.0" + + +[build-options] +observabilityIncluded = true + diff --git a/project-api-tests/src/test/resources/maven-repos/pkg3/Package.md b/project-api-tests/src/test/resources/maven-repos/pkg3/Package.md new file mode 100644 index 0000000000..64d9154830 --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg3/Package.md @@ -0,0 +1,2 @@ +Package documentation for pkg3. + diff --git a/project-api-tests/src/test/resources/maven-repos/pkg3/main.bal b/project-api-tests/src/test/resources/maven-repos/pkg3/main.bal new file mode 100644 index 0000000000..6b71ef415c --- /dev/null +++ b/project-api-tests/src/test/resources/maven-repos/pkg3/main.bal @@ -0,0 +1,2 @@ +public function main() { +}