diff --git a/pom.xml b/pom.xml index 85f0693..9bc2d59 100644 --- a/pom.xml +++ b/pom.xml @@ -41,12 +41,25 @@ 2.0.17 3.0.0 1.22.0 - 6.0.0 + 6.0.3 + 2.3.0 5.20.0 3.27.6 3.5.0 + + + + org.junit + junit-bom + ${version.junit} + pom + import + + + + eu.maveniverse.maven.mima @@ -98,7 +111,17 @@ org.junit.jupiter junit-jupiter - ${version.junit} + test + + + org.junit.platform + junit-platform-launcher + test + + + org.junit-pioneer + junit-pioneer + ${version.junit-pioneer} test @@ -279,6 +302,9 @@ org.apache.maven.plugins maven-surefire-plugin + + --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED + org.apache.maven.plugins diff --git a/src/main/java/org/codejive/jpm/Main.java b/src/main/java/org/codejive/jpm/Main.java index bab71fd..efe7c5b 100644 --- a/src/main/java/org/codejive/jpm/Main.java +++ b/src/main/java/org/codejive/jpm/Main.java @@ -15,6 +15,7 @@ import java.io.*; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.*; import java.util.concurrent.Callable; @@ -90,7 +91,7 @@ public Integer call() throws Exception { Jpm.builder() .directory(artifactsMixin.directory) .noLinks(artifactsMixin.noLinks) - .cacheDir(artifactsMixin.cacheDir) + .cacheDir(artifactsMixin.getCacheDir()) .build() .copy( artifactsMixin.artifactNames, @@ -163,7 +164,7 @@ public Integer call() throws Exception { Jpm.builder() .directory(depsMixin.directory) .noLinks(depsMixin.noLinks) - .cacheDir(depsMixin.cacheDir) + .cacheDir(depsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .build() .install( @@ -177,7 +178,7 @@ public Integer call() throws Exception { Jpm.builder() .directory(depsMixin.directory) .noLinks(depsMixin.noLinks) - .cacheDir(depsMixin.cacheDir) + .cacheDir(depsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .build() .copy( @@ -219,7 +220,7 @@ String[] search(String artifactPattern) { return Jpm.builder() .directory(depsMixin.directory) .noLinks(depsMixin.noLinks) - .cacheDir(depsMixin.cacheDir) + .cacheDir(depsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .build() .search(artifactPattern, Math.min(max, 200), backend); @@ -320,7 +321,7 @@ public Integer call() throws Exception { Jpm.builder() .directory(optionalArtifactsMixin.directory) .noLinks(optionalArtifactsMixin.noLinks) - .cacheDir(optionalArtifactsMixin.cacheDir) + .cacheDir(optionalArtifactsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .build() .install( @@ -351,7 +352,7 @@ public Integer call() throws Exception { Jpm.builder() .directory(optionalArtifactsMixin.directory) .noLinks(optionalArtifactsMixin.noLinks) - .cacheDir(optionalArtifactsMixin.cacheDir) + .cacheDir(optionalArtifactsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .build() .path( @@ -409,7 +410,7 @@ public Integer call() throws Exception { return Jpm.builder() .directory(depsMixin.directory) .noLinks(depsMixin.noLinks) - .cacheDir(depsMixin.cacheDir) + .cacheDir(depsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .verbose(!quietMixin.quiet) .build() @@ -468,7 +469,7 @@ public Integer call() throws Exception { Jpm.builder() .directory(depsMixin.directory) .noLinks(depsMixin.noLinks) - .cacheDir(depsMixin.cacheDir) + .cacheDir(depsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .build() .listActions(); @@ -514,7 +515,7 @@ public Integer call() throws Exception { Jpm.builder() .directory(depsMixin.directory) .noLinks(depsMixin.noLinks) - .cacheDir(depsMixin.cacheDir) + .cacheDir(depsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .verbose(!quietMixin.quiet) .build() @@ -548,7 +549,7 @@ public Integer call() throws Exception { return Jpm.builder() .directory(depsMixin.directory) .noLinks(depsMixin.noLinks) - .cacheDir(depsMixin.cacheDir) + .cacheDir(depsMixin.getCacheDir()) .appFile(appInfoFileMixin.appInfoFile) .build() .executeAction(actionName(), args, depsMixin.getRepositoryMap()); @@ -624,6 +625,23 @@ static class DepsMixin { "Directory where downloaded artifacts will be cached (default: value of JPM_CACHE environment variable; whatever is set in Maven's settings.xml or $HOME/.m2/repository") Path cacheDir; + Path getCacheDir() { + if (cacheDir != null) { + return cacheDir; + } + String envCache = System.getenv("JPM_CACHE"); + if (envCache != null && !envCache.isEmpty()) { + try { + return Path.of(envCache); + } catch (InvalidPathException e) { + System.err.println( + "Warning: Invalid path in JPM_CACHE environment variable, ignoring: " + + envCache); + } + } + return null; + } + Map getRepositoryMap() { Map repoMap = new HashMap<>(); for (String repo : repositories) { diff --git a/src/test/java/org/codejive/jpm/MainCacheIntegrationTest.java b/src/test/java/org/codejive/jpm/MainCacheIntegrationTest.java new file mode 100644 index 0000000..0adda31 --- /dev/null +++ b/src/test/java/org/codejive/jpm/MainCacheIntegrationTest.java @@ -0,0 +1,166 @@ +package org.codejive.jpm; + +import static org.assertj.core.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; +import org.junitpioneer.jupiter.SetEnvironmentVariable; +import picocli.CommandLine; + +/** Integration tests for the --cache option and JPM_CACHE environment variable. */ +class MainCacheIntegrationTest { + + @TempDir Path tempDir; + @TempDir Path cacheDir1; + @TempDir Path cacheDir2; + + private String originalUserDir; + private PrintStream originalOut; + private PrintStream originalErr; + private ByteArrayOutputStream outContent; + private ByteArrayOutputStream errContent; + + @BeforeEach + void setUp() { + originalUserDir = System.getProperty("user.dir"); + System.setProperty("user.dir", tempDir.toString()); + System.setProperty("picocli.ansi", "false"); + + // Capture stdout and stderr + originalOut = System.out; + originalErr = System.err; + outContent = new ByteArrayOutputStream(); + errContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + @AfterEach + void tearDown() { + System.setProperty("user.dir", originalUserDir); + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + @ClearEnvironmentVariable(key = "JPM_CACHE") + void testPathCommandWithCacheOption() throws IOException { + // Create app.yml + createSimpleAppYml(); + + CommandLine cmd = Main.getCommandLine(); + int exitCode = cmd.execute("path", "--cache", cacheDir1.toString()); + + // Exit code 0 or 1 is acceptable (1 means dependency not found, which is expected) + assertThat(exitCode).isIn(0, 1); + } + + @Test + @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/env-cache") + void testPathCommandWithEnvironmentVariable() throws IOException { + // Create app.yml + createSimpleAppYml(); + + CommandLine cmd = Main.getCommandLine(); + int exitCode = cmd.execute("path"); + + // The command should succeed with JPM_CACHE set + assertThat(exitCode).isIn(0, 1); + } + + @Test + @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/env-cache") + void testCopyCommandCacheOptionOverridesEnvironmentVariable() throws IOException { + CommandLine cmd = Main.getCommandLine(); + int exitCode = + cmd.execute( + "copy", "--cache", cacheDir1.toString(), "--quiet", "fake:artifact:1.0.0"); + + // The command should use cacheDir1 (from --cache) not /tmp/env-cache + // Even though it will fail to resolve, it should parse correctly + assertThat(exitCode).isIn(0, 1); // May fail to resolve, but shouldn't crash + } + + @Test + @ClearEnvironmentVariable(key = "JPM_CACHE") + void testInstallCommandWithShortCacheOption() throws IOException { + createSimpleAppYml(); + + CommandLine cmd = Main.getCommandLine(); + int exitCode = + cmd.execute( + "install", "-c", cacheDir1.toString(), "--quiet", "fake:artifact:1.0.0"); + + // The -c short form should work the same as --cache + assertThat(exitCode).isIn(0, 1); // May fail to resolve, but shouldn't crash + } + + @Test + void testCacheOptionInHelp() { + CommandLine cmd = Main.getCommandLine(); + int exitCode = cmd.execute("copy", "--help"); + + // PicoCLI may return 0 or 2 for help depending on configuration + // What matters is that the help text is displayed + String output = outContent.toString() + errContent.toString(); + assertThat(output) + .contains("-c, --cache") + .contains("Directory where downloaded artifacts will be cached") + .contains("JPM_CACHE"); + } + + @Test + @SetEnvironmentVariable(key = "JPM_CACHE", value = " ") + void testGetCacheDirWithWhitespaceOnlyEnvironmentVariable() throws IOException { + // An environment variable with only whitespace should be treated as empty + createSimpleAppYml(); + + CommandLine cmd = Main.getCommandLine(); + // This should not crash - whitespace-only JPM_CACHE should be ignored + int exitCode = cmd.execute("path"); + + assertThat(exitCode).isIn(0, 1); + } + + @Test + @ClearEnvironmentVariable(key = "JPM_CACHE") + void testCacheOptionWithRelativePath() throws IOException { + createSimpleAppYml(); + + CommandLine cmd = Main.getCommandLine(); + int exitCode = cmd.execute("path", "--cache", "./my-cache"); + + assertThat(exitCode).isIn(0, 1); + // Should accept relative paths + } + + @Test + @ClearEnvironmentVariable(key = "JPM_CACHE") + void testCacheOptionWithAbsolutePath() throws IOException { + createSimpleAppYml(); + + CommandLine cmd = Main.getCommandLine(); + int exitCode = cmd.execute("path", "--cache", cacheDir1.toAbsolutePath().toString()); + + assertThat(exitCode).isIn(0, 1); + // Should accept absolute paths + } + + private void createSimpleAppYml() throws IOException { + String yamlContent = + "dependencies:\n" + + " fake:dummy: \"1.2.3\"\n" + + "\n" + + "actions:\n" + + " build: \"echo building\"\n"; + Files.writeString(tempDir.resolve("app.yml"), yamlContent); + } +} diff --git a/src/test/java/org/codejive/jpm/MainCacheOptionsTest.java b/src/test/java/org/codejive/jpm/MainCacheOptionsTest.java new file mode 100644 index 0000000..33722a5 --- /dev/null +++ b/src/test/java/org/codejive/jpm/MainCacheOptionsTest.java @@ -0,0 +1,114 @@ +package org.codejive.jpm; + +import static org.assertj.core.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +/** Tests for Main class CLI cache option parsing. */ +class MainCacheOptionsTest { + + @TempDir Path tempCacheDir1; + @TempDir Path tempCacheDir2; + + private PrintStream originalErr; + private ByteArrayOutputStream errContent; + + @BeforeEach + void setUp() { + // Capture stderr to verify warning messages + originalErr = System.err; + errContent = new ByteArrayOutputStream(); + System.setErr(new PrintStream(errContent)); + } + + @AfterEach + void tearDown() { + System.setErr(originalErr); + } + + @Test + @ClearEnvironmentVariable(key = "JPM_CACHE") + void testGetCacheDirWithCommandLineOption() { + Main.DepsMixin mixin = new Main.DepsMixin(); + mixin.cacheDir = tempCacheDir1; + + Path result = mixin.getCacheDir(); + + assertThat(result).isEqualTo(tempCacheDir1); + assertThat(errContent.toString()).isEmpty(); // No warnings + } + + @Test + @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/jpm-cache-from-env") + void testGetCacheDirFromEnvironmentVariable() { + Main.DepsMixin mixin = new Main.DepsMixin(); + // Don't set mixin.cacheDir - it should use the env var + + Path result = mixin.getCacheDir(); + + assertThat(result).isNotNull(); + String expectedPath = Path.of("/tmp/jpm-cache-from-env").toString(); + assertThat(result.toString()).isEqualTo(expectedPath); + assertThat(errContent.toString()).isEmpty(); // No warnings + } + + @Test + @SetEnvironmentVariable(key = "JPM_CACHE", value = "/tmp/jpm-cache-from-env") + void testCommandLineOptionTakesPrecedenceOverEnvironmentVariable() { + Main.DepsMixin mixin = new Main.DepsMixin(); + mixin.cacheDir = tempCacheDir1; // Command line option is set + + Path result = mixin.getCacheDir(); + + // Should return the command line option, not the environment variable + assertThat(result).isEqualTo(tempCacheDir1); + assertThat(errContent.toString()).isEmpty(); // No warnings + } + + @Test + @ClearEnvironmentVariable(key = "JPM_CACHE") + void testGetCacheDirWithNoOptionAndNoEnvironmentVariable() { + Main.DepsMixin mixin = new Main.DepsMixin(); + // Neither command line option nor environment variable is set + + Path result = mixin.getCacheDir(); + + assertThat(result).isNull(); + assertThat(errContent.toString()).isEmpty(); // No warnings + } + + @Test + @SetEnvironmentVariable(key = "JPM_CACHE", value = "") + void testGetCacheDirWithEmptyEnvironmentVariable() { + Main.DepsMixin mixin = new Main.DepsMixin(); + + Path result = mixin.getCacheDir(); + + assertThat(result).isNull(); + assertThat(errContent.toString()).isEmpty(); // No warnings + } + + @Test + @ClearEnvironmentVariable(key = "JPM_CACHE") + void testGetCacheDirMultipleCalls() { + Main.DepsMixin mixin = new Main.DepsMixin(); + mixin.cacheDir = tempCacheDir1; + + // Call multiple times to ensure consistent behavior + Path result1 = mixin.getCacheDir(); + Path result2 = mixin.getCacheDir(); + + assertThat(result1).isEqualTo(tempCacheDir1); + assertThat(result2).isEqualTo(tempCacheDir1); + assertThat(result1).isEqualTo(result2); + assertThat(errContent.toString()).isEmpty(); // No warnings + } +}