diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e2cb467..c0d4991 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -38,8 +38,7 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v7 - # TODO For compiling moby only. - # Because https://github.com/actions/runner-images/blob/win11-arm64/20260920.174/images/windows/Windows11-Arm64-Readme.md is still using `Go 1.24.13`. + # TODO For compiling moby only. Because https://github.com/actions/runner-images/blob/win11-arm64/20260920.174/images/windows/Windows11-Arm64-Readme.md is still using `Go 1.24.13`. - if: matrix.runs-on == 'windows-11-arm' uses: actions/setup-go@v7 with: @@ -51,7 +50,7 @@ jobs: name: Install Compose CLI plugin run: | New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.docker\cli-plugins" | Out-Null - curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors "https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-windows-aarch64.exe" -o "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe" + curl.exe -fsSL "https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-windows-aarch64.exe" -o "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe" docker compose version - run: docker compose -f tests/e2e/windows/compose.yaml run --rm --build e2e env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 75d5fdc..53832dc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,13 +9,11 @@ permissions: contents: read jobs: - lint: + lint-go: if: github.repository == 'version-fox/vfox-flutter' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - with: - persist-credentials: false - uses: actions/setup-go@v6 with: go-version: '1.26.8' @@ -26,3 +24,18 @@ jobs: - name: Run go vet run: go vet ./... working-directory: tests/e2e + + lint-lua: + if: github.repository == 'version-fox/vfox-flutter' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: jdx/mise-action@v5 + - name: Check Lua formatting + uses: JohnnyMorganz/stylua-action@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: v2.5.2 + args: --check . + - name: Check Lua + run: mise x aqua:LuaLS/lua-language-server@3.19.1 -- lua-language-server --check . --checklevel=Warning diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 10f2b99..c07c460 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - persist-credentials: false - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.24.0' diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..0a05bb1 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "Lua.runtime.version": "Lua 5.1", + "Lua.diagnostics.globals": ["PLUGIN", "RUNTIME"], + "Lua.diagnostics.disable": ["duplicate-set-field"] +} diff --git a/Injection.lua b/Injection.lua index 798d95a..533f8c5 100644 --- a/Injection.lua +++ b/Injection.lua @@ -12,4 +12,3 @@ RUNTIME = { --- vfox runtime version version = "", } - diff --git a/hooks/available.lua b/hooks/available.lua index ed60371..8e77166 100644 --- a/hooks/available.lua +++ b/hooks/available.lua @@ -42,9 +42,9 @@ function PLUGIN:Available(ctx) addition = { { name = "dart", - version = info.dart_sdk_version - } - } + version = info.dart_sdk_version, + }, + }, }) end for _, info in ipairs(ohos.list()) do diff --git a/hooks/env_keys.lua b/hooks/env_keys.lua index 6c4fb40..3a62cb8 100644 --- a/hooks/env_keys.lua +++ b/hooks/env_keys.lua @@ -3,7 +3,7 @@ function PLUGIN:EnvKeys(ctx) return { { key = "PATH", - value = mainPath .. "/bin" - } + value = mainPath .. "/bin", + }, } -end \ No newline at end of file +end diff --git a/hooks/pre_install.lua b/hooks/pre_install.lua index 2c963ac..1a47e29 100644 --- a/hooks/pre_install.lua +++ b/hooks/pre_install.lua @@ -16,7 +16,7 @@ function PLUGIN:PreInstall(ctx) local channelKey if arg == "beta" or arg == "dev" or arg == "stable" then local resp, err = http.get({ - url = getReleasesUrl(platform.osType) + url = getReleasesUrl(platform.osType), }) if err ~= nil or resp.status_code ~= 200 then error("get version failed: " .. tostring(err) .. " (status " .. tostring(resp and resp.status_code) .. ")") @@ -36,15 +36,14 @@ function PLUGIN:PreInstall(ctx) return { version = versionName, url = info.url, - sha256 = info.sha256 + sha256 = info.sha256, } end local legacy for _, info in ipairs(self:Available({})) do local version, arch = splitVersionAndArch(info.version) - local matches = channelKey and info.key == channelKey or - (channelKey == nil and version == arg) + local matches = channelKey and info.key == channelKey or (channelKey == nil and version == arg) if matches then if arch == targetArch then return package(info, version) @@ -65,6 +64,7 @@ function PLUGIN:PostInstall(ctx) end if sdk.path ~= nil and sdk.path ~= "" then pcall(manifest.write, sdk.path, sdk) + pcall(manifest.touchPubspecLock, sdk.path) end if ohos.isOhosVersion(sdk.version) then ohos.clean(sdk.version) diff --git a/hooks/pre_use.lua b/hooks/pre_use.lua index 5cf4817..280c973 100644 --- a/hooks/pre_use.lua +++ b/hooks/pre_use.lua @@ -2,14 +2,15 @@ local manifest = require("manifest") local REASON = { ["head-drifted"] = "the SDK's git HEAD no longer matches the commit vfox installed", - ["sdk-no-longer-git"] = "vfox can no longer read the SDK's git HEAD (its .git may be broken)" + ["sdk-no-longer-git"] = "vfox can no longer read the SDK's git HEAD (its .git may be broken)", } local function printDriftWarning(sdkPath, version, manifestInfo, currentHead, reason) local installedVersion = (manifestInfo and manifestInfo.version) or version local expectedHead = (manifestInfo and manifestInfo.expected_head) or "unknown" - io.write(string.format( - [[ + io.write( + string.format( + [[ Warning: flutter SDK at %s has drifted from the version vfox installed. vfox installed: flutter@%s (git HEAD %s) current git HEAD: %s @@ -29,16 +30,17 @@ To upgrade to a newer version, use vfox directly: vfox install flutter@ vfox use flutter@ ]], - sdkPath, - installedVersion, - expectedHead, - currentHead or "unknown", - REASON[reason] or reason, - version, - version, - version, - version - )) + sdkPath, + installedVersion, + expectedHead, + currentHead or "unknown", + REASON[reason] or reason, + version, + version, + version, + version + ) + ) end function PLUGIN:PreUse(ctx) diff --git a/lib/manifest.lua b/lib/manifest.lua index 841d0fc..54c7710 100644 --- a/lib/manifest.lua +++ b/lib/manifest.lua @@ -60,10 +60,7 @@ local function hasGitDir(path) end local function isEmptyTable(table) - for _ in pairs(table) do - return false - end - return true + return next(table) == nil end local M = {} @@ -179,4 +176,44 @@ function M.checkDrift(sdkRoot) return false, "ok" end +-- vfox extracts archives with Go's archive/tar (see +-- internal/shared/util/decompressor.go), which creates files with the +-- extraction time instead of the mtime stored in the archive. The official +-- `tar -xf` flow preserves mtimes, where +-- packages/flutter_tools/pubspec.lock is newer than pubspec.yaml. After a +-- vfox install the two files share the extraction time and whichever was +-- extracted last (currently pubspec.yaml) looks newer, so +-- bin/internal/shared.sh's `upgrade_flutter` sees +-- `pubspec.yaml -nt pubspec.lock` and rebuilds the Flutter tool on first +-- run (`Building flutter tool...`), deleting `version` and +-- `bin/cache/flutter.version.json` and forcing a `fetch --tags` version +-- lookup. Rewriting pubspec.lock bumps its mtime past pubspec.yaml and +-- restores the official first-run behavior (no rebuild). +function M.touchPubspecLock(sdkRoot) + local root = M.gitRoot(sdkRoot) + if root == nil then + if type(sdkRoot) ~= "string" or sdkRoot == "" then + return false + end + root = sdkRoot + end + local lockPath = joinPath(joinPath(joinPath(root, "packages"), "flutter_tools"), "pubspec.lock") + local f = io.open(lockPath, "rb") + if f == nil then + return false + end + local content = f:read("*a") + f:close() + if content == nil then + return false + end + local w = io.open(lockPath, "wb") + if w == nil then + return false + end + w:write(content) + w:close() + return true +end + return M diff --git a/lib/ohos.lua b/lib/ohos.lua index 4d92851..d82cbf5 100644 --- a/lib/ohos.lua +++ b/lib/ohos.lua @@ -59,7 +59,7 @@ function M.list() version = version, url = CLONE_URL:format(REPO), key = version, - note = NOTE + note = NOTE, }) end end @@ -73,8 +73,7 @@ function M.checkout(version, requestedArch) local commit local body, err = releases() if body == nil then - error("OpenHarmony releases are unavailable from " .. RELEASES_URL:format(REPO) - .. ": " .. tostring(err)) + error("OpenHarmony releases are unavailable from " .. RELEASES_URL:format(REPO) .. ": " .. tostring(err)) end for _, info in ipairs(body) do if info.tag_name == version then @@ -106,7 +105,7 @@ function M.checkout(version, requestedArch) return { version = version, url = dir, - note = NOTE + note = NOTE, } end diff --git a/lib/source.lua b/lib/source.lua index ed1ac37..25519a8 100644 --- a/lib/source.lua +++ b/lib/source.lua @@ -21,8 +21,7 @@ end function M.list(releases) local result = {} for _, info in ipairs(releases or {}) do - if type(info.version) == "string" and string.sub(info.version, 1, 1) ~= "v" - and info.dart_sdk_arch == "x64" then + if type(info.version) == "string" and string.sub(info.version, 1, 1) ~= "v" and info.dart_sdk_arch == "x64" then table.insert(result, { version = info.version .. "-arm64", url = M.repoUrl(), @@ -32,9 +31,9 @@ function M.list(releases) addition = { { name = "dart", - version = info.dart_sdk_version - } - } + version = info.dart_sdk_version, + }, + }, }) end end @@ -73,7 +72,7 @@ function M.checkout(baseVersion, versionName) end return { version = versionName, - url = dir + url = dir, } end diff --git a/lib/util.lua b/lib/util.lua index 0137d4c..7dff8a4 100644 --- a/lib/util.lua +++ b/lib/util.lua @@ -20,7 +20,6 @@ function getReleasesUrl(osType) return getStorageBaseUrl() .. "/flutter_infra_release/releases/releases_" .. osType .. ".json" end - function getOsTypeAndArch() local osType = RUNTIME.osType local archType = RUNTIME.archType @@ -35,7 +34,8 @@ function getOsTypeAndArch() error("flutter does not support" .. archType .. "architecture") end return { - osType = osType, archType = archType + osType = osType, + archType = archType, } end diff --git a/metadata.lua b/metadata.lua index 5a6a01e..3169e3a 100644 --- a/metadata.lua +++ b/metadata.lua @@ -13,7 +13,6 @@ PLUGIN.license = "Apache 2.0" --- Plugin description PLUGIN.description = "Flutter plugin, https://flutter.dev" - --- !!! OPTIONAL !!! --[[ NOTE: diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..c537618 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,5 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 4 +quote_style = "AutoPreferDouble" diff --git a/tests/e2e/common/retry.go b/tests/e2e/common/retry.go new file mode 100644 index 0000000..7599fa5 --- /dev/null +++ b/tests/e2e/common/retry.go @@ -0,0 +1,77 @@ +// Retry helpers shared by the Linux and Windows container suites. +// +// Both suites shell out into throwaway containers where downloads and +// daemon calls flake, so every network-touching exec goes through Retry: +// the old lib.sh rhythm (linear 10s*attempt backoff, per-attempt logging, +// context-aware waits), now driven by github.com/cenkalti/backoff/v7 +// instead of a hand-rolled loop. Only the backoff schedule itself stays +// local (linearAttemptBackOff): no third-party policy reproduces the exact +// historical 10s/20s/... cadence the suite's timeouts were tuned against. +package common + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/cenkalti/backoff/v7" +) + +// linearAttemptBackOff waits 10s after the first failure, 20s after the +// second, and so on. It exists only to preserve the suite's historical +// cadence; the retry loop, logging fan-out and context handling come from +// the backoff library. +type linearAttemptBackOff struct { + attempt int +} + +func (b *linearAttemptBackOff) NextBackOff() time.Duration { + b.attempt++ + return time.Duration(10*b.attempt) * time.Second +} + +func (b *linearAttemptBackOff) Reset() { + b.attempt = 0 +} + +// Retry runs run until it reports exit code 0, giving up after +// RetryAttempts() attempts. It returns the successful run's output and +// fatals (with the last output attached) once the budget is exhausted, +// exactly like the loop it replaces. +func Retry(ctx context.Context, t *testing.T, label string, run func() (code int, output string)) string { + t.Helper() + max := RetryAttempts() + if max < 1 { + max = 1 + } + var out string + var code, attempts int + notify := func(_ error, _ time.Duration) { + t.Logf("retrying %s, attempt %d exited %d", label, attempts, code) + t.Logf("--- output ---\n%s", out) + } + result, err := backoff.Retry(ctx, func() (string, error) { + attempts++ + code, out = run() + if code == 0 { + return out, nil + } + return "", fmt.Errorf("%s: attempt %d exited %d", label, attempts, code) + }, + backoff.WithBackOff(&linearAttemptBackOff{}), + backoff.WithMaxTries(uint(max)), + // Bound retries by attempt count and the caller's context only; + // the library's default 15-minute elapsed budget must not cut + // long Flutter installs short. + backoff.WithMaxElapsedTime(0), + backoff.WithNotify(notify), + ) + if err != nil { + if ctx.Err() != nil { + t.Fatalf("%s: %v", label, ctx.Err()) + } + t.Fatalf("%s failed after %d attempt(s) (exit %d)\n--- output ---\n%s", label, attempts, code, out) + } + return result +} diff --git a/tests/e2e/common/retry_test.go b/tests/e2e/common/retry_test.go new file mode 100644 index 0000000..52e5e4f --- /dev/null +++ b/tests/e2e/common/retry_test.go @@ -0,0 +1,53 @@ +package common + +import ( + "context" + "testing" + "time" +) + +// The linear schedule is the one behavior the suite pins down: 10s, 20s, +// ... . Everything else (loop, logging, cancellation) belongs to the +// backoff library. +func TestLinearAttemptBackOffSchedule(t *testing.T) { + b := &linearAttemptBackOff{} + for i, want := range []time.Duration{10 * time.Second, 20 * time.Second, 30 * time.Second} { + if got := b.NextBackOff(); got != want { + t.Fatalf("attempt %d backoff = %v, want %v", i+1, got, want) + } + } + b.Reset() + if got := b.NextBackOff(); got != 10*time.Second { + t.Fatalf("after Reset backoff = %v, want 10s", got) + } +} + +// Immediate success passes the output through with a single run. +func TestRetryImmediateSuccess(t *testing.T) { + t.Setenv("E2E_RETRY_ATTEMPTS", "") + calls := 0 + out := Retry(context.Background(), t, "immediate", func() (int, string) { + calls++ + return 0, "ok" + }) + if out != "ok" || calls != 1 { + t.Fatalf("out = %q after %d calls, want %q after 1", out, calls, "ok") + } +} + +// One failure then success exercises the library round trip (notify + +// backoff wait + second attempt) end to end; it takes ~10s of real wait. +func TestRetryRecoversAfterFailure(t *testing.T) { + t.Setenv("E2E_RETRY_ATTEMPTS", "") + calls := 0 + out := Retry(context.Background(), t, "recover", func() (int, string) { + calls++ + if calls == 1 { + return 1, "first failed" + } + return 0, "second ok" + }) + if out != "second ok" || calls != 2 { + t.Fatalf("out = %q after %d calls, want %q after 2", out, calls, "second ok") + } +} diff --git a/tests/e2e/go.mod b/tests/e2e/go.mod index 6c10c03..d0a0af5 100644 --- a/tests/e2e/go.mod +++ b/tests/e2e/go.mod @@ -3,6 +3,7 @@ module github.com/version-fox/vfox-flutter/tests/e2e go 1.25.0 require ( + github.com/cenkalti/backoff/v7 v7.0.1 github.com/containerd/platforms v0.2.1 github.com/moby/moby/client v0.5.0 github.com/opencontainers/image-spec v1.1.1 diff --git a/tests/e2e/go.sum b/tests/e2e/go.sum index 5001fb9..d529f76 100644 --- a/tests/e2e/go.sum +++ b/tests/e2e/go.sum @@ -8,6 +8,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v7 v7.0.1 h1:+ulHti5s1d9QnfyOnLWVmQ/SaZ+TJQF9OdOI0i9BGZI= +github.com/cenkalti/backoff/v7 v7.0.1/go.mod h1:qcKBGwsu4hpxHtQ8tWYsQ+ifzx2+sS+Xx/3jfe30lI8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= diff --git a/tests/e2e/linux/e2e_test.go b/tests/e2e/linux/e2e_test.go index 996d09b..f392319 100644 --- a/tests/e2e/linux/e2e_test.go +++ b/tests/e2e/linux/e2e_test.go @@ -166,5 +166,6 @@ func runOne(t *testing.T, ctx context.Context, image, platform string, c common. sdk := sdkDir(ctx, t, ctr) verifySdkLayout(ctx, t, ctr, flavor, sdk) verifyManifestDrift(ctx, t, ctr, version, sdk) + verifyFirstRunMatchesOfficial(ctx, t, ctr, flavor, sdk) verifyToolchain(ctx, t, ctr, sdk) } diff --git a/tests/e2e/linux/exec.go b/tests/e2e/linux/exec.go index 3f0e593..7c2fa54 100644 --- a/tests/e2e/linux/exec.go +++ b/tests/e2e/linux/exec.go @@ -4,7 +4,6 @@ import ( "context" "io" "testing" - "time" tc "github.com/testcontainers/testcontainers-go" tcexec "github.com/testcontainers/testcontainers-go/exec" @@ -42,31 +41,14 @@ func execOK(ctx context.Context, t *testing.T, ctr tc.Container, script string, return out } -// execRetry keeps the old lib.sh retry semantics (linear 10s*attempt backoff, -// per-attempt logging) with the standard library only: the established retry -// libraries (cenkalti/backoff, avast/retry-go) have no release in the last -// 6 months, so no third-party candidate qualifies. +// execRetry runs script through the shared common.Retry helper (linear +// 10s*attempt backoff, per-attempt logging), preserving the old lib.sh +// retry semantics without duplicating the loop per platform. func execRetry(ctx context.Context, t *testing.T, ctr tc.Container, label, script string, extraEnv ...string) string { t.Helper() - max := common.RetryAttempts() - var out string - for attempt := 1; ; attempt++ { - code, o := execRun(ctx, t, ctr, script, extraEnv...) - out = o - if code == 0 { - return out - } - if attempt >= max { - t.Fatalf("%s failed after %d attempt(s) (exit %d)\n--- output ---\n%s", label, attempt, code, out) - } - t.Logf("retrying %s, attempt %d exited %d", label, attempt, code) - t.Logf("--- output ---\n%s", out) - select { - case <-ctx.Done(): - t.Fatalf("%s: %v", label, ctx.Err()) - case <-time.After(time.Duration(10*attempt) * time.Second): - } - } + return common.Retry(ctx, t, label, func() (int, string) { + return execRun(ctx, t, ctr, script, extraEnv...) + }) } // activated prefixes script with the vfox env setup, the equivalent of the diff --git a/tests/e2e/linux/verify.go b/tests/e2e/linux/verify.go index 597b351..d69d82a 100644 --- a/tests/e2e/linux/verify.go +++ b/tests/e2e/linux/verify.go @@ -94,6 +94,28 @@ func verifyManifestDrift(ctx context.Context, t *testing.T, ctr tc.Container, ve require.NotContains(t, use(), "has drifted", "a restored SDK reports no drift") } +// verifyFirstRunMatchesOfficial proves a freshly installed archive SDK +// behaves like the official tarball flow from issue #37: the first flutter +// invocation must not rebuild the tool (`Building flutter tool...`). +// Source installs (no prebuilt flutter_tools.snapshot, e.g. linux/arm64) +// bootstrap on first run by design, so they skip the rebuild assertion; +// other flavors skip entirely. Must run as the first flutter invocation: +// verifyToolchain boots the toolchain afterwards. +func verifyFirstRunMatchesOfficial(ctx context.Context, t *testing.T, ctr tc.Container, flavor, sdk string) { + t.Helper() + if flavor != "official" { + return + } + if code, _ := execRun(ctx, t, ctr, fmt.Sprintf("test -f %q/bin/cache/flutter_tools.snapshot", sdk)); code != 0 { + t.Log("SKIP first-run rebuild check: no prebuilt flutter_tools.snapshot (source install)") + return + } + out := execRetry(ctx, t, ctr, "flutter --disable-analytics", activated("flutter --disable-analytics")) + require.Contains(t, out, "Analytics reporting disabled", "flutter --disable-analytics") + require.NotContains(t, out, "Building flutter tool", "a fresh archive SDK must not rebuild the Flutter tool (issue #37)") + t.Log("PASS the first flutter run matches the official install (no tool rebuild)") +} + // verifyToolchain boots the toolchain (pub.dev reachable) and checks the // dart/flutter versions agree with each other and the SDK checkout. func verifyToolchain(ctx context.Context, t *testing.T, ctr tc.Container, sdk string) { diff --git a/tests/e2e/windows/exec.go b/tests/e2e/windows/exec.go index 9fb4527..26b119b 100644 --- a/tests/e2e/windows/exec.go +++ b/tests/e2e/windows/exec.go @@ -4,7 +4,6 @@ import ( "context" "io" "testing" - "time" tc "github.com/testcontainers/testcontainers-go" tcexec "github.com/testcontainers/testcontainers-go/exec" @@ -48,27 +47,12 @@ func execOK(ctx context.Context, t *testing.T, ctr tc.Container, script string, return out } -// execRetry mirrors Invoke-WithRetry: run script up to common.RetryAttempts -// times with linear backoff (10s * attempt), logging each failed attempt. +// execRetry runs script through the shared common.Retry helper (linear +// 10s*attempt backoff, per-attempt logging), preserving the old +// Invoke-WithRetry semantics without duplicating the loop per platform. func execRetry(ctx context.Context, t *testing.T, ctr tc.Container, label, script string, extraEnv ...string) string { t.Helper() - max := common.RetryAttempts() - var out string - for attempt := 1; ; attempt++ { - code, o := execRun(ctx, t, ctr, script, extraEnv...) - out = o - if code == 0 { - return out - } - if attempt >= max { - t.Fatalf("%s failed after %d attempt(s) (exit %d)\n--- output ---\n%s", label, attempt, code, out) - } - t.Logf("retrying %s, attempt %d exited %d", label, attempt, code) - t.Logf("--- output ---\n%s", out) - select { - case <-ctx.Done(): - t.Fatalf("%s: %v", label, ctx.Err()) - case <-time.After(time.Duration(10*attempt) * time.Second): - } - } + return common.Retry(ctx, t, label, func() (int, string) { + return execRun(ctx, t, ctr, script, extraEnv...) + }) } diff --git a/tests/e2e/windows/verify.go b/tests/e2e/windows/verify.go index 33310fc..810cece 100644 --- a/tests/e2e/windows/verify.go +++ b/tests/e2e/windows/verify.go @@ -114,6 +114,13 @@ func verifyManifestDrift(ctx context.Context, t *testing.T, ctr tc.Container, bo require.NotContains(t, use(), "has drifted", "a restored SDK reports no drift") } +// NOTE: there is deliberately no first-run rebuild check on Windows. +// Issue #37 is Linux-specific: the Linux tarball ships a flutter_tools.stamp +// matching its git HEAD, so the official first run skips the tool rebuild. +// The Windows release zip instead ships a stale stamp (e.g. 3.47.5 carries +// "fa78ae3..." while its HEAD is 6a19cca...), so the official Windows +// first run always rebuilds too and vfox already matches it. + // verifyToolchain boots the toolchain (pub.dev reachable) and checks the // dart/flutter versions agree with each other and the SDK checkout. func verifyToolchain(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, activation, sdk string) { diff --git a/tests/hooks_test.lua b/tests/hooks_test.lua index b8dbf1c..3fba29e 100644 --- a/tests/hooks_test.lua +++ b/tests/hooks_test.lua @@ -3,17 +3,25 @@ package.path = "./lib/?.lua;" .. package.path local fixture, requests, mirror local originalGetenv = os.getenv os.getenv = function(name) - if name == "FLUTTER_STORAGE_BASE_URL" then return mirror end + if name == "FLUTTER_STORAGE_BASE_URL" then + return mirror + end return originalGetenv(name) end package.preload.http = function() - return { get = function(request) - table.insert(requests, request.url) - return { status_code = 200, body = "fixture" }, nil - end } + return { + get = function(request) + table.insert(requests, request.url) + return { status_code = 200, body = "fixture" }, nil + end, + } end package.preload.json = function() - return { decode = function() return fixture end } + return { + decode = function() + return fixture + end, + } end local function equal(actual, expected) @@ -22,9 +30,13 @@ end local function release(version, arch, channel, hash) return { - version = version, dart_sdk_arch = arch, channel = channel, hash = hash, + version = version, + dart_sdk_arch = arch, + channel = channel, + hash = hash, archive = "sdk/" .. version .. "-" .. (arch or "legacy") .. ".zip", - sha256 = version .. ":" .. (arch or "legacy"), dart_sdk_version = "3.12.0" + sha256 = version .. ":" .. (arch or "legacy"), + dart_sdk_version = "3.12.0", } end @@ -43,8 +55,8 @@ local function setup(osType, archType, storage) release("3.44.0-0.1.pre", "arm64", "beta", "beta"), release("3.43.0-0.1.pre", "x64", "dev", "dev"), release("3.43.0-0.1.pre", "arm64", "dev", "dev"), - release("2.10.0", nil, "stable", "legacy") - } + release("2.10.0", nil, "stable", "legacy"), + }, } package.loaded.util = nil dofile("metadata.lua") @@ -62,79 +74,111 @@ local function installed(request, version, arch) end local tests = {} -tests[#tests + 1] = { "all variants are listed with native architecture first", function() - for _, arch in ipairs({ "arm64", "amd64" }) do - setup("darwin", arch) - local versions = PLUGIN:Available({}) - equal(#versions, 7) - equal(versions[1].version, "3.44.0-" .. (arch == "amd64" and "x64" or arch)) - local found = {} - for _, version in ipairs(versions) do found[version.version] = true end - assert(found["3.44.0-x64"] and found["3.44.0-arm64"] and found["2.10.0"]) - equal(versions[1].addition[1].version, "3.12.0") - end -end } -tests[#tests + 1] = { "explicit architectures select matching archives", function() - setup("darwin", "arm64") - installed("3.44.0-x64", "3.44.0-x64", "x64") - installed("3.44.0-arm64", "3.44.0-arm64", "arm64") -end } -tests[#tests + 1] = { "latest keeps the default architecture when a foreign build is newer", function() - setup("darwin", "arm64") - table.insert(fixture.releases, 1, release("3.45.0", "x64", "beta", "newer")) - local versions = PLUGIN:Available({}) - equal(versions[1].version, "3.44.0-arm64") - installed(versions[1].version, "3.44.0-arm64", "arm64") -end } -tests[#tests + 1] = { "existing version commands retain native selection and version names", function() - for _, platform in ipairs({ "darwin", "linux", "windows" }) do +tests[#tests + 1] = { + "all variants are listed with native architecture first", + function() for _, arch in ipairs({ "arm64", "amd64" }) do - setup(platform, arch) - installed("3.44.0", "3.44.0", arch == "amd64" and "x64" or arch) + setup("darwin", arch) + local versions = PLUGIN:Available({}) + equal(#versions, 7) + equal(versions[1].version, "3.44.0-" .. (arch == "amd64" and "x64" or arch)) + local found = {} + for _, version in ipairs(versions) do + found[version.version] = true + end + assert(found["3.44.0-x64"] and found["3.44.0-arm64"] and found["2.10.0"]) + equal(versions[1].addition[1].version, "3.12.0") end - end -end } -tests[#tests + 1] = { "channels use the native architecture even when hashes are shared", function() - for _, arch in ipairs({ "arm64", "amd64" }) do - setup("darwin", arch) - for channel, version in pairs({ stable = "3.44.0", beta = "3.44.0-0.1.pre", dev = "3.43.0-0.1.pre" }) do - installed(channel, version, arch == "amd64" and "x64" or arch) - installed(channel .. "-x64", version .. "-x64", "x64") - installed(channel .. "-arm64", version .. "-arm64", "arm64") + end, +} +tests[#tests + 1] = { + "explicit architectures select matching archives", + function() + setup("darwin", "arm64") + installed("3.44.0-x64", "3.44.0-x64", "x64") + installed("3.44.0-arm64", "3.44.0-arm64", "arm64") + end, +} +tests[#tests + 1] = { + "latest keeps the default architecture when a foreign build is newer", + function() + setup("darwin", "arm64") + table.insert(fixture.releases, 1, release("3.45.0", "x64", "beta", "newer")) + local versions = PLUGIN:Available({}) + equal(versions[1].version, "3.44.0-arm64") + installed(versions[1].version, "3.44.0-arm64", "arm64") + end, +} +tests[#tests + 1] = { + "existing version commands retain native selection and version names", + function() + for _, platform in ipairs({ "darwin", "linux", "windows" }) do + for _, arch in ipairs({ "arm64", "amd64" }) do + setup(platform, arch) + installed("3.44.0", "3.44.0", arch == "amd64" and "x64" or arch) + end end - end -end } -tests[#tests + 1] = { "pre-release versions retain their full version strings", function() - setup("darwin", "arm64") - installed("3.44.0-0.1.pre", "3.44.0-0.1.pre", "arm64") - installed("3.44.0-0.1.pre-x64", "3.44.0-0.1.pre-x64", "x64") -end } -tests[#tests + 1] = { "architecture suffixes do not change version ordering", function() - setup("darwin", "arm64") - equal(compare_versions("3.44.0-arm64", "3.44.0-x64"), 0) - equal(compare_versions("3.44.0", "3.44.0-arm64"), 0) - assert(compare_versions("3.44.0-arm64", "3.44.0-0.1.pre-x64") > 0) - assert(compare_versions("3.44.0-0.2.pre-x64", "3.44.0-0.1.pre-arm64") > 0) -end } -tests[#tests + 1] = { "mirror is used for index and every architecture's archive", function() - setup("darwin", "arm64", "https://mirror.example/flutter/") - local result = installed("3.44.0-x64", "3.44.0-x64", "x64") - equal(requests[1], "https://mirror.example/flutter/flutter_infra_release/releases/releases_macos.json") - equal(result.url, "https://mirror.example/flutter/flutter_infra_release/releases/sdk/3.44.0-x64.zip") -end } -tests[#tests + 1] = { "legacy releases remain installable without inventing architecture variants", function() - setup("darwin", "arm64") - installed("2.10.0", "2.10.0", "legacy") - equal(PLUGIN:PreInstall({ version = "2.10.0-arm64" }), nil) -end } -tests[#tests + 1] = { "missing architecture never falls back to an incompatible archive", function() - setup("darwin", "arm64") - fixture.releases = { release("3.44.0", "x64", "stable", "stable") } - equal(PLUGIN:PreInstall({ version = "3.44.0" }), nil) - equal(PLUGIN:PreInstall({ version = "stable" }), nil) - equal(PLUGIN:PreInstall({ version = "stable-arm64" }), nil) - installed("stable-x64", "3.44.0-x64", "x64") -end } + end, +} +tests[#tests + 1] = { + "channels use the native architecture even when hashes are shared", + function() + for _, arch in ipairs({ "arm64", "amd64" }) do + setup("darwin", arch) + for channel, version in pairs({ stable = "3.44.0", beta = "3.44.0-0.1.pre", dev = "3.43.0-0.1.pre" }) do + installed(channel, version, arch == "amd64" and "x64" or arch) + installed(channel .. "-x64", version .. "-x64", "x64") + installed(channel .. "-arm64", version .. "-arm64", "arm64") + end + end + end, +} +tests[#tests + 1] = { + "pre-release versions retain their full version strings", + function() + setup("darwin", "arm64") + installed("3.44.0-0.1.pre", "3.44.0-0.1.pre", "arm64") + installed("3.44.0-0.1.pre-x64", "3.44.0-0.1.pre-x64", "x64") + end, +} +tests[#tests + 1] = { + "architecture suffixes do not change version ordering", + function() + setup("darwin", "arm64") + equal(compare_versions("3.44.0-arm64", "3.44.0-x64"), 0) + equal(compare_versions("3.44.0", "3.44.0-arm64"), 0) + assert(compare_versions("3.44.0-arm64", "3.44.0-0.1.pre-x64") > 0) + assert(compare_versions("3.44.0-0.2.pre-x64", "3.44.0-0.1.pre-arm64") > 0) + end, +} +tests[#tests + 1] = { + "mirror is used for index and every architecture's archive", + function() + setup("darwin", "arm64", "https://mirror.example/flutter/") + local result = installed("3.44.0-x64", "3.44.0-x64", "x64") + equal(requests[1], "https://mirror.example/flutter/flutter_infra_release/releases/releases_macos.json") + equal(result.url, "https://mirror.example/flutter/flutter_infra_release/releases/sdk/3.44.0-x64.zip") + end, +} +tests[#tests + 1] = { + "legacy releases remain installable without inventing architecture variants", + function() + setup("darwin", "arm64") + installed("2.10.0", "2.10.0", "legacy") + equal(PLUGIN:PreInstall({ version = "2.10.0-arm64" }), nil) + end, +} +tests[#tests + 1] = { + "missing architecture never falls back to an incompatible archive", + function() + setup("darwin", "arm64") + fixture.releases = { release("3.44.0", "x64", "stable", "stable") } + equal(PLUGIN:PreInstall({ version = "3.44.0" }), nil) + equal(PLUGIN:PreInstall({ version = "stable" }), nil) + equal(PLUGIN:PreInstall({ version = "stable-arm64" }), nil) + installed("stable-x64", "3.44.0-x64", "x64") + end, +} local failures = 0 for _, test in ipairs(tests) do