diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 48d0159..e2cb467 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -24,9 +24,6 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v7 - # No host Go anywhere: the suite runs from the Compose runner service - # driving the runner daemon through the mounted socket (same invocation - # as docs/e2e.md), so CI dogfoods the documented setup. - run: docker compose -f tests/e2e/linux/compose.yaml run --rm e2e e2e-windows: @@ -41,9 +38,6 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v7 - # Host Go on windows-11-arm is for compiling moby only - # (install-docker-arm64.ps1); the suite itself runs from the Compose - # runner service with no host Go anywhere. # 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' @@ -53,18 +47,12 @@ jobs: cache: false - if: matrix.runs-on == 'windows-11-arm' run: pwsh -NoProfile -File tests/e2e/windows/install-docker-arm64.ps1 - # windows-latest ships Docker Compose; windows-11-arm builds its engine - # from source with no plugin, so pin one explicitly there. - if: matrix.runs-on == 'windows-11-arm' - name: Install Compose v5.5.1 CLI plugin + 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" docker compose version - # Both runners drive the daemon from the Compose runner service - # (Dockerfile-runner: servercore plus the pinned Go MSI, built for the - # host arch): no host Go is needed by the suite on any arch. Host Go on - # windows-11-arm exists only to compile moby above. - run: docker compose -f tests/e2e/windows/compose.yaml run --rm --build e2e env: ARCH: ${{ contains(matrix.runs-on, 'arm') && 'arm64' || 'amd64' }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..75d5fdc --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + lint: + 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' + cache: false + - name: Check gofmt + run: test -z "$(gofmt -l .)" + working-directory: tests/e2e + - name: Run go vet + run: go vet ./... + working-directory: tests/e2e diff --git a/docs/e2e.md b/docs/e2e.md index a5ec5f7..77120de 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -24,28 +24,9 @@ DOCKER_SOCK="$XDG_RUNTIME_DIR/docker.sock" \ docker compose -f tests/e2e/linux/compose.yaml run --rm e2e ``` -To check the `vfox` x `flavor` x `mirror` matrix expansion without -starting containers, override the service command: - -```bash -docker compose -f tests/e2e/linux/compose.yaml run --rm e2e go test -short ./... -``` - On ARM64 it runs the official flavor only: OpenHarmony publishes no `linux-arm64` Dart SDK (see `docs/ohos.md`), so the ohos flavor cannot bootstrap there. -### Run the ARM64 E2E on a x64 host - -To run the ARM64 E2E on an x64 host instead (emulated, much slower), -register QEMU binfmt support with one command (re-run after each reboot) and -override the architecture: - -```bash -docker run --privileged --rm tonistiigi/binfmt --install all -cd ./vfox-flutter/ -ARCH=arm64 docker compose -f tests/e2e/linux/compose.yaml run --rm e2e -``` - ## Windows 11 Pro Windows containers cannot be emulated across architectures, so the ARM64 suite @@ -67,7 +48,7 @@ winget install --id Microsoft.PowerShell --source winget --exact ```powershell winget install --id Git.Git --source winget --exact -curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors ` +curl.exe -fsSL ` "https://raw.githubusercontent.com/microsoft/Windows-Containers/Main/helpful_tools/Install-DockerCE/install-docker-ce.ps1" ` -o ./install-docker-ce.ps1 ``` @@ -82,7 +63,8 @@ curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors ` ```powershell New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.docker\cli-plugins" | Out-Null -curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors ` + +curl.exe -fsSL ` "https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-windows-x86_64.exe" ` -o "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe" ``` @@ -92,14 +74,7 @@ curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors ` ```powershell git clone git@github.com:version-fox/vfox-flutter.git cd ./vfox-flutter/ -docker compose -f tests/e2e/windows/compose.yaml run --rm --build e2e -``` - -To check the `vfox` x `flavor` x `mirror` matrix expansion without -starting containers, override the service command: - -```powershell -docker compose -f tests/e2e/windows/compose.yaml run --rm e2e go test -short ./... +pwsh -NoProfile -File .\tests\e2e\windows\run.ps1 ``` ### arm64 @@ -114,16 +89,15 @@ winget install --id Microsoft.PowerShell --source winget --exact ```powershell winget install --id Git.Git --source winget --exact -# Host Go stays here: building the moby engine from source needs it. The -# suite itself runs from a container (see below), no host Go involved. winget install --id GoLang.Go --source winget --exact git clone git@github.com:version-fox/vfox-flutter.git cd ./vfox-flutter/ pwsh -NoProfile -File .\tests\e2e\windows\install-docker-arm64.ps1 New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.docker\cli-plugins" | Out-Null -curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors ` + +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" -$env:ARCH = 'arm64' -docker compose -f tests/e2e/windows/compose.yaml run --rm --build e2e + +pwsh -NoProfile -File .\tests\e2e\windows\run.ps1 ``` diff --git a/tests/e2e/common/common.go b/tests/e2e/common/common.go new file mode 100644 index 0000000..72254bc --- /dev/null +++ b/tests/e2e/common/common.go @@ -0,0 +1,239 @@ +// Package common holds the shared logic of the Linux and Windows container +// E2E suites. +// +// The two suites target different container systems (bash vs pwsh, different +// release probing and isolation), so the code interacting with containers +// stays in the linux / windows packages; the target-independent parts +// (matrix expansion, version defaults, retry counts, name handling) live +// here and are reused by both platform packages, instead of being copied. +package common + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "os" + "runtime" + "strconv" + "strings" + "testing" + "time" +) + +// Flutter defaults, maintained in one place for both packages. +const ( + DefaultOfficialVersion = "3.47.4" + DefaultOhosVersion = "3.41.10-ohos-1.0.0" + // BogusMirror is the unreachable mirror domain used by the negative probes. + BogusMirror = "https://invalid.example.invalid" + // DefaultMirrorCN is the default mainland-China Flutter storage mirror. + DefaultMirrorCN = "https://storage.flutter-io.cn" +) + +// PassThroughEnvs are host env knobs the in-container scripts honour. +// Forwarded only when set, so unset stays default. +var PassThroughEnvs = []string{ + "FLUTTER_VERSION", + "OHOS_VERSION", + "VFOX_FLUTTER_GITHUB_MIRROR", + "E2E_RETRY_ATTEMPTS", +} + +// Combo is one matrix cell: a vfox version, a flutter flavor and a mirror. +type Combo struct { + Vfox string + Flavor string + Mirror string +} + +// Prefix renders the log prefix: +// "vfox , , mirror , ". +func (c Combo) Prefix(platform string) string { + return fmt.Sprintf("vfox %s, %s, mirror %s, %s", c.Vfox, c.Flavor, c.Mirror, platform) +} + +// Slug renders a docker-safe container name fragment for the combo. +func (c Combo) Slug() string { + return c.Vfox + "-" + c.Flavor + "-" + Sanitize(c.Mirror) +} + +// Sanitize replaces every non-ASCII-alphanumeric byte with '_' so arbitrary +// mirror URLs stay valid in container names. +func Sanitize(s string) string { + var b strings.Builder + b.Grow(len(s)) + for i := 0; i < len(s); i++ { + ch := s[i] + if ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' { + b.WriteByte(ch) + } else { + b.WriteByte('_') + } + } + return b.String() +} + +// NormalizeArch maps uname-style names, Windows PROCESSOR_ARCHITECTURE values +// and common aliases to docker arch names. +func NormalizeArch(s string) (string, error) { + switch strings.ToLower(s) { + case "x86_64", "x64", "amd64": + return "amd64", nil + case "aarch64", "arm64": + return "arm64", nil + default: + return "", fmt.Errorf("unknown architecture %s", s) + } +} + +// DetectArch reports the runner architecture: $PROCESSOR_ARCHITECTURE on +// Windows hosts (AMD64/ARM64/x86), else the test binary's own arch. There is +// deliberately no override knob: emulated runs (arm64 on an x64 host) are +// not supported, and CI covers each arch on native runners. +func DetectArch() (string, error) { + if arch, ok := os.LookupEnv("PROCESSOR_ARCHITECTURE"); ok && strings.TrimSpace(arch) != "" { + return NormalizeArch(strings.TrimSpace(arch)) + } + return NormalizeArch(runtime.GOARCH) +} + +// Fields splits space-separated env lists (e.g. FLAVOR="official ohos"). +func Fields(s string) []string { + return strings.Fields(s) +} + +// DefaultFlavours mirrors the old rule: ohos publishes no linux-arm64 / +// windows-arm64 artefacts, so arm64 runs official only. +func DefaultFlavours(arch string) []string { + if arch == "arm64" { + return []string{"official"} + } + return []string{"official", "ohos"} +} + +// DefaultMirrors mirrors the old rule: the extra mirror only runs on amd64, +// where the full matrix is affordable. +func DefaultMirrors(arch string) []string { + if arch == "arm64" { + return []string{"default"} + } + return []string{"default", DefaultMirrorCN} +} + +// Matrix expands the vfox x flavor x mirror combinations for the runner +// architecture. Env overrides: VFOX_VERSION (default "latest main"), FLAVOR, +// MIRROR. The mirror matrix only runs against latest vfox unless the caller +// explicitly pins MIRROR, to keep CI time bounded (same rule as the old +// e2e.sh / e2e.ps1). +func Matrix() ([]Combo, error) { + arch, err := DetectArch() + if err != nil { + return nil, err + } + return MatrixForArch(arch) +} + +// MatrixForArch expands the matrix for an explicit arch. Tests use it +// directly so each arch branch stays covered on any host. +func MatrixForArch(arch string) ([]Combo, error) { + + foxes := []string{"latest", "main"} + if v, ok := os.LookupEnv("VFOX_VERSION"); ok && len(Fields(v)) > 0 { + foxes = Fields(v) + } + flavours := DefaultFlavours(arch) + if v, ok := os.LookupEnv("FLAVOR"); ok && len(Fields(v)) > 0 { + flavours = Fields(v) + } + mirrors := DefaultMirrors(arch) + if v, ok := os.LookupEnv("MIRROR"); ok && len(Fields(v)) > 0 { + mirrors = Fields(v) + } + _, mirrorExplicit := os.LookupEnv("MIRROR") + mirrorExplicit = mirrorExplicit && len(Fields(os.Getenv("MIRROR"))) > 0 + + var combos []Combo + for _, vfox := range foxes { + for _, flavor := range flavours { + for _, mirror := range mirrors { + if !mirrorExplicit && mirror != "default" && vfox != "latest" { + continue + } + combos = append(combos, Combo{Vfox: vfox, Flavor: flavor, Mirror: mirror}) + } + } + } + return combos, nil +} + +// MaxJobs bounds parallel containers; honours $E2E_MAX_JOBS and falls back +// to def when unset or invalid. Linux passes 3, Windows passes 2 (matching +// the old e2e.ps1). +func MaxJobs(def int) int { + if v, ok := os.LookupEnv("E2E_MAX_JOBS"); ok { + if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 1 { + return n + } + } + return def +} + +// RetryAttempts caps exec retries; honours $E2E_RETRY_ATTEMPTS (default 3). +func RetryAttempts() int { + if v, ok := os.LookupEnv("E2E_RETRY_ATTEMPTS"); ok && strings.TrimSpace(v) != "" { + if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { + return n + } + } + return 3 +} + +// ResolveFlutterVersion honours $FLUTTER_VERSION / $OHOS_VERSION, else the +// defaults above. +func ResolveFlutterVersion(t *testing.T, flavor string) string { + t.Helper() + switch flavor { + case "official": + if v := os.Getenv("FLUTTER_VERSION"); v != "" { + return v + } + return DefaultOfficialVersion + case "ohos": + if v := os.Getenv("OHOS_VERSION"); v != "" { + return v + } + return DefaultOhosVersion + default: + t.Fatalf("unknown flavor %s", flavor) + return "" + } +} + +// ToolsDart extracts the Dart version from `flutter --version` output +// (was: awk '/Tools/ { print $4 }' and its pwsh equivalent; both agree). +func ToolsDart(flutterVersion string) string { + for _, line := range strings.Split(flutterVersion, "\n") { + if strings.Contains(line, "Tools") { + if f := strings.Fields(line); len(f) >= 4 { + return f[3] + } + } + } + return "" +} + +// ContainerName returns a unique container name per run: the combo slug stays +// greppable in `docker ps`, the random suffix avoids clashes with leftovers +// from killed runs. +func ContainerName(slug string) string { + var b [4]byte + if _, err := rand.Read(b[:]); err != nil { + return fmt.Sprintf("vfox-flutter-e2e-%s-%d", slug, time.Now().UnixNano()) + } + return "vfox-flutter-e2e-" + slug + "-" + hex.EncodeToString(b[:]) +} + +// Manifest is the anchored-install record the plugin writes into the SDK. +type Manifest struct { + ExpectedHead string `json:"expected_head"` +} diff --git a/tests/e2e/common/common_test.go b/tests/e2e/common/common_test.go new file mode 100644 index 0000000..b46bc28 --- /dev/null +++ b/tests/e2e/common/common_test.go @@ -0,0 +1,188 @@ +package common + +import ( + "testing" +) + +func setEnv(t *testing.T, kv map[string]string) { + t.Helper() + for k, v := range kv { + t.Setenv(k, v) + } +} + +func comboKeys(combos []Combo) []string { + keys := make([]string, 0, len(combos)) + for _, c := range combos { + keys = append(keys, c.Vfox+"/"+c.Flavor+"/"+c.Mirror) + } + return keys +} + +// Default amd64 matrix: latest runs both mirrors, main runs default only. +func TestMatrixDefaultAmd64(t *testing.T) { + t.Setenv("VFOX_VERSION", "") + t.Setenv("FLAVOR", "") + t.Setenv("MIRROR", "") + + // t.Setenv sets the var to "" which LookupEnv reports as set; the empty + // values above exercise the real defaults via Fields dropping empties. + combos, err := MatrixForArch("amd64") + if err != nil { + t.Fatal(err) + } + if len(combos) == 0 { + t.Fatal("empty matrix") + } + t.Logf("combos: %v", comboKeys(combos)) + for _, c := range combos { + if c.Mirror != "default" && c.Vfox != "latest" { + t.Errorf("non-default mirror runs against %s vfox: %+v", c.Vfox, c) + } + } +} + +// arm64 defaults: official flavor, default mirror only. +func TestMatrixDefaultArm64(t *testing.T) { + setEnv(t, map[string]string{ + "VFOX_VERSION": "latest main", + "FLAVOR": "", + "MIRROR": "", + }) + + combos, err := MatrixForArch("arm64") + if err != nil { + t.Fatal(err) + } + for _, c := range combos { + if c.Flavor != "official" { + t.Errorf("arm64 flavor = %s, want official", c.Flavor) + } + if c.Mirror != "default" { + t.Errorf("arm64 mirror = %s, want default", c.Mirror) + } + } + if len(combos) == 0 { + t.Fatal("empty matrix") + } +} + +// Explicit MIRROR pins the full mirror matrix even for non-latest vfox. +func TestMatrixExplicitMirror(t *testing.T) { + setEnv(t, map[string]string{ + "VFOX_VERSION": "main", + "FLAVOR": "official", + "MIRROR": "default https://storage.flutter-io.cn", + }) + + combos, err := MatrixForArch("amd64") + if err != nil { + t.Fatal(err) + } + if len(combos) != 2 { + t.Fatalf("combos = %v, want 2", comboKeys(combos)) + } +} + +// PROCESSOR_ARCHITECTURE values from real Windows hosts resolve correctly. +func TestMatrixProcessorArchitecture(t *testing.T) { + t.Setenv("PROCESSOR_ARCHITECTURE", "ARM64") + t.Setenv("VFOX_VERSION", "latest") + t.Setenv("FLAVOR", "official") + t.Setenv("MIRROR", "default") + combos, err := Matrix() + if err != nil { + t.Fatal(err) + } + if len(combos) != 1 { + t.Fatalf("combos = %v, want 1", comboKeys(combos)) + } +} + +// Unknown architectures fail fast instead of running a wrong matrix. +func TestMatrixUnknownArch(t *testing.T) { + t.Setenv("PROCESSOR_ARCHITECTURE", "bogus64") + if _, err := Matrix(); err == nil { + t.Error("Matrix() with bogus PROCESSOR_ARCHITECTURE succeeded, want error") + } +} + +func TestNormalizeArch(t *testing.T) { + for in, want := range map[string]string{ + "AMD64": "amd64", "x86_64": "amd64", "x64": "amd64", "amd64": "amd64", + "ARM64": "arm64", "aarch64": "arm64", "arm64": "arm64", + } { + got, err := NormalizeArch(in) + if err != nil || got != want { + t.Errorf("NormalizeArch(%q) = %q, %v; want %q", in, got, err, want) + } + } + for _, bad := range []string{"riscv64", "x86"} { + if _, err := NormalizeArch(bad); err == nil { + t.Errorf("NormalizeArch(%q) succeeded, want error", bad) + } + } +} + +func TestSanitize(t *testing.T) { + got := Combo{Vfox: "latest", Flavor: "official", Mirror: "https://storage.flutter-io.cn"}.Slug() + want := "latest-official-https___storage_flutter_io_cn" + if got != want { + t.Errorf("slug = %q, want %q", got, want) + } +} + +func TestMaxJobs(t *testing.T) { + t.Setenv("E2E_MAX_JOBS", "") + // empty means unset-equivalent for MaxJobs (Atoi fails -> default) + if got := MaxJobs(3); got != 3 { + t.Errorf("MaxJobs = %d, want 3", got) + } + t.Setenv("E2E_MAX_JOBS", "0") + if got := MaxJobs(3); got != 3 { + t.Errorf("MaxJobs(0) = %d, want 3 (clamped to default)", got) + } + t.Setenv("E2E_MAX_JOBS", "2") + if got := MaxJobs(3); got != 2 { + t.Errorf("MaxJobs(2) = %d, want 2", got) + } +} + +func TestResolveFlutterVersion(t *testing.T) { + t.Setenv("FLUTTER_VERSION", "") + t.Setenv("OHOS_VERSION", "") + if got := ResolveFlutterVersion(t, "official"); got != "3.47.4" { + t.Errorf("official default = %q", got) + } + if got := ResolveFlutterVersion(t, "ohos"); got != "3.41.10-ohos-1.0.0" { + t.Errorf("ohos default = %q", got) + } + t.Setenv("FLUTTER_VERSION", "3.99.0") + if got := ResolveFlutterVersion(t, "official"); got != "3.99.0" { + t.Errorf("official override = %q", got) + } +} + +func TestToolsDart(t *testing.T) { + out := "Flutter 3.47.4 • channel stable • https://github.com/flutter/flutter.git\n" + + "Framework • revision abc123 (4 weeks ago) • 2026-01-01\n" + + "Engine • revision def456\n" + + "Tools • Dart 3.10.0 • DevTools 2.48.0\n" + if got := ToolsDart(out); got != "3.10.0" { + t.Errorf("toolsDart = %q, want 3.10.0", got) + } + if got := ToolsDart("no tools here\n"); got != "" { + t.Errorf("toolsDart without Tools line = %q, want empty", got) + } +} + +func TestRetryAttemptsDefault(t *testing.T) { + t.Setenv("E2E_RETRY_ATTEMPTS", "") + if got := RetryAttempts(); got != 3 { + t.Errorf("retryAttempts = %d, want 3", got) + } + t.Setenv("E2E_RETRY_ATTEMPTS", "5") + if got := RetryAttempts(); got != 5 { + t.Errorf("retryAttempts = %d, want 5", got) + } +} diff --git a/tests/e2e/doc.go b/tests/e2e/doc.go new file mode 100644 index 0000000..f6778cd --- /dev/null +++ b/tests/e2e/doc.go @@ -0,0 +1,18 @@ +// Package e2e is the single Go module root of the container E2E suites. +// +// This directory holds no test logic, only the module definition +// (go.mod/go.sum) and the orchestration files; the code is split by +// subpackage: +// +// - common: target-independent shared logic (matrix expansion, version +// defaults, retry counts, name handling); +// - linux: the Linux container suite (bash-driven), run in CI and locally +// via tests/e2e/linux/compose.yaml as `go test ./linux/`; +// - windows: the Windows container suite (pwsh-driven), run via +// tests/e2e/windows/compose.yaml as `go test ./windows/`. +// +// Pure unit tests without containers (matrix expansion and friends) run from +// the module root: +// +// go test -short ./... +package e2e diff --git a/tests/e2e/linux/go.mod b/tests/e2e/go.mod similarity index 97% rename from tests/e2e/linux/go.mod rename to tests/e2e/go.mod index b9391bc..6c10c03 100644 --- a/tests/e2e/linux/go.mod +++ b/tests/e2e/go.mod @@ -1,4 +1,4 @@ -module github.com/version-fox/vfox-flutter/tests/e2e/linux +module github.com/version-fox/vfox-flutter/tests/e2e go 1.25.0 diff --git a/tests/e2e/linux/go.sum b/tests/e2e/go.sum similarity index 100% rename from tests/e2e/linux/go.sum rename to tests/e2e/go.sum diff --git a/tests/e2e/linux/compose.yaml b/tests/e2e/linux/compose.yaml index c066753..40f12ab 100644 --- a/tests/e2e/linux/compose.yaml +++ b/tests/e2e/linux/compose.yaml @@ -10,12 +10,16 @@ # DOCKER_SOCK="$XDG_RUNTIME_DIR/docker.sock" docker compose -f ... run --rm e2e # # Every knob below forwards only when set in the caller's env; empty means -# "use the built-in default" (see matrix.go). +# "use the built-in default" (see common/common.go). +# +# The module root is the working dir (single Go module for common + linux + +# windows); the command targets the linux package only, because Windows +# containers cannot run on a Linux host. services: e2e: image: golang:1.26.8 - working_dir: /work/tests/e2e/linux + working_dir: /work/tests/e2e volumes: - ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock - ../../..:/work @@ -29,7 +33,6 @@ services: # via t.Cleanup instead; leftovers from killed runs share the # vfox-flutter-e2e- name prefix for easy pruning. TESTCONTAINERS_RYUK_DISABLED: "true" - ARCH: ${ARCH:-} E2E_MAX_JOBS: ${E2E_MAX_JOBS:-} VFOX_VERSION: ${VFOX_VERSION:-} FLAVOR: ${FLAVOR:-} @@ -38,7 +41,7 @@ services: OHOS_VERSION: ${OHOS_VERSION:-} VFOX_FLUTTER_GITHUB_MIRROR: ${VFOX_FLUTTER_GITHUB_MIRROR:-} E2E_RETRY_ATTEMPTS: ${E2E_RETRY_ATTEMPTS:-} - command: ["go", "test", "-v", "-timeout", "55m", "-run", "TestE2E", "./..."] + command: ["go", "test", "-v", "-timeout", "55m", "-run", "TestE2E", "./linux"] volumes: e2e-go-modcache: diff --git a/tests/e2e/linux/e2e_test.go b/tests/e2e/linux/e2e_test.go index e208804..996d09b 100644 --- a/tests/e2e/linux/e2e_test.go +++ b/tests/e2e/linux/e2e_test.go @@ -1,10 +1,15 @@ -package e2e +// Package linux is the Linux container E2E suite, driven entirely from Go via +// testcontainers-go. +// +// TestE2E builds the image in tests/e2e/linux/Dockerfile once, then runs one +// long-lived container per (vfox version x flavor x mirror) combination and +// drives setup -> install -> verify through exec calls (setup.go, install.go, +// verify.go), asserting in Go. Target-independent logic (matrix expansion and +// friends) is reused from the common package. +package linux import ( "context" - "crypto/rand" - "encoding/hex" - "fmt" "os" "path/filepath" "strings" @@ -16,28 +21,21 @@ import ( tc "github.com/testcontainers/testcontainers-go" client "github.com/moby/moby/client" -) -// passThroughEnvs are host env knobs the in-container scripts (run.sh and -// friends) honour. Forwarded only when set, so unset stays default. -var passThroughEnvs = []string{ - "FLUTTER_VERSION", - "OHOS_VERSION", - "VFOX_FLUTTER_GITHUB_MIRROR", - "E2E_RETRY_ATTEMPTS", -} + "github.com/version-fox/vfox-flutter/tests/e2e/common" +) // TestE2E builds the image once, then runs one container per matrix combo. // Bounded parallelism via t.Parallel + semaphore (like E2E_MAX_JOBS). // // Full matrix (~45min) runs in CI; locally scope it down, e.g.: -// VFOX_VERSION=latest FLAVOR=official MIRROR=default E2E_MAX_JOBS=1 go test -v +// VFOX_VERSION=latest FLAVOR=official MIRROR=default E2E_MAX_JOBS=1 go test -v ./linux/ func TestE2E(t *testing.T) { if testing.Short() { t.Skip("skip container E2E in short mode") } - arch, err := detectArch() + arch, err := common.DetectArch() if err != nil { t.Fatal(err) } @@ -53,7 +51,7 @@ func TestE2E(t *testing.T) { ctx := context.Background() buildImage(t, ctx, repoRoot, image, platform) - combos, err := matrix() + combos, err := common.Matrix() if err != nil { t.Fatal(err) } @@ -64,7 +62,7 @@ func TestE2E(t *testing.T) { sem := make(chan struct{}, maxJobs()) for _, c := range combos { c := c - t.Run(c.slug(), func(t *testing.T) { + t.Run(c.Slug(), func(t *testing.T) { t.Parallel() sem <- struct{}{} defer func() { <-sem }() @@ -111,33 +109,22 @@ func buildImage(t *testing.T, ctx context.Context, repoRoot, image, platform str } } -// containerName returns a unique container name per run: the combo slug stays -// greppable in `docker ps`, the random suffix avoids clashes with leftovers -// from killed runs (the old bash runner used anonymous names). -func containerName(c combo) string { - var b [4]byte - if _, err := rand.Read(b[:]); err != nil { - return fmt.Sprintf("vfox-flutter-e2e-%s-%d", c.slug(), time.Now().UnixNano()) - } - return "vfox-flutter-e2e-" + c.slug() + "-" + hex.EncodeToString(b[:]) -} - // runOne starts one long-lived container per combo and drives the whole // E2E flow (setup -> install -> verify) through exec calls, asserting in Go. // A combo passes when every phase exits 0 and the outputs carry the expected -// markers (same bar as the old run.sh and the Windows matrix). -func runOne(t *testing.T, ctx context.Context, image, platform string, c combo) { +// markers. +func runOne(t *testing.T, ctx context.Context, image, platform string, c common.Combo) { t.Helper() - prefix := c.prefix(platform) + prefix := c.Prefix(platform) env := map[string]string{ - "VFOX_VERSION": c.vfox, - "FLAVOR": c.flavor, + "VFOX_VERSION": c.Vfox, + "FLAVOR": c.Flavor, } - if c.mirror != "default" { - env["FLUTTER_STORAGE_BASE_URL"] = c.mirror + if c.Mirror != "default" { + env["FLUTTER_STORAGE_BASE_URL"] = c.Mirror } - for _, name := range passThroughEnvs { + for _, name := range common.PassThroughEnvs { if v, ok := os.LookupEnv(name); ok && v != "" { env[name] = v } @@ -147,7 +134,7 @@ func runOne(t *testing.T, ctx context.Context, image, platform string, c combo) ContainerRequest: tc.ContainerRequest{ Image: image, ImagePlatform: platform, - Name: containerName(c), + Name: common.ContainerName(c.Slug()), Env: env, }, Started: true, @@ -163,14 +150,14 @@ func runOne(t *testing.T, ctx context.Context, image, platform string, c combo) ctx, cancel := context.WithTimeout(ctx, 45*time.Minute) defer cancel() - flavor := c.flavor - mirror := c.mirror - version := resolveFlutterVersion(t, flavor) + flavor := c.Flavor + mirror := c.Mirror + version := common.ResolveFlutterVersion(t, flavor) machine := strings.TrimSpace(execOK(ctx, t, ctr, "uname --machine")) - t.Logf("=== vfox %s, flutter %s, %s, mirror %s, %s ===", c.vfox, version, flavor, mirror, machine) + t.Logf("=== vfox %s, flutter %s, %s, mirror %s, %s ===", c.Vfox, version, flavor, mirror, machine) checkMirrorReachable(ctx, t, ctr, flavor, mirror) - setupVfox(ctx, t, ctr, c.vfox) + setupVfox(ctx, t, ctr, c.Vfox) checkBogusMirrorRejected(ctx, t, ctr, flavor, version) checkBogusGithubMirrorRejected(ctx, t, ctr, flavor, version) diff --git a/tests/e2e/linux/exec.go b/tests/e2e/linux/exec.go index d0213de..3f0e593 100644 --- a/tests/e2e/linux/exec.go +++ b/tests/e2e/linux/exec.go @@ -1,27 +1,16 @@ -package e2e +package linux import ( "context" "io" - "os" - "strconv" - "strings" "testing" "time" tc "github.com/testcontainers/testcontainers-go" tcexec "github.com/testcontainers/testcontainers-go/exec" -) -// retryAttempts mirrors lib.sh: up to $E2E_RETRY_ATTEMPTS attempts (default 3). -func retryAttempts() int { - if v, ok := os.LookupEnv("E2E_RETRY_ATTEMPTS"); ok && strings.TrimSpace(v) != "" { - if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { - return n - } - } - return 3 -} + "github.com/version-fox/vfox-flutter/tests/e2e/common" +) // execRun runs script via `bash -c` in the container and returns its exit // code with the combined stdout+stderr output. extraEnv entries ("K=V") @@ -59,7 +48,7 @@ func execOK(ctx context.Context, t *testing.T, ctr tc.Container, script string, // 6 months, so no third-party candidate qualifies. func execRetry(ctx context.Context, t *testing.T, ctr tc.Container, label, script string, extraEnv ...string) string { t.Helper() - max := retryAttempts() + max := common.RetryAttempts() var out string for attempt := 1; ; attempt++ { code, o := execRun(ctx, t, ctr, script, extraEnv...) diff --git a/tests/e2e/linux/install.go b/tests/e2e/linux/install.go index 20fcddd..c13ac71 100644 --- a/tests/e2e/linux/install.go +++ b/tests/e2e/linux/install.go @@ -1,4 +1,4 @@ -package e2e +package linux import ( "context" diff --git a/tests/e2e/linux/matrix.go b/tests/e2e/linux/matrix.go deleted file mode 100644 index 2aafa95..0000000 --- a/tests/e2e/linux/matrix.go +++ /dev/null @@ -1,146 +0,0 @@ -// Package e2e is the Linux container E2E suite, driven entirely from Go via -// testcontainers-go. -// -// TestE2E builds the image in tests/e2e/linux/Dockerfile once, then runs one -// long-lived container per (vfox version x flavor x mirror) combination and -// drives setup -> install -> verify through exec calls (setup.go, install.go, -// verify.go), asserting in Go. matrix.go holds the matrix expansion. -package e2e - -import ( - "fmt" - "os" - "runtime" - "strconv" - "strings" -) - -// defaultMirrorCN is the default mainland-China Flutter storage mirror. -const defaultMirrorCN = "https://storage.flutter-io.cn" - -// combo is one matrix cell: a vfox version, a flutter flavor and a mirror. -type combo struct { - vfox string - flavor string - mirror string -} - -// prefix renders the log prefix shared with the old e2e.sh and e2e.ps1: -// "vfox , , mirror , ". -func (c combo) prefix(platform string) string { - return fmt.Sprintf("vfox %s, %s, mirror %s, %s", c.vfox, c.flavor, c.mirror, platform) -} - -// slug renders a docker-safe container name fragment for the combo. -func (c combo) slug() string { - return c.vfox + "-" + c.flavor + "-" + sanitize(c.mirror) -} - -// sanitize replaces every non-ASCII-alphanumeric byte with '_' so arbitrary -// mirror URLs stay valid in container names (mirrors e2e.ps1). -func sanitize(s string) string { - var b strings.Builder - b.Grow(len(s)) - for i := 0; i < len(s); i++ { - ch := s[i] - if ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' { - b.WriteByte(ch) - } else { - b.WriteByte('_') - } - } - return b.String() -} - -// normalizeArch maps uname-style names and ARCH aliases to docker arches. -func normalizeArch(s string) (string, error) { - switch strings.ToLower(s) { - case "x86_64", "x64", "amd64": - return "amd64", nil - case "aarch64", "arm64": - return "arm64", nil - default: - return "", fmt.Errorf("unknown architecture %s", s) - } -} - -// detectArch honours $ARCH and falls back to the test binary's own arch. -func detectArch() (string, error) { - if arch, ok := os.LookupEnv("ARCH"); ok && strings.TrimSpace(arch) != "" { - return normalizeArch(strings.TrimSpace(arch)) - } - return normalizeArch(runtime.GOARCH) -} - -// fields splits space-separated env lists (e.g. FLAVOR="official ohos"). -// strings.Fields matches bash `read -ra` on the default IFS. -func fields(s string) []string { - return strings.Fields(s) -} - -// defaultFlavours mirrors the old config.sh rule: ohos publishes no -// linux-arm64 artefacts, so arm64 runs official only (same as Windows). -func defaultFlavours(arch string) []string { - if arch == "arm64" { - return []string{"official"} - } - return []string{"official", "ohos"} -} - -// defaultMirrors mirrors the old config.sh rule: the extra mirror only runs -// on amd64, where the full matrix is affordable. -func defaultMirrors(arch string) []string { - if arch == "arm64" { - return []string{"default"} - } - return []string{"default", defaultMirrorCN} -} - -// matrix expands the vfox x flavor x mirror combinations. Env overrides: -// VFOX_VERSION (default "latest main"), FLAVOR, MIRROR, ARCH. The mirror -// matrix only runs against latest vfox unless the caller explicitly pins -// MIRROR, to keep CI time bounded (same rule as e2e.sh / e2e.ps1). -func matrix() ([]combo, error) { - arch, err := detectArch() - if err != nil { - return nil, err - } - - foxes := []string{"latest", "main"} - if v, ok := os.LookupEnv("VFOX_VERSION"); ok && len(fields(v)) > 0 { - foxes = fields(v) - } - flavours := defaultFlavours(arch) - if v, ok := os.LookupEnv("FLAVOR"); ok && len(fields(v)) > 0 { - flavours = fields(v) - } - mirrors := defaultMirrors(arch) - if v, ok := os.LookupEnv("MIRROR"); ok && len(fields(v)) > 0 { - mirrors = fields(v) - } - _, mirrorExplicit := os.LookupEnv("MIRROR") - mirrorExplicit = mirrorExplicit && len(fields(os.Getenv("MIRROR"))) > 0 - - var combos []combo - for _, vfox := range foxes { - for _, flavor := range flavours { - for _, mirror := range mirrors { - if !mirrorExplicit && mirror != "default" && vfox != "latest" { - continue - } - combos = append(combos, combo{vfox: vfox, flavor: flavor, mirror: mirror}) - } - } - } - return combos, nil -} - -// maxJobs bounds parallel containers; honours $E2E_MAX_JOBS (default 3). -func maxJobs() int { - if v, ok := os.LookupEnv("E2E_MAX_JOBS"); ok { - if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 1 { - return n - } - } - return 3 -} diff --git a/tests/e2e/linux/matrix_test.go b/tests/e2e/linux/matrix_test.go index 443f608..a2ad045 100644 --- a/tests/e2e/linux/matrix_test.go +++ b/tests/e2e/linux/matrix_test.go @@ -1,120 +1,12 @@ -package e2e +package linux import ( "testing" ) -func setEnv(t *testing.T, kv map[string]string) { - t.Helper() - for k, v := range kv { - t.Setenv(k, v) - } -} - -func comboKeys(combos []combo) []string { - keys := make([]string, 0, len(combos)) - for _, c := range combos { - keys = append(keys, c.vfox+"/"+c.flavor+"/"+c.mirror) - } - return keys -} - -// Default amd64 matrix: latest runs both mirrors, main runs default only. -func TestMatrixDefaultAmd64(t *testing.T) { - setEnv(t, map[string]string{"ARCH": "amd64"}) - t.Setenv("VFOX_VERSION", "") - t.Setenv("FLAVOR", "") - t.Setenv("MIRROR", "") - - // t.Setenv sets the var to "" which LookupEnv reports as set; unset - // MIRROR/VFOX_VERSION/FLAVOR explicitly to exercise real defaults. - // (os.Unsetenv cannot be used with t.Setenv in the same test, so set - // the zero-ish values above and rely on fields() dropping empties.) - combos, err := matrix() - if err != nil { - t.Fatal(err) - } - if len(combos) == 0 { - t.Fatal("empty matrix") - } - t.Logf("combos: %v", comboKeys(combos)) - for _, c := range combos { - if c.mirror != "default" && c.vfox != "latest" { - t.Errorf("non-default mirror runs against %s vfox: %+v", c.vfox, c) - } - } -} - -// arm64 defaults: official flavor, default mirror only. -func TestMatrixDefaultArm64(t *testing.T) { - setEnv(t, map[string]string{ - "ARCH": "arm64", - "VFOX_VERSION": "latest main", - "FLAVOR": "", - "MIRROR": "", - }) - - combos, err := matrix() - if err != nil { - t.Fatal(err) - } - for _, c := range combos { - if c.flavor != "official" { - t.Errorf("arm64 flavor = %s, want official", c.flavor) - } - if c.mirror != "default" { - t.Errorf("arm64 mirror = %s, want default", c.mirror) - } - } - if len(combos) == 0 { - t.Fatal("empty matrix") - } -} - -// Explicit MIRROR pins the full mirror matrix even for non-latest vfox. -func TestMatrixExplicitMirror(t *testing.T) { - setEnv(t, map[string]string{ - "ARCH": "amd64", - "VFOX_VERSION": "main", - "FLAVOR": "official", - "MIRROR": "default https://storage.flutter-io.cn", - }) - - combos, err := matrix() - if err != nil { - t.Fatal(err) - } - if len(combos) != 2 { - t.Fatalf("combos = %v, want 2", comboKeys(combos)) - } -} - -func TestNormalizeArch(t *testing.T) { - for in, want := range map[string]string{ - "x86_64": "amd64", "amd64": "amd64", "x64": "amd64", - "aarch64": "arm64", "arm64": "arm64", - } { - got, err := normalizeArch(in) - if err != nil || got != want { - t.Errorf("normalizeArch(%q) = %q, %v; want %q", in, got, err, want) - } - } - if _, err := normalizeArch("riscv64"); err == nil { - t.Error("normalizeArch(riscv64) succeeded, want error") - } -} - -func TestSanitize(t *testing.T) { - got := combo{vfox: "latest", flavor: "official", mirror: "https://storage.flutter-io.cn"}.slug() - want := "latest-official-https___storage_flutter_io_cn" - if got != want { - t.Errorf("slug = %q, want %q", got, want) - } -} - func TestMaxJobsDefault(t *testing.T) { t.Setenv("E2E_MAX_JOBS", "") - // empty means unset-equivalent for maxJobs (Atoi fails -> default) + // empty means unset-equivalent for maxJobs (Atoi fails -> default 3) if got := maxJobs(); got != 3 { t.Errorf("maxJobs = %d, want 3", got) } @@ -124,21 +16,6 @@ func TestMaxJobsDefault(t *testing.T) { } } -func TestResolveFlutterVersion(t *testing.T) { - t.Setenv("FLUTTER_VERSION", "") - t.Setenv("OHOS_VERSION", "") - if got := resolveFlutterVersion(t, "official"); got != "3.47.4" { - t.Errorf("official default = %q", got) - } - if got := resolveFlutterVersion(t, "ohos"); got != "3.41.10-ohos-1.0.0" { - t.Errorf("ohos default = %q", got) - } - t.Setenv("FLUTTER_VERSION", "3.99.0") - if got := resolveFlutterVersion(t, "official"); got != "3.99.0" { - t.Errorf("official override = %q", got) - } -} - func TestReleasesIndex(t *testing.T) { got := releasesIndex("https://storage.flutter-io.cn/") want := "https://storage.flutter-io.cn/flutter_infra_release/releases/releases_linux.json" @@ -146,27 +23,3 @@ func TestReleasesIndex(t *testing.T) { t.Errorf("releasesIndex = %q, want %q", got, want) } } - -func TestToolsDart(t *testing.T) { - out := "Flutter 3.47.4 • channel stable • https://github.com/flutter/flutter.git\n" + - "Framework • revision abc123 (4 weeks ago) • 2026-01-01\n" + - "Engine • revision def456\n" + - "Tools • Dart 3.10.0 • DevTools 2.48.0\n" - if got := toolsDart(out); got != "3.10.0" { - t.Errorf("toolsDart = %q, want 3.10.0", got) - } - if got := toolsDart("no tools here\n"); got != "" { - t.Errorf("toolsDart without Tools line = %q, want empty", got) - } -} - -func TestRetryAttemptsDefault(t *testing.T) { - t.Setenv("E2E_RETRY_ATTEMPTS", "") - if got := retryAttempts(); got != 3 { - t.Errorf("retryAttempts = %d, want 3", got) - } - t.Setenv("E2E_RETRY_ATTEMPTS", "5") - if got := retryAttempts(); got != 5 { - t.Errorf("retryAttempts = %d, want 5", got) - } -} diff --git a/tests/e2e/linux/platform.go b/tests/e2e/linux/platform.go new file mode 100644 index 0000000..289075d --- /dev/null +++ b/tests/e2e/linux/platform.go @@ -0,0 +1,10 @@ +package linux + +import ( + "github.com/version-fox/vfox-flutter/tests/e2e/common" +) + +// maxJobs bounds parallel containers; honours $E2E_MAX_JOBS (default 3). +func maxJobs() int { + return common.MaxJobs(3) +} diff --git a/tests/e2e/linux/setup.go b/tests/e2e/linux/setup.go index e3b73aa..e2a2a3b 100644 --- a/tests/e2e/linux/setup.go +++ b/tests/e2e/linux/setup.go @@ -1,9 +1,8 @@ -package e2e +package linux import ( "context" "fmt" - "os" "strings" "testing" @@ -11,34 +10,6 @@ import ( tc "github.com/testcontainers/testcontainers-go" ) -// NOTE: keep in sync with tests/e2e/windows/setup.go, which duplicates these -// defaults (the two Go modules share no config format). Bump both together. -const ( - defaultOfficialVersion = "3.47.4" - defaultOhosVersion = "3.41.10-ohos-1.0.0" -) - -// resolveFlutterVersion honours $FLUTTER_VERSION / $OHOS_VERSION, else the -// defaults above (was config.sh resolve_flutter_version). -func resolveFlutterVersion(t *testing.T, flavor string) string { - t.Helper() - switch flavor { - case "official": - if v := os.Getenv("FLUTTER_VERSION"); v != "" { - return v - } - return defaultOfficialVersion - case "ohos": - if v := os.Getenv("OHOS_VERSION"); v != "" { - return v - } - return defaultOhosVersion - default: - t.Fatalf("unknown flavor %s", flavor) - return "" - } -} - // releasesIndex mirrors config.sh releases_index_for_mirror for linux. func releasesIndex(mirror string) string { return strings.TrimSuffix(mirror, "/") + "/flutter_infra_release/releases/releases_linux.json" @@ -58,7 +29,7 @@ func checkMirrorReachable(ctx context.Context, t *testing.T, ctr tc.Container, f } // setupVfox installs vfox (release, or builds main with the in-image Go -// toolchain) and registers the local flutter plugin (was setup.sh). +// toolchain) and registers the local flutter plugin. func setupVfox(ctx context.Context, t *testing.T, ctr tc.Container, vfoxVersion string) { t.Helper() work := strings.TrimSpace(execOK(ctx, t, ctr, "mktemp --directory")) diff --git a/tests/e2e/linux/verify.go b/tests/e2e/linux/verify.go index 76d706e..597b351 100644 --- a/tests/e2e/linux/verify.go +++ b/tests/e2e/linux/verify.go @@ -1,4 +1,4 @@ -package e2e +package linux import ( "context" @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/require" tc "github.com/testcontainers/testcontainers-go" + + "github.com/version-fox/vfox-flutter/tests/e2e/common" ) // NOTE: the drift-warning strings asserted here are part of the plugin's @@ -43,11 +45,6 @@ func verifySdkLayout(ctx context.Context, t *testing.T, ctr tc.Container, flavor t.Log("PASS the OpenHarmony SDK is a git checkout with its engine pins tracked") } -// manifest is the anchored-install record the plugin writes into the SDK. -type manifest struct { - ExpectedHead string `json:"expected_head"` -} - // verifyManifestDrift anchors the installed HEAD via .vfox-manifest, proves // `vfox use` reports drift exactly when the SDK moves (simulated // `flutter upgrade`), and goes quiet again once restored. @@ -55,7 +52,7 @@ func verifyManifestDrift(ctx context.Context, t *testing.T, ctr tc.Container, ve t.Helper() manifestPath := sdk + "/.vfox-manifest" raw := execOK(ctx, t, ctr, fmt.Sprintf("cat %q", manifestPath)) - var m manifest + var m common.Manifest if err := json.Unmarshal([]byte(raw), &m); err != nil { t.Fatalf("the .vfox-manifest is not valid JSON: %v\n--- actual ---\n%s", err, raw) } @@ -97,19 +94,6 @@ func verifyManifestDrift(ctx context.Context, t *testing.T, ctr tc.Container, ve require.NotContains(t, use(), "has drifted", "a restored SDK reports no drift") } -// toolsDart extracts the Dart version from `flutter --version` output -// (was: awk '/Tools/ { print $4 }'). -func toolsDart(flutterVersion string) string { - for _, line := range strings.Split(flutterVersion, "\n") { - if strings.Contains(line, "Tools") { - if f := strings.Fields(line); len(f) >= 4 { - return f[3] - } - } - } - return "" -} - // 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) { @@ -122,7 +106,7 @@ func verifyToolchain(ctx context.Context, t *testing.T, ctr tc.Container, sdk st head := strings.TrimSpace(execOK(ctx, t, ctr, fmt.Sprintf("git -C %q rev-parse HEAD", sdk))) require.Contains(t, flutter, head[:10], "flutter --version revision") - dartWant := toolsDart(flutter) + dartWant := common.ToolsDart(flutter) if dartWant == "" { t.Fatalf("flutter --version has no Tools line\n--- actual ---\n%s", flutter) } diff --git a/tests/e2e/windows/compose.yaml b/tests/e2e/windows/compose.yaml index 4c094c8..0d68a05 100644 --- a/tests/e2e/windows/compose.yaml +++ b/tests/e2e/windows/compose.yaml @@ -12,7 +12,11 @@ # via t.Cleanup instead. # # Every knob below forwards only when set in the caller's env; empty means -# "use the built-in default" (see matrix.go). +# "use the built-in default" (see common/common.go). +# +# The module root is the working dir (single Go module for common + linux + +# windows); the command targets the windows package only, because Linux +# containers cannot run on a Windows host. services: e2e: @@ -22,7 +26,7 @@ services: args: BASE_IMAGE: mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-${ARCH:-amd64} image: vfox-flutter/e2e-runner:1.26.8 - working_dir: C:/work/tests/e2e/windows + working_dir: C:/work/tests/e2e volumes: - type: npipe source: \\.\pipe\docker_engine @@ -30,7 +34,6 @@ services: - ../../..:C:/work - e2e-go-modcache:C:/m environment: - ARCH: ${ARCH:-} E2E_MAX_JOBS: ${E2E_MAX_JOBS:-} VFOX_VERSION: ${VFOX_VERSION:-} FLAVOR: ${FLAVOR:-} @@ -39,7 +42,7 @@ services: OHOS_VERSION: ${OHOS_VERSION:-} VFOX_FLUTTER_GITHUB_MIRROR: ${VFOX_FLUTTER_GITHUB_MIRROR:-} E2E_RETRY_ATTEMPTS: ${E2E_RETRY_ATTEMPTS:-} - command: ["go", "test", "-v", "-timeout", "55m", "-run", "TestE2E", "./..."] + command: ["go", "test", "-v", "-timeout", "55m", "-run", "TestE2E", "./windows"] volumes: e2e-go-modcache: diff --git a/tests/e2e/windows/e2e_test.go b/tests/e2e/windows/e2e_test.go index fe2a190..f58a541 100644 --- a/tests/e2e/windows/e2e_test.go +++ b/tests/e2e/windows/e2e_test.go @@ -1,10 +1,15 @@ -package e2e +// Package windows is the Windows container E2E suite, driven entirely from +// Go via testcontainers-go. +// +// TestE2E builds the image in tests/e2e/windows/Dockerfile once, then runs +// one long-lived container per (vfox version x flavor x mirror) combination +// and drives setup -> install -> verify through pwsh exec calls (setup.go, +// install.go, verify.go), asserting in Go. Target-independent logic (matrix +// expansion and friends) is reused from the common package. +package windows import ( "context" - "crypto/rand" - "encoding/hex" - "fmt" "os" "path/filepath" "strings" @@ -16,24 +21,15 @@ import ( tc "github.com/testcontainers/testcontainers-go" client "github.com/moby/moby/client" -) -// passThroughEnvs are host env knobs the suite honours. Forwarded only when -// set, so unset stays default. (The old e2e.ps1 only passed VFOX_VERSION, -// FLAVOR, VFOX_E2E_SLOT and FLUTTER_STORAGE_BASE_URL into the container; -// forwarding the rest is a strict improvement shared with the Linux suite.) -var passThroughEnvs = []string{ - "FLUTTER_VERSION", - "OHOS_VERSION", - "VFOX_FLUTTER_GITHUB_MIRROR", - "E2E_RETRY_ATTEMPTS", -} + "github.com/version-fox/vfox-flutter/tests/e2e/common" +) // TestE2E builds the image once, then runs one container per matrix combo. // Bounded parallelism via t.Parallel + semaphore (like E2E_MAX_JOBS). // // Full matrix runs in CI; locally scope it down, e.g.: -// VFOX_VERSION=latest FLAVOR=official MIRROR=default E2E_MAX_JOBS=1 go test -v +// VFOX_VERSION=latest FLAVOR=official MIRROR=default E2E_MAX_JOBS=1 go test -v ./windows/ func TestE2E(t *testing.T) { if testing.Short() { t.Skip("skip container E2E in short mode") @@ -42,7 +38,7 @@ func TestE2E(t *testing.T) { // Every container is terminated explicitly via t.Cleanup instead. t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") - arch, err := detectArch() + arch, err := common.DetectArch() if err != nil { t.Fatal(err) } @@ -59,7 +55,7 @@ func TestE2E(t *testing.T) { ctx := context.Background() buildImage(t, ctx, repoRoot, image, platform, baseImage) - combos, err := matrix() + combos, err := common.Matrix() if err != nil { t.Fatal(err) } @@ -70,7 +66,7 @@ func TestE2E(t *testing.T) { sem := make(chan struct{}, maxJobs()) for _, c := range combos { c := c - t.Run(c.slug(), func(t *testing.T) { + t.Run(c.Slug(), func(t *testing.T) { t.Parallel() sem <- struct{}{} defer func() { <-sem }() @@ -119,34 +115,23 @@ func buildImage(t *testing.T, ctx context.Context, repoRoot, image, platform, ba } } -// containerName returns a unique container name per run: the combo slug stays -// greppable in `docker ps`, the random suffix avoids clashes with leftovers -// from killed runs (the old ps1 runner detached anonymous names per batch). -func containerName(c combo) string { - var b [4]byte - if _, err := rand.Read(b[:]); err != nil { - return fmt.Sprintf("vfox-flutter-e2e-%s-%d", c.slug(), time.Now().UnixNano()) - } - return "vfox-flutter-e2e-" + c.slug() + "-" + hex.EncodeToString(b[:]) -} - // runOne starts one long-lived container per combo and drives the whole // E2E flow (setup -> preflight -> install -> verify) through pwsh exec // calls, asserting in Go. A combo passes when every phase exits 0 and the -// outputs carry the expected markers (same bar as the old e2e.ps1). -func runOne(t *testing.T, ctx context.Context, image, platform string, c combo) { +// outputs carry the expected markers. +func runOne(t *testing.T, ctx context.Context, image, platform string, c common.Combo) { t.Helper() - prefix := c.prefix(platform) + prefix := c.Prefix(platform) env := map[string]string{ - "VFOX_VERSION": c.vfox, - "FLAVOR": c.flavor, - "VFOX_E2E_SLOT": c.slug(), + "VFOX_VERSION": c.Vfox, + "FLAVOR": c.Flavor, + "VFOX_E2E_SLOT": c.Slug(), } - if c.mirror != "default" { - env["FLUTTER_STORAGE_BASE_URL"] = c.mirror + if c.Mirror != "default" { + env["FLUTTER_STORAGE_BASE_URL"] = c.Mirror } - for _, name := range passThroughEnvs { + for _, name := range common.PassThroughEnvs { if v, ok := os.LookupEnv(name); ok && v != "" { env[name] = v } @@ -166,7 +151,7 @@ func runOne(t *testing.T, ctx context.Context, image, platform string, c combo) ctr, err := provider.RunContainer(ctx, tc.ContainerRequest{ Image: image, ImagePlatform: platform, - Name: containerName(c), + Name: common.ContainerName(c.Slug()), Env: env, }) if err != nil { @@ -180,15 +165,15 @@ func runOne(t *testing.T, ctx context.Context, image, platform string, c combo) ctx, cancel := context.WithTimeout(ctx, 45*time.Minute) defer cancel() - flavor := c.flavor - mirror := c.mirror - version := resolveFlutterVersion(t, flavor) + flavor := c.Flavor + mirror := c.Mirror + version := common.ResolveFlutterVersion(t, flavor) procArch := procArchOfContainer(ctx, t, ctr) - t.Logf("=== vfox %s, flutter %s, %s, mirror %s, %s ===", c.vfox, version, flavor, mirror, procArch) + t.Logf("=== vfox %s, flutter %s, %s, mirror %s, %s ===", c.Vfox, version, flavor, mirror, procArch) - box := setupSlotEnv(ctx, t, ctr, c.slug()) + box := setupSlotEnv(ctx, t, ctr, c.Slug()) checkMirrorReachable(ctx, t, ctr, flavor, mirror) - setupVfox(ctx, t, ctr, box, c.vfox) + setupVfox(ctx, t, ctr, box, c.Vfox) checkBogusMirrorRejected(ctx, t, ctr, box, flavor, version) checkBogusGithubMirrorRejected(ctx, t, ctr, box, flavor, version) diff --git a/tests/e2e/windows/exec.go b/tests/e2e/windows/exec.go index e07e56f..9fb4527 100644 --- a/tests/e2e/windows/exec.go +++ b/tests/e2e/windows/exec.go @@ -1,30 +1,16 @@ -package e2e +package windows import ( "context" "io" - "os" - "strconv" - "strings" "testing" "time" tc "github.com/testcontainers/testcontainers-go" tcexec "github.com/testcontainers/testcontainers-go/exec" -) -// retryAttempts caps exec retries; honours $E2E_RETRY_ATTEMPTS (default 3, -// matching Invoke-WithRetry). Kept on the standard library: the established -// retry libraries (cenkalti/backoff, avast/retry-go) have no release in the -// last 6 months, so no third-party candidate qualifies. -func retryAttempts() int { - if v, ok := os.LookupEnv("E2E_RETRY_ATTEMPTS"); ok && strings.TrimSpace(v) != "" { - if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { - return n - } - } - return 3 -} + "github.com/version-fox/vfox-flutter/tests/e2e/common" +) // pwshPrelude mirrors the run.ps1/lib.ps1 headers: any cmdlet error aborts // and native failures propagate. Without it, pwsh exits 0 despite script @@ -62,11 +48,11 @@ func execOK(ctx context.Context, t *testing.T, ctr tc.Container, script string, return out } -// execRetry mirrors Invoke-WithRetry: run script up to retryAttempts() times -// with linear backoff (10s * attempt), logging each failed attempt. +// execRetry mirrors Invoke-WithRetry: run script up to common.RetryAttempts +// times with linear backoff (10s * attempt), logging each failed attempt. func execRetry(ctx context.Context, t *testing.T, ctr tc.Container, label, script string, extraEnv ...string) string { t.Helper() - max := retryAttempts() + max := common.RetryAttempts() var out string for attempt := 1; ; attempt++ { code, o := execRun(ctx, t, ctr, script, extraEnv...) diff --git a/tests/e2e/windows/go.mod b/tests/e2e/windows/go.mod deleted file mode 100644 index 7cca9fa..0000000 --- a/tests/e2e/windows/go.mod +++ /dev/null @@ -1,58 +0,0 @@ -module github.com/version-fox/vfox-flutter/tests/e2e/windows - -go 1.25.0 - -require ( - github.com/containerd/platforms v0.2.1 - github.com/moby/moby/client v0.5.0 - github.com/opencontainers/image-spec v1.1.1 - github.com/stretchr/testify v1.12.1 - github.com/testcontainers/testcontainers-go v0.44.0 -) - -require ( - dario.cat/mergo v1.0.2 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.7.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.10.1 // indirect - github.com/felixge/httpsnoop v1.1.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.18.6 // indirect - github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/moby/api v1.55.0 // indirect - github.com/moby/patternmatcher v0.6.1 // indirect - github.com/moby/sys/sequential v0.7.0 // indirect - github.com/moby/sys/user v0.4.0 // indirect - github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.26.6 // indirect - github.com/sirupsen/logrus v1.9.4 // indirect - github.com/tklauser/go-sysconf v0.4.0 // indirect - github.com/tklauser/numcpus v0.12.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/sys v0.47.0 // indirect -) diff --git a/tests/e2e/windows/go.sum b/tests/e2e/windows/go.sum deleted file mode 100644 index 5001fb9..0000000 --- a/tests/e2e/windows/go.sum +++ /dev/null @@ -1,122 +0,0 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -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/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= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= -github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= -github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= -github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= -github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= -github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= -github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= -github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= -github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= -github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= -github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= -github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= -github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI= -github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE= -github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= -github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= -github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= -github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= -go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= -pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/tests/e2e/windows/install.go b/tests/e2e/windows/install.go index d802b24..86e50aa 100644 --- a/tests/e2e/windows/install.go +++ b/tests/e2e/windows/install.go @@ -1,4 +1,4 @@ -package e2e +package windows import ( "context" diff --git a/tests/e2e/windows/matrix.go b/tests/e2e/windows/matrix.go deleted file mode 100644 index ca1dc83..0000000 --- a/tests/e2e/windows/matrix.go +++ /dev/null @@ -1,155 +0,0 @@ -// Package e2e is the Windows container E2E suite, driven entirely from Go -// via testcontainers-go. -// -// TestE2E builds the image in tests/e2e/windows/Dockerfile once, then runs -// one long-lived container per (vfox version x flavor x mirror) combination -// and drives setup -> install -> verify through pwsh exec calls (setup.go, -// install.go, verify.go), asserting in Go. matrix.go holds the matrix -// expansion. -package e2e - -import ( - "fmt" - "os" - "runtime" - "strconv" - "strings" -) - -// defaultMirrorCN is the default mainland-China Flutter storage mirror. -const defaultMirrorCN = "https://storage.flutter-io.cn" - -// combo is one matrix cell: a vfox version, a flutter flavor and a mirror. -type combo struct { - vfox string - flavor string - mirror string -} - -// prefix renders the log prefix shared with the old e2e.ps1: -// "vfox , , mirror , ". -func (c combo) prefix(platform string) string { - return fmt.Sprintf("vfox %s, %s, mirror %s, %s", c.vfox, c.flavor, c.mirror, platform) -} - -// slug renders a docker-safe container name fragment for the combo. -func (c combo) slug() string { - return c.vfox + "-" + c.flavor + "-" + sanitize(c.mirror) -} - -// sanitize replaces every non-ASCII-alphanumeric byte with '_' so arbitrary -// mirror URLs stay valid in container names (mirrors e2e.ps1). -func sanitize(s string) string { - var b strings.Builder - b.Grow(len(s)) - for i := 0; i < len(s); i++ { - ch := s[i] - if ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' { - b.WriteByte(ch) - } else { - b.WriteByte('_') - } - } - return b.String() -} - -// normalizeArch maps PROCESSOR_ARCHITECTURE values and ARCH aliases to -// docker arches. -func normalizeArch(s string) (string, error) { - switch strings.ToLower(s) { - case "x86_64", "x64", "amd64": - return "amd64", nil - case "aarch64", "arm64": - return "arm64", nil - default: - return "", fmt.Errorf("unknown architecture %s", s) - } -} - -// detectArch honours $ARCH, then $PROCESSOR_ARCHITECTURE (set on Windows -// hosts to AMD64/ARM64/x86), then falls back to the test binary's own arch. -func detectArch() (string, error) { - if arch, ok := os.LookupEnv("ARCH"); ok && strings.TrimSpace(arch) != "" { - return normalizeArch(strings.TrimSpace(arch)) - } - if arch, ok := os.LookupEnv("PROCESSOR_ARCHITECTURE"); ok && strings.TrimSpace(arch) != "" { - return normalizeArch(strings.TrimSpace(arch)) - } - return normalizeArch(runtime.GOARCH) -} - -// fields splits space-separated env lists (e.g. MIRROR="default "). -// Kept for the MIRROR knob, which e2e.ps1 splits on whitespace. -func fields(s string) []string { - return strings.Fields(s) -} - -// defaultFlavours mirrors the old e2e.ps1 rule: ohos publishes no -// windows-arm64 artefacts, so arm64 runs official only. -func defaultFlavours(arch string) []string { - if arch == "arm64" { - return []string{"official"} - } - return []string{"official", "ohos"} -} - -// defaultMirrors mirrors the old e2e.ps1 rule: the extra mirror only runs -// on amd64, where the full matrix is affordable. -func defaultMirrors(arch string) []string { - if arch == "arm64" { - return []string{"default"} - } - return []string{"default", defaultMirrorCN} -} - -// matrix expands the vfox x flavor x mirror combinations. Env overrides: -// VFOX_VERSION (default latest + main), FLAVOR and MIRROR (all split on -// whitespace, so FLAVOR="official ohos" works like on Linux). The mirror -// matrix only runs against latest vfox unless the caller explicitly pins -// MIRROR, to keep CI time bounded (same rule as the old e2e.ps1 and the -// Linux suite). -func matrix() ([]combo, error) { - arch, err := detectArch() - if err != nil { - return nil, err - } - - foxes := []string{"latest", "main"} - if v, ok := os.LookupEnv("VFOX_VERSION"); ok && len(fields(v)) > 0 { - foxes = fields(v) - } - flavours := defaultFlavours(arch) - if v, ok := os.LookupEnv("FLAVOR"); ok && len(fields(v)) > 0 { - flavours = fields(v) - } - mirrors := defaultMirrors(arch) - if v, ok := os.LookupEnv("MIRROR"); ok && len(fields(v)) > 0 { - mirrors = fields(v) - } - _, mirrorExplicit := os.LookupEnv("MIRROR") - mirrorExplicit = mirrorExplicit && len(fields(os.Getenv("MIRROR"))) > 0 - - var combos []combo - for _, vfox := range foxes { - for _, flavor := range flavours { - for _, mirror := range mirrors { - if !mirrorExplicit && mirror != "default" && vfox != "latest" { - continue - } - combos = append(combos, combo{vfox: vfox, flavor: flavor, mirror: mirror}) - } - } - } - return combos, nil -} - -// maxJobs bounds parallel containers; honours $E2E_MAX_JOBS (default 2, -// matching the old e2e.ps1). -func maxJobs() int { - if v, ok := os.LookupEnv("E2E_MAX_JOBS"); ok { - if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 1 { - return n - } - } - return 2 -} diff --git a/tests/e2e/windows/matrix_test.go b/tests/e2e/windows/matrix_test.go index 0ea9404..680cd71 100644 --- a/tests/e2e/windows/matrix_test.go +++ b/tests/e2e/windows/matrix_test.go @@ -1,129 +1,9 @@ -package e2e +package windows import ( "testing" ) -func setEnv(t *testing.T, kv map[string]string) { - t.Helper() - for k, v := range kv { - t.Setenv(k, v) - } -} - -func comboKeys(combos []combo) []string { - keys := make([]string, 0, len(combos)) - for _, c := range combos { - keys = append(keys, c.vfox+"/"+c.flavor+"/"+c.mirror) - } - return keys -} - -// Default amd64 matrix: latest runs both mirrors, main runs default only. -func TestMatrixDefaultAmd64(t *testing.T) { - setEnv(t, map[string]string{"ARCH": "amd64"}) - t.Setenv("VFOX_VERSION", "") - t.Setenv("FLAVOR", "") - t.Setenv("MIRROR", "") - - combos, err := matrix() - if err != nil { - t.Fatal(err) - } - if len(combos) == 0 { - t.Fatal("empty matrix") - } - t.Logf("combos: %v", comboKeys(combos)) - for _, c := range combos { - if c.mirror != "default" && c.vfox != "latest" { - t.Errorf("non-default mirror runs against %s vfox: %+v", c.vfox, c) - } - } -} - -// arm64 defaults: official flavor, default mirror only. -func TestMatrixDefaultArm64(t *testing.T) { - setEnv(t, map[string]string{ - "ARCH": "arm64", - "VFOX_VERSION": "latest main", - "FLAVOR": "", - "MIRROR": "", - }) - - combos, err := matrix() - if err != nil { - t.Fatal(err) - } - for _, c := range combos { - if c.flavor != "official" { - t.Errorf("arm64 flavor = %s, want official", c.flavor) - } - if c.mirror != "default" { - t.Errorf("arm64 mirror = %s, want default", c.mirror) - } - } - if len(combos) == 0 { - t.Fatal("empty matrix") - } -} - -// Explicit MIRROR pins the full mirror matrix even for non-latest vfox. -func TestMatrixExplicitMirror(t *testing.T) { - setEnv(t, map[string]string{ - "ARCH": "amd64", - "VFOX_VERSION": "main", - "FLAVOR": "official", - "MIRROR": "default https://storage.flutter-io.cn", - }) - - combos, err := matrix() - if err != nil { - t.Fatal(err) - } - if len(combos) != 2 { - t.Fatalf("combos = %v, want 2", comboKeys(combos)) - } -} - -// PROCESSOR_ARCHITECTURE values from real Windows hosts resolve correctly. -func TestMatrixProcessorArchitecture(t *testing.T) { - t.Setenv("ARCH", "") - t.Setenv("PROCESSOR_ARCHITECTURE", "ARM64") - t.Setenv("VFOX_VERSION", "latest") - t.Setenv("FLAVOR", "official") - t.Setenv("MIRROR", "default") - combos, err := matrix() - if err != nil { - t.Fatal(err) - } - if len(combos) != 1 { - t.Fatalf("combos = %v, want 1", comboKeys(combos)) - } -} - -func TestNormalizeArch(t *testing.T) { - for in, want := range map[string]string{ - "AMD64": "amd64", "x86_64": "amd64", "x64": "amd64", "amd64": "amd64", - "ARM64": "arm64", "aarch64": "arm64", "arm64": "arm64", - } { - got, err := normalizeArch(in) - if err != nil || got != want { - t.Errorf("normalizeArch(%q) = %q, %v; want %q", in, got, err, want) - } - } - if _, err := normalizeArch("x86"); err == nil { - t.Error("normalizeArch(x86) succeeded, want error") - } -} - -func TestSanitize(t *testing.T) { - got := combo{vfox: "latest", flavor: "official", mirror: "https://storage.flutter-io.cn"}.slug() - want := "latest-official-https___storage_flutter_io_cn" - if got != want { - t.Errorf("slug = %q, want %q", got, want) - } -} - func TestMaxJobsDefault(t *testing.T) { t.Setenv("E2E_MAX_JOBS", "") // empty means unset-equivalent for maxJobs (Atoi fails -> default 2) @@ -136,21 +16,6 @@ func TestMaxJobsDefault(t *testing.T) { } } -func TestResolveFlutterVersion(t *testing.T) { - t.Setenv("FLUTTER_VERSION", "") - t.Setenv("OHOS_VERSION", "") - if got := resolveFlutterVersion(t, "official"); got != "3.47.4" { - t.Errorf("official default = %q", got) - } - if got := resolveFlutterVersion(t, "ohos"); got != "3.41.10-ohos-1.0.0" { - t.Errorf("ohos default = %q", got) - } - t.Setenv("FLUTTER_VERSION", "3.99.0") - if got := resolveFlutterVersion(t, "official"); got != "3.99.0" { - t.Errorf("official override = %q", got) - } -} - func TestReleasesIndex(t *testing.T) { got := releasesIndex("https://storage.flutter-io.cn/") want := "https://storage.flutter-io.cn/flutter_infra_release/releases/releases_windows.json" @@ -159,30 +24,6 @@ func TestReleasesIndex(t *testing.T) { } } -func TestToolsDart(t *testing.T) { - out := "Flutter 3.47.4 • channel stable • https://github.com/flutter/flutter.git\n" + - "Framework • revision abc123 (4 weeks ago) • 2026-01-01\n" + - "Engine • revision def456\n" + - "Tools • Dart 3.10.0 • DevTools 2.48.0\n" - if got := toolsDart(out); got != "3.10.0" { - t.Errorf("toolsDart = %q, want 3.10.0", got) - } - if got := toolsDart("no tools here\n"); got != "" { - t.Errorf("toolsDart without Tools line = %q, want empty", got) - } -} - -func TestRetryAttemptsDefault(t *testing.T) { - t.Setenv("E2E_RETRY_ATTEMPTS", "") - if got := retryAttempts(); got != 3 { - t.Errorf("retryAttempts = %d, want 3", got) - } - t.Setenv("E2E_RETRY_ATTEMPTS", "5") - if got := retryAttempts(); got != 5 { - t.Errorf("retryAttempts = %d, want 5", got) - } -} - func TestPwshQuote(t *testing.T) { if got := pwshQuote(`C:\a\b`); got != `'C:\a\b'` { t.Errorf("pwshQuote = %q", got) diff --git a/tests/e2e/windows/platform.go b/tests/e2e/windows/platform.go new file mode 100644 index 0000000..c2d0f1e --- /dev/null +++ b/tests/e2e/windows/platform.go @@ -0,0 +1,11 @@ +package windows + +import ( + "github.com/version-fox/vfox-flutter/tests/e2e/common" +) + +// maxJobs bounds parallel containers; honours $E2E_MAX_JOBS (default 2, +// matching the old e2e.ps1). +func maxJobs() int { + return common.MaxJobs(2) +} diff --git a/tests/e2e/windows/run.ps1 b/tests/e2e/windows/run.ps1 new file mode 100644 index 0000000..3bd75c0 --- /dev/null +++ b/tests/e2e/windows/run.ps1 @@ -0,0 +1,21 @@ +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +# Entry point for the Windows container E2E suite on local hosts +# (see docs/e2e.md). Derives ARCH from the host so callers never set it by +# hand: the test matrix already falls back to the runner arch, and this only +# feeds the compose BASE_IMAGE build arg. An explicitly set $env:ARCH still +# wins (CI sets it per runner). +if ($env:ARCH -and $env:ARCH.Trim()) { + $arch = $env:ARCH.Trim().ToLowerInvariant() +} elseif ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { + $arch = 'arm64' +} else { + $arch = 'amd64' +} +$env:ARCH = $arch + +$compose = Join-Path $PSScriptRoot 'compose.yaml' +& docker compose -f $compose run --rm --build e2e +if ($LASTEXITCODE -ne 0) { throw "FAIL the e2e run exited with code $LASTEXITCODE" } +Write-Output "PASS the windows/$arch e2e run finished" diff --git a/tests/e2e/windows/setup.go b/tests/e2e/windows/setup.go index b570b6e..1272457 100644 --- a/tests/e2e/windows/setup.go +++ b/tests/e2e/windows/setup.go @@ -1,55 +1,30 @@ -package e2e +package windows import ( "context" "fmt" - "os" "strings" "testing" "github.com/stretchr/testify/require" tc "github.com/testcontainers/testcontainers-go" + + "github.com/version-fox/vfox-flutter/tests/e2e/common" ) -// NOTE: keep the Flutter defaults in sync with tests/e2e/linux/setup.go. -// The two Go modules share no config format, so the versions are duplicated -// on purpose. Bump both together. +// Flutter defaults live in the common package; only the Windows-specific +// toolchain pin stays here. const ( - defaultOfficialVersion = "3.47.4" - defaultOhosVersion = "3.41.10-ohos-1.0.0" // goWindowsVersion pins the Go toolchain MSI used to build vfox@main // inside the container (was setup.ps1). goWindowsVersion = "1.27.1" ) -const bogusMirror = "https://invalid.example.invalid" - // pwshQuote renders a single-quoted PowerShell string literal. func pwshQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" } -// resolveFlutterVersion honours $FLUTTER_VERSION / $OHOS_VERSION, else the -// defaults above (was lib.ps1 Resolve-FlutterVersion). -func resolveFlutterVersion(t *testing.T, flavor string) string { - t.Helper() - switch flavor { - case "official": - if v := os.Getenv("FLUTTER_VERSION"); v != "" { - return v - } - return defaultOfficialVersion - case "ohos": - if v := os.Getenv("OHOS_VERSION"); v != "" { - return v - } - return defaultOhosVersion - default: - t.Fatalf("unknown flavor %s", flavor) - return "" - } -} - // releasesIndex mirrors the preflight probe for windows. func releasesIndex(mirror string) string { return strings.TrimSuffix(mirror, "/") + "/flutter_infra_release/releases/releases_windows.json" @@ -246,7 +221,7 @@ func checkBogusMirrorRejected(ctx context.Context, t *testing.T, ctr tc.Containe } code, out := execRun(ctx, t, ctr, fmt.Sprintf("vfox install %s", pwshQuote("flutter@"+version)), - withEnv(box, "FLUTTER_STORAGE_BASE_URL="+bogusMirror)...) + withEnv(box, "FLUTTER_STORAGE_BASE_URL="+common.BogusMirror)...) if code == 0 { t.Fatalf("bogus mirror install unexpectedly succeeded\n--- output ---\n%s", out) } @@ -265,7 +240,7 @@ func checkBogusGithubMirrorRejected(ctx context.Context, t *testing.T, ctr tc.Co } code, out := execRun(ctx, t, ctr, fmt.Sprintf("vfox install %s", pwshQuote("flutter@"+version)), - withEnv(box, "VFOX_FLUTTER_GITHUB_MIRROR="+bogusMirror)...) + withEnv(box, "VFOX_FLUTTER_GITHUB_MIRROR="+common.BogusMirror)...) if code == 0 { t.Fatalf("bogus GitHub mirror install unexpectedly succeeded\n--- output ---\n%s", out) } diff --git a/tests/e2e/windows/verify.go b/tests/e2e/windows/verify.go index b254c3a..33310fc 100644 --- a/tests/e2e/windows/verify.go +++ b/tests/e2e/windows/verify.go @@ -1,4 +1,4 @@ -package e2e +package windows import ( "context" @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" tc "github.com/testcontainers/testcontainers-go" + + "github.com/version-fox/vfox-flutter/tests/e2e/common" ) // NOTE: the drift-warning strings asserted here are part of the plugin's @@ -54,17 +56,12 @@ func verifySdkLayout(ctx context.Context, t *testing.T, ctr tc.Container, box sl t.Log("PASS the OpenHarmony SDK is a git checkout with its engine pins tracked") } -// manifest is the anchored-install record the plugin writes into the SDK. -type manifest struct { - ExpectedHead string `json:"expected_head"` -} - // manifestAnchor reads .vfox-manifest and returns its expected_head. func manifestAnchor(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, sdk string) string { t.Helper() raw := execOK(ctx, t, ctr, "Get-Content -Raw "+pwshQuote(sdk+`\.vfox-manifest`), box.vars...) - var m manifest + var m common.Manifest if err := json.Unmarshal([]byte(raw), &m); err != nil { t.Fatalf("the .vfox-manifest is not valid JSON: %v\n--- actual ---\n%s", err, raw) } @@ -117,21 +114,6 @@ func verifyManifestDrift(ctx context.Context, t *testing.T, ctr tc.Container, bo require.NotContains(t, use(), "has drifted", "a restored SDK reports no drift") } -// toolsDart extracts the Dart version from `flutter --version` output: -// (($flutter -split "`n" | Where-Object { $_ -like '*Tools*' }) -split ' ')[3]. -// (Duplicated from tests/e2e/linux/verify.go; the two Go modules share no -// helpers.) -func toolsDart(flutterVersion string) string { - for _, line := range strings.Split(flutterVersion, "\n") { - if strings.Contains(line, "Tools") { - if f := strings.Fields(line); len(f) >= 4 { - return f[3] - } - } - } - return "" -} - // 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) { @@ -148,7 +130,7 @@ func verifyToolchain(ctx context.Context, t *testing.T, ctr tc.Container, box sl "git -C "+pwshQuote(sdk)+" rev-parse HEAD", box.vars...)) require.Contains(t, flutter, head[:10], "flutter --version revision") - dartWant := toolsDart(flutter) + dartWant := common.ToolsDart(flutter) require.NotEmpty(t, dartWant, "flutter --version has no Tools line:\n"+flutter) require.Contains(t, dart, dartWant, "dart --version") }