diff --git a/docs/e2e.md b/docs/e2e.md index 98a8555..5f98908 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -63,7 +63,12 @@ JDK, so `java` can only come from what vfox installs: registered from this checkout. On Windows, the runner builds `main` once, while the `latest` combinations run, and the `main` containers start from the e2e image plus that `vfox.exe`. -2. A release no distribution has published (`.0.99`) fails to install, +2. A distribution without a version, alone or after an empty one + (`java@tem`, `java@-tem`), fails both `vfox install` and `vfox use` with + an error asking for one. The hooks stopped with a Lua error on the first, + while `PreInstall` sent the empty version to Foojay, which lists every + release, and installed the newest one. + A release no distribution has published (`.0.99`) fails to install, with `PreInstall` naming the platform it searched (`linux/amd64`, `windows/arm64`, ...), so the Foojay lookup is checked to target the runner. So does a build of the pinned release that was never published diff --git a/hooks/parse_legacy_file.lua b/hooks/parse_legacy_file.lua index e42d216..36b7a1e 100644 --- a/hooks/parse_legacy_file.lua +++ b/hooks/parse_legacy_file.lua @@ -83,7 +83,11 @@ function PLUGIN:ParseLegacyFile(ctx) if not distributionVersion then return {} end + --- A distribution alone (java=tem) selects nothing, as vfox use rejects it. local fullVersion = distributionVersion.version + if not fullVersion then + return {} + end local vendor = distributionVersion.distribution.short_name if not vendor then return {} diff --git a/hooks/pre_install.lua b/hooks/pre_install.lua index f399025..27ec4ba 100644 --- a/hooks/pre_install.lua +++ b/hooks/pre_install.lua @@ -30,6 +30,9 @@ function PLUGIN:PreInstall(ctx) if not distribution_version then error("Could not extract a valid distribution: " .. ctx.version) end + if not distribution_version.version then + error(distribution_version_parser.missing_version_error(ctx.version, distribution_version)) + end if loongnix.supported() or loongnix.is_distribution(distribution_version.distribution.name) then return loongnix.pre_install(distribution_version) end diff --git a/hooks/pre_use.lua b/hooks/pre_use.lua index 440df66..565aa00 100644 --- a/hooks/pre_use.lua +++ b/hooks/pre_use.lua @@ -29,6 +29,9 @@ function PLUGIN:PreUse(ctx) end if distribution_version then + if not distribution_version.version then + error(distribution_version_parser.missing_version_error(input_version, distribution_version)) + end -- OpenJDK versions are installed without a distribution suffix, so -- 25.0.2+10-open and openjdk-25.0.2+10 both name 25.0.2+10. local suffix = distribution_version.javafx_bundled and "-fx" or "" diff --git a/lib/distribution_version.lua b/lib/distribution_version.lua index 6671acf..f71949e 100644 --- a/lib/distribution_version.lua +++ b/lib/distribution_version.lua @@ -130,8 +130,15 @@ function distribution_version.parse_version(arg) end end - -- Convert version format for Foojay API - version = distribution_version.convert_version_for_api(version) + -- A distribution alone (tem, zulu-fx), or after an empty version (-tem, + -- temurin-), names no version: leave it nil for the hooks to reject. + if version == "" then + version = nil + end + if version then + -- Convert version format for Foojay API + version = distribution_version.convert_version_for_api(version) + end return { version = version, @@ -140,4 +147,21 @@ function distribution_version.parse_version(arg) } end +--- Returns the error for a user-input version that parse_version read +--- without a version, such as tem: it names no JDK to install or use. +--- @param arg string User-input version +--- @param parsed table What parse_version returned for arg +--- @return string Error message +function distribution_version.missing_version_error(arg, parsed) + local short_name = parsed.distribution.short_name + return "No version in " + .. arg + .. "; add one, such as 21-" + .. short_name + .. (parsed.javafx_bundled and "-fx" or "") + .. ". Please check available versions with 'vfox search java " + .. short_name + .. "'" +end + return distribution_version diff --git a/tests/e2e/common/common.go b/tests/e2e/common/common.go index 41afd0c..0f355a5 100644 --- a/tests/e2e/common/common.go +++ b/tests/e2e/common/common.go @@ -260,6 +260,16 @@ func MissingBuildSpec(distribution, version string) string { return Spec(distribution, version+"+99") } +// VersionlessSpecs renders the distribution without a version, alone and +// after an empty one ("tem" and "-tem", "librca-fx" and "-librca-fx", "open" +// and "-open" for OpenJDK). PreInstall and PreUse must reject both, asking +// for a version: they failed with a Lua error on the first (vfox-java#98), +// while PreInstall sent the empty version to Foojay, which lists every +// release, and installed the newest one. +func VersionlessSpecs(distribution string) []string { + return []string{distribution, "-" + distribution} +} + // Decoy renders the installed name of another release whose version starts // with the pinned one (0+1, e.g. 21.0.80+1-tem). vfox lists the // installed versions sorted as strings, descending, so it comes before the diff --git a/tests/e2e/common/common_test.go b/tests/e2e/common/common_test.go index d9edc2a..b1f19ad 100644 --- a/tests/e2e/common/common_test.go +++ b/tests/e2e/common/common_test.go @@ -209,6 +209,21 @@ func TestSpecs(t *testing.T) { } } +func TestVersionlessSpecs(t *testing.T) { + for _, c := range []struct { + dist string + want []string + }{ + {"open", []string{"open", "-open"}}, + {"tem", []string{"tem", "-tem"}}, + {"librca-fx", []string{"librca-fx", "-librca-fx"}}, + } { + if got := VersionlessSpecs(c.dist); !slices.Equal(got, c.want) { + t.Errorf("VersionlessSpecs(%s) = %q, want %q", c.dist, got, c.want) + } + } +} + func TestPaddedIdentifier(t *testing.T) { for _, c := range []struct{ dist, version, installed, want string }{ {"open", "25.0.2", "25.0.2+10", "25.0.2.0+10-open"}, diff --git a/tests/e2e/linux/e2e_test.go b/tests/e2e/linux/e2e_test.go index df449c2..030322c 100644 --- a/tests/e2e/linux/e2e_test.go +++ b/tests/e2e/linux/e2e_test.go @@ -166,6 +166,7 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c t.Logf("=== vfox %s, java@%s, %s (%s) ===", c.Vfox, common.Spec(c.Distribution, version), platform, machine) setupVfox(ctx, t, ctr, c.Vfox) + checkVersionlessRejected(ctx, t, ctr, c.Distribution) checkMissingVersionRejected(ctx, t, ctr, c.Distribution, version, arch) installed := installJava(ctx, t, ctr, common.Spec(c.Distribution, version)) diff --git a/tests/e2e/linux/setup.go b/tests/e2e/linux/setup.go index dcbee3e..575f72d 100644 --- a/tests/e2e/linux/setup.go +++ b/tests/e2e/linux/setup.go @@ -51,6 +51,26 @@ func setupVfox(ctx context.Context, t *testing.T, ctr tc.Container, vfoxVersion t.Logf("PASS %s with the local java plugin", strings.TrimSpace(execOK(ctx, t, ctr, "vfox --version"))) } +// checkVersionlessRejected proves a distribution without a version (e.g. +// java@tem, java@-tem, see common.VersionlessSpecs) fails both `vfox +// install` and `vfox use` with an error asking for one. The hooks raise it +// before any Foojay lookup, so nothing is retried. +func checkVersionlessRejected(ctx context.Context, t *testing.T, ctr tc.Container, distribution string) { + t.Helper() + for _, spec := range common.VersionlessSpecs(distribution) { + want := "No version in " + spec + ";" + for _, c := range []struct{ label, script string }{ + {"vfox install java@" + spec, "vfox install java@" + spec}, + {"vfox use --global java@" + spec, activated("vfox use --global java@" + spec)}, + } { + code, out := execRun(ctx, t, ctr, c.script) + require.NotZerof(t, code, "%s must fail\n--- output ---\n%s", c.label, out) + require.Containsf(t, common.StripANSI(out), want, "%s must ask for a version", c.label) + } + t.Logf("PASS java@%s is rejected by vfox install and vfox use", spec) + } +} + // checkMissingVersionRejected proves an unpublished release, and an // unpublished build of the pinned one, fail loudly instead of installing // something else: PreInstall finds no Foojay package and names the platform diff --git a/tests/e2e/windows/e2e_test.go b/tests/e2e/windows/e2e_test.go index b0799b1..39edbb1 100644 --- a/tests/e2e/windows/e2e_test.go +++ b/tests/e2e/windows/e2e_test.go @@ -242,9 +242,10 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c box := setupSlotEnv(ctx, t, ctr, c.Slug()) setupVfox(ctx, t, ctr, box, c.Vfox) + activation := fetchActivation(ctx, t, ctr, box) + checkVersionlessRejected(ctx, t, ctr, box, activation, c.Distribution) checkMissingVersionRejected(ctx, t, ctr, box, c.Distribution, version, arch) - activation := fetchActivation(ctx, t, ctr, box) installed := installJava(ctx, t, ctr, box, common.Spec(c.Distribution, version)) verifySearch(ctx, t, ctr, box, c.Distribution, installed) // Before any `vfox use`, so java can only come from the legacy file. diff --git a/tests/e2e/windows/setup.go b/tests/e2e/windows/setup.go index ee19f93..2b7ebcd 100644 --- a/tests/e2e/windows/setup.go +++ b/tests/e2e/windows/setup.go @@ -268,6 +268,27 @@ func activatedEnv(activation, script string) string { "if ($__vfoxExport.Trim()) { Invoke-Expression -Command $__vfoxExport }\r\n" + script } +// checkVersionlessRejected proves a distribution without a version (e.g. +// java@tem, java@-tem, see common.VersionlessSpecs) fails both `vfox +// install` and `vfox use` with an error asking for one. The hooks raise it +// before any Foojay lookup, so nothing is retried. +func checkVersionlessRejected(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, activation, distribution string) { + t.Helper() + for _, spec := range common.VersionlessSpecs(distribution) { + want := "No version in " + spec + ";" + arg := pwshQuote("java@" + spec) + for _, c := range []struct{ label, script string }{ + {"vfox install java@" + spec, "vfox install " + arg}, + {"vfox use --global java@" + spec, activated(activation, "vfox use --global "+arg)}, + } { + code, out := execRun(ctx, t, ctr, c.script, box.vars...) + require.NotZerof(t, code, "%s must fail\n--- output ---\n%s", c.label, out) + require.Containsf(t, common.StripANSI(out), want, "%s must ask for a version", c.label) + } + t.Logf("PASS java@%s is rejected by vfox install and vfox use", spec) + } +} + // checkMissingVersionRejected proves an unpublished release, and an // unpublished build of the pinned one, fail loudly instead of installing // something else: PreInstall finds no Foojay package (zip or tar.gz) and diff --git a/tests/loongnix_test.lua b/tests/loongnix_test.lua index c6bb6b6..ffca722 100644 --- a/tests/loongnix_test.lua +++ b/tests/loongnix_test.lua @@ -82,6 +82,9 @@ end, "No Loongnix JDK") rejects(function() PLUGIN:PreInstall({ version = "21" }) end, "ABI") +rejects(function() + PLUGIN:PreInstall({ version = "lnxabi2-fx" }) +end, "No version in lnxabi2-fx; add one, such as 21-lnxabi2-fx.") rejects(function() PLUGIN:Available({ args = { "tem" } }) end, "Foojay") diff --git a/tests/parse_legacy_file_test.lua b/tests/parse_legacy_file_test.lua index 94b3e5d..6471d52 100644 --- a/tests/parse_legacy_file_test.lua +++ b/tests/parse_legacy_file_test.lua @@ -120,6 +120,13 @@ local cases = { { "java=28-ea+19-zulu-fx\n", { "28-ea+19-zulu", "28-ea+19-zulu-fx" }, "28-ea+19-zulu-fx" }, { "java=26-ea+13-graal\n", { "26-ea+2-graal", "26-ea+13-graal" }, "26-ea+13-graal" }, { "java=28-ea+20\n", { "28-ea+18", "28-ea+19" }, nil }, + -- A distribution without a version selects nothing, as vfox use rejects it + -- (vfox-java#98), where parse_version failed with a Lua error. + { "java=tem\n", { "21.0.8+9-tem", "21.0.12+8-tem" }, nil }, + { "java=temurin-\n", { "21.0.8+9-tem" }, nil }, + { "java=-tem\n", { "21.0.8+9-tem" }, nil }, + { "java=zulu-fx\n", { "21.0.12+7-zulu-fx" }, nil }, + { "java=open\n", { "25.0.2+10" }, nil }, -- No java line at all selects nothing. { "maven=3.9.9\n", { "21.0.8+9-tem" }, nil }, } diff --git a/tests/pre_install_test.lua b/tests/pre_install_test.lua index 8844fe9..8aa60b9 100644 --- a/tests/pre_install_test.lua +++ b/tests/pre_install_test.lua @@ -204,4 +204,28 @@ assert( ) RUNTIME.osType = "linux" +-- A distribution without a version, alone or after an empty one, asks for a +-- version before any lookup (vfox-java#98). Alone it failed with a Lua error; +-- the empty version reached Foojay, which listed every release, and the newest +-- one installed. Once pcall has caught an error, gopher-lua's http stub keeps +-- the requests table it saw, so it is emptied in place. +for _, case in ipairs({ + { + "tem", + "No version in tem; add one, such as 21-tem. Please check available versions with 'vfox search java tem'", + }, + { "temurin", "No version in temurin; add one, such as 21-tem." }, + { "-tem", "No version in -tem; add one, such as 21-tem." }, + { "temurin-", "No version in temurin-; add one, such as 21-tem." }, + { "zulu-fx", "such as 21-zulu-fx. Please check available versions with 'vfox search java zulu'" }, + { "openjdk", "such as 21-open. Please check available versions with 'vfox search java open'" }, +}) do + for i = #requests, 1, -1 do + requests[i] = nil + end + local ok, err = pcall(PLUGIN.PreInstall, PLUGIN, { version = case[1] }) + assert(not ok and tostring(err):find(case[2], 1, true), case[1] .. ": unexpected " .. tostring(err)) + assert(#requests == 0, case[1] .. ": unexpected lookup " .. tostring(requests[1])) +end + print("PASS: " .. cases .. " PreInstall Foojay lookup cases") diff --git a/tests/pre_use_test.lua b/tests/pre_use_test.lua index f79526d..76ace00 100644 --- a/tests/pre_use_test.lua +++ b/tests/pre_use_test.lua @@ -121,4 +121,14 @@ for _, case in ipairs(cases) do local version = result and result.version assert(version == case[2], case[1] .. ": expected " .. tostring(case[2]) .. ", got " .. tostring(version)) end -print("PASS: " .. #cases .. " PreUse regression cases") + +-- A distribution without a version names no installed version, so PreUse asks +-- for one, as PreInstall does (vfox-java#98). Alone it failed with a Lua error; +-- after an empty version, vfox reported -tem not installed. +local rejected = { "tem", "temurin", "-tem", "temurin-", "zulu-fx", "open", "lnxabi2-fx" } +for _, version in ipairs(rejected) do + local ok, err = pcall(PLUGIN.PreUse, PLUGIN, { version = version, installedSdks = installed }) + local message = "No version in " .. version .. "; add one" + assert(not ok and tostring(err):find(message, 1, true), version .. ": unexpected " .. tostring(err)) +end +print("PASS: " .. #cases + #rejected .. " PreUse regression cases")