From 9e9fb1a8223b6f6ccf29702ee8e8584bc369a424 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Fri, 6 Mar 2026 03:29:47 +0000 Subject: [PATCH] ci: add Windows runner and cross-compile jobs for winget - New 'windows' job runs unit tests on windows-latest with coverage - New 'cross-compile' matrix job builds for windows/darwin/freebsd/openbsd - Integration test file for winget (gated behind integration build tag) - Windows coverage included in codecov upload --- .github/workflows/integration.yml | 34 ++++++- winget/winget_integration_test.go | 156 ++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 winget/winget_integration_test.go diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index b15125a..5296e6c 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -188,10 +188,40 @@ jobs: name: coverage-flatpak path: coverage-flatpak.out + cross-compile: + name: Cross Compile + runs-on: ubuntu-latest + strategy: + matrix: + goos: [windows, darwin, freebsd, openbsd] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Build for ${{ matrix.goos }} + run: GOOS=${{ matrix.goos }} go build ./... + + windows: + name: Windows (winget) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Unit tests + run: go test -race -coverprofile=coverage-windows.out ./winget/ ./detect/ + - uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-windows + path: coverage-windows.out + codecov: name: Upload Coverage runs-on: ubuntu-latest - needs: [lint, unit-tests, debian, ubuntu, fedora-dnf4, fedora-dnf5, alpine, arch, flatpak] + needs: [lint, unit-tests, debian, ubuntu, fedora-dnf4, fedora-dnf5, alpine, arch, flatpak, windows, cross-compile] if: always() steps: - uses: actions/checkout@v4 @@ -203,7 +233,7 @@ jobs: run: ls -la coverage-*.out 2>/dev/null || echo "No coverage files found" - uses: codecov/codecov-action@v5 with: - files: coverage-unit.out,coverage-debian.out,coverage-ubuntu-apt.out,coverage-ubuntu-snap.out,coverage-fedora39.out,coverage-fedora-latest.out,coverage-alpine.out,coverage-arch.out,coverage-flatpak.out + files: coverage-unit.out,coverage-debian.out,coverage-ubuntu-apt.out,coverage-ubuntu-snap.out,coverage-fedora39.out,coverage-fedora-latest.out,coverage-alpine.out,coverage-arch.out,coverage-flatpak.out,coverage-windows.out fail_ci_if_error: false env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/winget/winget_integration_test.go b/winget/winget_integration_test.go new file mode 100644 index 0000000..cdb12a4 --- /dev/null +++ b/winget/winget_integration_test.go @@ -0,0 +1,156 @@ +//go:build integration + +package winget_test + +import ( + "context" + "testing" + + "github.com/gogrlx/snack" + "github.com/gogrlx/snack/winget" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntegration_Winget(t *testing.T) { + var mgr snack.Manager = winget.New() + if !mgr.Available() { + t.Skip("winget not available") + } + ctx := context.Background() + + assert.Equal(t, "winget", mgr.Name()) + + caps := snack.GetCapabilities(mgr) + assert.True(t, caps.VersionQuery, "winget should support VersionQuery") + assert.True(t, caps.RepoManagement, "winget should support RepoManagement") + assert.True(t, caps.NameNormalize, "winget should support NameNormalize") + assert.True(t, caps.PackageUpgrade, "winget should support PackageUpgrade") + assert.False(t, caps.Hold) + assert.False(t, caps.Clean) + assert.False(t, caps.FileOwnership) + assert.False(t, caps.KeyManagement) + assert.False(t, caps.Groups) + assert.False(t, caps.DryRun) + + t.Run("Update", func(t *testing.T) { + require.NoError(t, mgr.Update(ctx)) + }) + + t.Run("Search", func(t *testing.T) { + pkgs, err := mgr.Search(ctx, "Microsoft.PowerToys") + require.NoError(t, err) + require.NotEmpty(t, pkgs) + t.Logf("search results: %d", len(pkgs)) + }) + + t.Run("Search_NoResults", func(t *testing.T) { + pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999zzz") + require.NoError(t, err) + assert.Empty(t, pkgs) + }) + + t.Run("Info", func(t *testing.T) { + pkg, err := mgr.Info(ctx, "Microsoft.PowerToys") + require.NoError(t, err) + require.NotNil(t, pkg) + assert.Equal(t, "Microsoft.PowerToys", pkg.Name) + assert.NotEmpty(t, pkg.Version) + t.Logf("PowerToys version: %s", pkg.Version) + }) + + t.Run("Info_NotFound", func(t *testing.T) { + _, err := mgr.Info(ctx, "xyznonexistentpackage999.notreal") + assert.Error(t, err) + }) + + t.Run("IsInstalled_False", func(t *testing.T) { + installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999.notreal") + require.NoError(t, err) + assert.False(t, installed) + }) + + t.Run("List", func(t *testing.T) { + pkgs, err := mgr.List(ctx) + require.NoError(t, err) + t.Logf("installed packages: %d", len(pkgs)) + // Windows runners should have at least some packages + assert.NotEmpty(t, pkgs) + }) + + // --- VersionQuerier --- + t.Run("VersionQuerier", func(t *testing.T) { + vq, ok := mgr.(snack.VersionQuerier) + require.True(t, ok) + + t.Run("LatestVersion", func(t *testing.T) { + ver, err := vq.LatestVersion(ctx, "Microsoft.PowerToys") + require.NoError(t, err) + assert.NotEmpty(t, ver) + t.Logf("PowerToys latest: %s", ver) + }) + + t.Run("LatestVersion_NotFound", func(t *testing.T) { + _, err := vq.LatestVersion(ctx, "xyznonexistentpackage999.notreal") + assert.Error(t, err) + }) + + t.Run("ListUpgrades", func(t *testing.T) { + pkgs, err := vq.ListUpgrades(ctx) + require.NoError(t, err) + t.Logf("upgradable packages: %d", len(pkgs)) + }) + + t.Run("VersionCmp", func(t *testing.T) { + tests := []struct { + v1, v2 string + want int + }{ + {"1.0", "2.0", -1}, + {"2.0", "1.0", 1}, + {"1.0", "1.0", 0}, + {"1.0.1", "1.0.0", 1}, + } + for _, tt := range tests { + cmp, err := vq.VersionCmp(ctx, tt.v1, tt.v2) + require.NoError(t, err, "VersionCmp(%s, %s)", tt.v1, tt.v2) + assert.Equal(t, tt.want, cmp, "VersionCmp(%s, %s)", tt.v1, tt.v2) + } + }) + }) + + // --- RepoManager --- + t.Run("RepoManager", func(t *testing.T) { + rm, ok := mgr.(snack.RepoManager) + require.True(t, ok) + + t.Run("ListRepos", func(t *testing.T) { + repos, err := rm.ListRepos(ctx) + require.NoError(t, err) + require.NotEmpty(t, repos) + t.Logf("sources: %d", len(repos)) + // Default sources should include "winget" + found := false + for _, r := range repos { + if r.Name == "winget" { + found = true + break + } + } + assert.True(t, found, "winget source should be in list") + }) + }) + + // --- NameNormalizer --- + t.Run("NameNormalizer", func(t *testing.T) { + nn, ok := mgr.(snack.NameNormalizer) + require.True(t, ok) + + assert.Equal(t, "Microsoft.PowerToys", nn.NormalizeName("Microsoft.PowerToys")) + assert.Equal(t, "Git.Git", nn.NormalizeName(" Git.Git ")) + + name, arch := nn.ParseArch("Microsoft.PowerToys") + assert.Equal(t, "Microsoft.PowerToys", name) + assert.Empty(t, arch) + }) +}