From 6ac389a182beb5ffcf81e2c264b6c7a1f842d6c8 Mon Sep 17 00:00:00 2001 From: seph Date: Fri, 6 Mar 2026 08:02:10 -0500 Subject: [PATCH 1/6] Tests from 2615 --- Makefile | 2 +- ee/tables/ci/performance.go | 23 +++ .../windowsupdate/windowsupdate_test.go | 170 ++++++++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 pkg/windows/windowsupdate/windowsupdate_test.go diff --git a/Makefile b/Makefile index 2090b37403..4e6e96e0a7 100644 --- a/Makefile +++ b/Makefile @@ -233,7 +233,7 @@ test: generate # -run=^$ will never match any of our regular non-benchmark tests, ensuring those don't run during benchmarking test-bench-tables: generate - go test ./ee/tables/... ./pkg/osquery/table/... -bench=. -count=20 -run=^$ -benchmem + go test ./ee/tables/... ./pkg/windows/windowsupdate/... ./pkg/osquery/table/... -bench=. -count=20 -run=^$ -benchmem ## ## Lint diff --git a/ee/tables/ci/performance.go b/ee/tables/ci/performance.go index 40553f918d..b74a1f1c7b 100644 --- a/ee/tables/ci/performance.go +++ b/ee/tables/ci/performance.go @@ -23,3 +23,26 @@ func ReportNonGolangMemoryUsage(b *testing.B, baselineStats *performance.Perform b.ReportMetric(float64(nonGolangMemDifferenceInBytes)/float64(b.N), "non-golang-B/op") } + +// RequireNonGolangMemoryBelowThreshold reports the non-Go memory metric and +// fails the benchmark if the per-operation growth exceeds maxBytesPerOp. This +// is useful for catching native memory leaks (e.g. COM IUnknown/VARIANT leaks) +// that live outside Go's garbage collector. +func RequireNonGolangMemoryBelowThreshold(b *testing.B, baselineStats *performance.PerformanceStats, maxBytesPerOp uint64) { + b.Helper() + + statsAfter, err := performance.CurrentProcessStats(b.Context()) + require.NoError(b, err) + var nonGolangMemDifferenceInBytes uint64 = 0 + if statsAfter.MemInfo.NonGoMemUsage > baselineStats.MemInfo.NonGoMemUsage { + nonGolangMemDifferenceInBytes = statsAfter.MemInfo.NonGoMemUsage - baselineStats.MemInfo.NonGoMemUsage + } + + perOp := nonGolangMemDifferenceInBytes / uint64(b.N) + b.ReportMetric(float64(perOp), "non-golang-B/op") + + require.LessOrEqual(b, perOp, maxBytesPerOp, + "non-Go memory grew %d B/op (total %d B over %d iterations), exceeding threshold of %d B/op — possible native memory leak", + perOp, nonGolangMemDifferenceInBytes, b.N, maxBytesPerOp, + ) +} diff --git a/pkg/windows/windowsupdate/windowsupdate_test.go b/pkg/windows/windowsupdate/windowsupdate_test.go new file mode 100644 index 0000000000..6c8c257458 --- /dev/null +++ b/pkg/windows/windowsupdate/windowsupdate_test.go @@ -0,0 +1,170 @@ +//go:build windows +// +build windows + +package windowsupdate + +import ( + "testing" + + comshim "github.com/NozomiNetworks/go-comshim" + "github.com/kolide/launcher/v2/ee/tables/ci" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func initCOM(t *testing.T) { + t.Helper() + require.NoError(t, comshim.TryAdd(1), "initializing COM") + t.Cleanup(comshim.Done) +} + +func initCOMBench(b *testing.B) { + b.Helper() + require.NoError(b, comshim.TryAdd(1), "initializing COM") + b.Cleanup(comshim.Done) +} + +func TestNewUpdateSession(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + // ClientApplicationID is a string (may be empty on a default session) + assert.IsType(t, "", session.ClientApplicationID) + // ReadOnly should be a bool; default sessions are not read-only + assert.False(t, session.ReadOnly) +} + +func TestCreateUpdateSearcher(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + searcher, err := session.CreateUpdateSearcher() + require.NoError(t, err, "CreateUpdateSearcher") + + // ServerSelection is an enum: ssDefault(0), ssManagedServer(1), ssWindowsUpdate(2), ssOthers(3) + assert.GreaterOrEqual(t, searcher.ServerSelection, int32(0)) + assert.LessOrEqual(t, searcher.ServerSelection, int32(3)) + + assert.IsType(t, "", searcher.ServiceID) +} + +func TestGetTotalHistoryCount(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + searcher, err := session.CreateUpdateSearcher() + require.NoError(t, err, "CreateUpdateSearcher") + + count, err := searcher.GetTotalHistoryCount() + require.NoError(t, err, "GetTotalHistoryCount") + assert.GreaterOrEqual(t, count, int32(0), "history count should be non-negative") +} + +func TestQueryHistorySmall(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + searcher, err := session.CreateUpdateSearcher() + require.NoError(t, err, "CreateUpdateSearcher") + + totalCount, err := searcher.GetTotalHistoryCount() + require.NoError(t, err, "GetTotalHistoryCount") + + if totalCount == 0 { + t.Skip("no update history entries on this machine, skipping") + } + + // Query a small number of entries to keep the test fast + queryCount := totalCount + if queryCount > 3 { + queryCount = 3 + } + + entries, err := searcher.QueryHistory(0, queryCount) + require.NoError(t, err, "QueryHistory") + require.Len(t, entries, int(queryCount)) + + for i, entry := range entries { + assert.NotEmpty(t, entry.Title, "entry[%d].Title should not be empty", i) + + // OperationResultCode enum: orcNotStarted(0), orcInProgress(1), orcSucceeded(2), + // orcSucceededWithErrors(3), orcFailed(4), orcAborted(5) + assert.GreaterOrEqual(t, entry.ResultCode, int32(0), "entry[%d].ResultCode", i) + assert.LessOrEqual(t, entry.ResultCode, int32(5), "entry[%d].ResultCode", i) + + // UpdateOperation enum: uoInstallation(1), uoUninstallation(2) + assert.GreaterOrEqual(t, entry.Operation, int32(1), "entry[%d].Operation", i) + assert.LessOrEqual(t, entry.Operation, int32(2), "entry[%d].Operation", i) + + // UpdateIdentity should be populated + if assert.NotNil(t, entry.UpdateIdentity, "entry[%d].UpdateIdentity", i) { + assert.NotEmpty(t, entry.UpdateIdentity.UpdateID, "entry[%d].UpdateIdentity.UpdateID", i) + } + } +} + +// BenchmarkQueryHistory exercises the full COM lifecycle path in a loop: +// session creation, searcher creation, history query with real VARIANT +// extraction and IDispatch Release. The non-golang-B/op metric captures +// native memory growth -- this is where COM leaks from missing +// Release()/Clear() calls would show up. The test fails if per-op +// native growth exceeds the threshold. +func BenchmarkQueryHistory(b *testing.B) { + initCOMBench(b) + + // Verify there's history to query; skip if not. + session, err := NewUpdateSession() + require.NoError(b, err) + searcher, err := session.CreateUpdateSearcher() + require.NoError(b, err) + totalCount, err := searcher.GetTotalHistoryCount() + require.NoError(b, err) + session.Release() + + if totalCount == 0 { + b.Skip("no update history entries on this machine") + } + + queryCount := totalCount + if queryCount > 5 { + queryCount = 5 + } + + baselineStats := ci.BaselineStats(b) + b.ReportAllocs() + b.ResetTimer() + + for range b.N { + session, err := NewUpdateSession() + require.NoError(b, err) + + searcher, err := session.CreateUpdateSearcher() + require.NoError(b, err) + + entries, err := searcher.QueryHistory(0, queryCount) + require.NoError(b, err) + require.NotEmpty(b, entries) + + session.Release() + } + + // 64 KiB per op is generous — a leaking implementation easily exceeds + // this after the benchmark framework scales up b.N. + ci.RequireNonGolangMemoryBelowThreshold(b, baselineStats, 64*1024) +} From 6db602c29e3858156a03ffc6f50642bd85540966 Mon Sep 17 00:00:00 2001 From: seph Date: Fri, 6 Mar 2026 08:16:24 -0500 Subject: [PATCH 2/6] fix benchmarks --- .github/workflows/go.yml | 911 +++++++++--------- .../windowsupdate/windowsupdate_test.go | 108 --- 2 files changed, 454 insertions(+), 565 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 00a4261ced..a2c168da7f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -4,13 +4,12 @@ on: workflow_dispatch: push: branches: [main, master] - tags: '*' + tags: "*" pull_request: - branches: '**' + branches: "**" merge_group: types: [checks_requested] - jobs: # Our non-containerized launcher builds -- macOS and Windows. Linux is handled separately # below to preserve Ubuntu 20.04 support. @@ -24,70 +23,70 @@ jobs: - macos-14 - windows-latest steps: - - name: Check out code - id: checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # need a full checkout for `git describe` - - - name: Setup Go - uses: actions/setup-go@v6 - with: - go-version-file: './go.mod' - check-latest: true - cache: false - id: go - - # use bash, because the powershell syntax is different and this is a cross platform workflow - - id: go-cache-paths - shell: bash - run: | - echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" - echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - - - name: Go Build Cache - uses: actions/cache@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - - - name: Go Mod Cache - uses: actions/cache@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - - - name: Get dependencies - run: make deps - - - name: Build (debug) - if: github.ref_type != 'tag' - shell: bash - run: MAKE_debugsymbols=true make -j2 github-build - - - name: Build - if: github.ref_type == 'tag' - run: make -j2 github-build - - - name: Check macOS build target - if: contains(matrix.os, 'macos') - # this uses grep's exit code - run: otool -l build/launcher | grep -A1 "minos 11" - - - name: Lipo - run: make github-lipo - if: ${{ contains(matrix.os, 'macos') }} - - - name: App Bundle - run: make github-launcherapp - if: ${{ contains(matrix.os, 'macos') }} - - - name: Cache build output - uses: actions/cache@v5 - with: - path: ./build - key: ${{ runner.os }}-${{ github.run_id }} - enableCrossOsArchive: true + - name: Check out code + id: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # need a full checkout for `git describe` + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version-file: "./go.mod" + check-latest: true + cache: false + id: go + + # use bash, because the powershell syntax is different and this is a cross platform workflow + - id: go-cache-paths + shell: bash + run: | + echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" + echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" + + - name: Go Build Cache + uses: actions/cache@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + + - name: Go Mod Cache + uses: actions/cache@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + + - name: Get dependencies + run: make deps + + - name: Build (debug) + if: github.ref_type != 'tag' + shell: bash + run: MAKE_debugsymbols=true make -j2 github-build + + - name: Build + if: github.ref_type == 'tag' + run: make -j2 github-build + + - name: Check macOS build target + if: contains(matrix.os, 'macos') + # this uses grep's exit code + run: otool -l build/launcher | grep -A1 "minos 11" + + - name: Lipo + run: make github-lipo + if: ${{ contains(matrix.os, 'macos') }} + + - name: App Bundle + run: make github-launcherapp + if: ${{ contains(matrix.os, 'macos') }} + + - name: Cache build output + uses: actions/cache@v5 + with: + path: ./build + key: ${{ runner.os }}-${{ github.run_id }} + enableCrossOsArchive: true # Our containerized launcher build -- we need to build launcher on Ubuntu 20.04 # in order to continue to support that platform, but that GH runner has been EOL'ed -- @@ -97,67 +96,67 @@ jobs: runs-on: ubuntu-22.04 container: ubuntu:20.04 # Required to support launcher on Ubuntu 20.04 steps: - # zstd is needed so we can restore cache later -- see https://github.com/actions/cache/issues/1455#issuecomment-2328358604 - - name: Install build dependencies - run: | - apt-get -y update - apt-get -y install build-essential ca-certificates openssl git zstd - update-ca-certificates - - - name: Check out code - id: checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # need a full checkout for `git describe` - - - name: Ignore dubious ownership - run: git config --global --add safe.directory /__w/launcher/launcher - - - name: Setup Go - uses: actions/setup-go@v6 - with: - go-version-file: './go.mod' - check-latest: true - cache: false - id: go - - - id: go-cache-paths - run: | - echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" - echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - - - name: Go Build Cache - uses: actions/cache@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('go.sum') }} - - - name: Go Mod Cache - uses: actions/cache@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('go.sum') }} - - - name: Get dependencies - run: make deps - - - name: Set up zig - uses: mlugg/setup-zig@v2 - - - name: Build (debug) - if: github.ref_type != 'tag' - run: MAKE_debugsymbols=true make -j2 github-build - - - name: Build - if: github.ref_type == 'tag' - run: make -j2 github-build - - - name: Cache build output - uses: actions/cache@v5 - with: - path: ./build - key: ${{ runner.os }}-${{ github.run_id }} - enableCrossOsArchive: true + # zstd is needed so we can restore cache later -- see https://github.com/actions/cache/issues/1455#issuecomment-2328358604 + - name: Install build dependencies + run: | + apt-get -y update + apt-get -y install build-essential ca-certificates openssl git zstd + update-ca-certificates + + - name: Check out code + id: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # need a full checkout for `git describe` + + - name: Ignore dubious ownership + run: git config --global --add safe.directory /__w/launcher/launcher + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version-file: "./go.mod" + check-latest: true + cache: false + id: go + + - id: go-cache-paths + run: | + echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" + echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" + + - name: Go Build Cache + uses: actions/cache@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-go-build-${{ hashFiles('go.sum') }} + + - name: Go Mod Cache + uses: actions/cache@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('go.sum') }} + + - name: Get dependencies + run: make deps + + - name: Set up zig + uses: mlugg/setup-zig@v2 + + - name: Build (debug) + if: github.ref_type != 'tag' + run: MAKE_debugsymbols=true make -j2 github-build + + - name: Build + if: github.ref_type == 'tag' + run: make -j2 github-build + + - name: Cache build output + uses: actions/cache@v5 + with: + path: ./build + key: ${{ runner.os }}-${{ github.run_id }} + enableCrossOsArchive: true # this job captures the version of launcher on one of the runners then that version is # compared to the version of all other runners during exec testing. This is to ensure @@ -171,24 +170,24 @@ jobs: outputs: version: ${{ steps.version.outputs.version }} steps: - # Needed so we can restore cache -- see https://github.com/actions/cache/issues/1455#issuecomment-2328358604 - - name: Install zstd - run: | - apt-get -y update - apt-get -y install zstd - - name: cache restore build output - uses: actions/cache/restore@v5 - with: - path: ./build - key: ${{ runner.os }}-${{ github.run_id }} - fail-on-cache-miss: true # Need launcher build from cache to run it below - enableCrossOsArchive: true - - - id: version - name: Launcher Version - working-directory: build - shell: bash - run: ./launcher --version 2>/dev/null | awk '/version /{print "version="$4}' >> "$GITHUB_OUTPUT" + # Needed so we can restore cache -- see https://github.com/actions/cache/issues/1455#issuecomment-2328358604 + - name: Install zstd + run: | + apt-get -y update + apt-get -y install zstd + - name: cache restore build output + uses: actions/cache/restore@v5 + with: + path: ./build + key: ${{ runner.os }}-${{ github.run_id }} + fail-on-cache-miss: true # Need launcher build from cache to run it below + enableCrossOsArchive: true + + - id: version + name: Launcher Version + working-directory: build + shell: bash + run: ./launcher --version 2>/dev/null | awk '/version /{print "version="$4}' >> "$GITHUB_OUTPUT" launcher_test: name: test @@ -204,55 +203,55 @@ jobs: - macos-14 - windows-latest steps: - - name: Check out code - id: checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # need a full checkout for `git describe` - - - name: Setup Go - uses: actions/setup-go@v6 - with: - go-version-file: './go.mod' - check-latest: true - cache: false - - - name: cache restore - build - uses: actions/cache/restore@v5 - with: - path: ./build - key: ${{ runner.os }}-${{ github.run_id }} - enableCrossOsArchive: true - - - id: go-cache-paths - shell: bash - run: | - echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" - echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - - - name: cache restore - GOCACHE - uses: actions/cache/restore@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - enableCrossOsArchive: true - - - name: cache restore - GOMODCACHE - uses: actions/cache/restore@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - enableCrossOsArchive: true - - - name: Test - run: make test - - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: ${{ runner.os }}-coverage.out - path: ./coverage.out - if-no-files-found: error + - name: Check out code + id: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # need a full checkout for `git describe` + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version-file: "./go.mod" + check-latest: true + cache: false + + - name: cache restore - build + uses: actions/cache/restore@v5 + with: + path: ./build + key: ${{ runner.os }}-${{ github.run_id }} + enableCrossOsArchive: true + + - id: go-cache-paths + shell: bash + run: | + echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" + echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" + + - name: cache restore - GOCACHE + uses: actions/cache/restore@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + enableCrossOsArchive: true + + - name: cache restore - GOMODCACHE + uses: actions/cache/restore@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + enableCrossOsArchive: true + + - name: Test + run: make test + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-coverage.out + path: ./coverage.out + if-no-files-found: error launcher_table_test: name: Launcher table performance test @@ -268,113 +267,112 @@ jobs: - macos-14 - windows-latest steps: - - name: Check out code - id: checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # need a full checkout for `git describe` - - - name: Setup Go - uses: actions/setup-go@v6 - with: - go-version-file: './go.mod' - check-latest: true - cache: false - - - name: cache restore - build - uses: actions/cache/restore@v5 - with: - path: ./build - key: ${{ runner.os }}-${{ github.run_id }} - enableCrossOsArchive: true - - - id: go-cache-paths - shell: bash - run: | - echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" - echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - - - name: cache restore - GOCACHE - uses: actions/cache/restore@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - enableCrossOsArchive: true - - - name: cache restore - GOMODCACHE - uses: actions/cache/restore@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - enableCrossOsArchive: true - - - name: Test - run: make test-bench-tables > benchmark.txt - - - name: Display results - shell: bash - run: cat ./benchmark.txt - - - name: Get run ID for last launcher table performance test on main - id: main-run-id - if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - echo "main-run-id=$(gh run list --repo kolide/launcher --branch main --workflow ci --limit 1 --json databaseId -q '.[].databaseId')" >> "$GITHUB_OUTPUT" - - - name: Download benchmark from main - id: download-benchmark - if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' - uses: actions/download-artifact@v4 - continue-on-error: true - with: - name: ${{ runner.os }}-benchmark.txt - github-token: ${{ github.token }} # Grants permission to read from other workflow runs - run-id: ${{ steps.main-run-id.outputs.main-run-id }} - path: main - - - name: Install benchstat - if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' && steps.download-benchmark.outcome == 'success' - run: go install golang.org/x/perf/cmd/benchstat@latest - - - name: Compare performance against main - if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' && steps.download-benchmark.outcome == 'success' - shell: bash - run: | - benchstat main/benchmark.txt ./benchmark.txt > benchstat.txt - cat ./benchstat.txt - - # We want to check for performance regressions. - # The regex matches lines that look like: - # AppIcons-3 5.606m ± 10% 10012.759m ± 0% +178505.57% (p=0.000 n=10) - # Format is: ± ± + - # We're capturing the percent change against main, whenever it is a performance regression (positive delta). - # We only care when it's at least a 1% change (so we ignore e.g. +0.02%). - # When it's a negative change (e.g. -178505.57%), that's a performance improvement. - # When there's no difference, benchstat outputs a ~ instead. - REGRESSIONS=$(grep -E '^.+-\d+\s+(?:\d+\.?\d*[a-zA-Z]*\s±\s\d+%\s+){2}(\+[1-9]+\.?\d*%)' ./benchstat.txt || :) - if [ -n "$REGRESSIONS" ] - then - echo "Performance regressions found:" - echo "$REGRESSIONS" - exit 1 - fi - - - name: Skip benchmark comparison (no baseline) - if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' && steps.download-benchmark.outcome != 'success' - shell: bash - run: | - echo "::notice::Skipping benchmark comparison - no baseline found on main branch. This is expected for new benchmarks." - - - name: Save benchmark results for main - if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 - with: - name: ${{ runner.os }}-benchmark.txt - path: ./benchmark.txt - if-no-files-found: error + - name: Check out code + id: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # need a full checkout for `git describe` + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version-file: "./go.mod" + check-latest: true + cache: false + + - name: cache restore - build + uses: actions/cache/restore@v5 + with: + path: ./build + key: ${{ runner.os }}-${{ github.run_id }} + enableCrossOsArchive: true + + - id: go-cache-paths + shell: bash + run: | + echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" + echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" + + - name: cache restore - GOCACHE + uses: actions/cache/restore@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + enableCrossOsArchive: true + + - name: cache restore - GOMODCACHE + uses: actions/cache/restore@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + enableCrossOsArchive: true + + - name: Test + run: make test-bench-tables > benchmark.txt + + - name: Display results + shell: bash + run: cat ./benchmark.txt + + - name: Get run ID for last launcher table performance test on main + id: main-run-id + if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "main-run-id=$(gh run list --repo kolide/launcher --branch main --workflow ci --limit 1 --json databaseId -q '.[].databaseId')" >> "$GITHUB_OUTPUT" + + - name: Download benchmark from main + id: download-benchmark + if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: ${{ runner.os }}-benchmark.txt + github-token: ${{ github.token }} # Grants permission to read from other workflow runs + run-id: ${{ steps.main-run-id.outputs.main-run-id }} + path: main + + - name: Install benchstat + if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' && steps.download-benchmark.outcome == 'success' + run: go install golang.org/x/perf/cmd/benchstat@latest + + - name: Compare performance against main + if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' && steps.download-benchmark.outcome == 'success' + shell: bash + run: | + benchstat main/benchmark.txt ./benchmark.txt > benchstat.txt + cat ./benchstat.txt + + # We want to check for performance regressions. + # The regex matches lines that look like: + # AppIcons-3 5.606m ± 10% 10012.759m ± 0% +178505.57% (p=0.000 n=10) + # Format is: ± ± + + # We're capturing the percent change against main, whenever it is a performance regression (positive delta). + # We only care when it's at least a 1% change (so we ignore e.g. +0.02%). + # When it's a negative change (e.g. -178505.57%), that's a performance improvement. + # When there's no difference, benchstat outputs a ~ instead. + REGRESSIONS=$(grep -E '^.+-\d+\s+(?:\d+\.?\d*[a-zA-Z]*\s±\s\d+%\s+){2}(\+[1-9]+\.?\d*%)' ./benchstat.txt || :) + if [ -n "$REGRESSIONS" ] + then + echo "Performance regressions found:" + echo "$REGRESSIONS" + exit 1 + fi + + - name: Skip benchmark comparison (no baseline) + if: github.ref != 'refs/heads/main' && github.ref_type != 'tag' && steps.download-benchmark.outcome != 'success' + shell: bash + run: | + echo "::notice::Skipping benchmark comparison - no baseline found on main branch. This is expected for new benchmarks." + + - name: Save benchmark results + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-benchmark.txt + path: ./benchmark.txt + if-no-files-found: error exec_testing: name: Exec Test @@ -394,37 +392,37 @@ jobs: - version_baseline # version_baseline implies build_linux - build # need the other builds too steps: - - name: cache restore build output - uses: actions/cache/restore@v5 - with: - path: ./build - key: ${{ runner.os }}-${{ github.run_id }} - fail-on-cache-miss: true # If we can't restore the cache, then we won't have a launcher binary to run below - enableCrossOsArchive: true - - - name: Launcher Version - working-directory: build - shell: bash - run: | - ./launcher --version - thisVersion=$(./launcher --version 2>/dev/null | grep "version" | awk '{print $4}') - baseVersion="${{ needs.version_baseline.outputs.version }}" - if [[ "$thisVersion" != "$baseVersion" ]]; then - printf "launcher version %s does not match baseline version %s" "$thisVersion" "$baseVersion" - exit 1 - fi - - - name: Download Osquery - working-directory: build - run: ./launcher download-osquery --directory . - - - name: Osquery Version - working-directory: build - run: ./osqueryd --version - - - name: Launcher Doctor - working-directory: build - run: ./launcher doctor + - name: cache restore build output + uses: actions/cache/restore@v5 + with: + path: ./build + key: ${{ runner.os }}-${{ github.run_id }} + fail-on-cache-miss: true # If we can't restore the cache, then we won't have a launcher binary to run below + enableCrossOsArchive: true + + - name: Launcher Version + working-directory: build + shell: bash + run: | + ./launcher --version + thisVersion=$(./launcher --version 2>/dev/null | grep "version" | awk '{print $4}') + baseVersion="${{ needs.version_baseline.outputs.version }}" + if [[ "$thisVersion" != "$baseVersion" ]]; then + printf "launcher version %s does not match baseline version %s" "$thisVersion" "$baseVersion" + exit 1 + fi + + - name: Download Osquery + working-directory: build + run: ./launcher download-osquery --directory . + + - name: Osquery Version + working-directory: build + run: ./osqueryd --version + + - name: Launcher Doctor + working-directory: build + run: ./launcher doctor container_exec_testing: name: Exec Test Containers @@ -444,65 +442,64 @@ jobs: - version_baseline # version_baseline implies build_linux - build # need the other builds too steps: - - - name: OS Info - run: | - echo uname: - uname -a - - for f in /etc/*release; do echo -e "\n\n$f:"; cat "$f" || true; done - - # zstd is needed so we can restore cache -- see https://github.com/actions/cache/issues/1455#issuecomment-2328358604; - # ca-certificates and openssl are needed to download osquery. - - name: Install dependencies - run: | - if grep NAME /etc/os-release | grep -q centos; then - echo CentOS detected - - sed -i 's/mirror.centos.org/vault.centos.org/g' /etc/yum.repos.d/CentOS-*.repo - sed -i 's/^#.*baseurl=http/baseurl=http/g' /etc/yum.repos.d/CentOS-*.repo - sed -i 's/^mirrorlist=http/#mirrorlist=http/g' /etc/yum.repos.d/CentOS-*.repo - - dnf install -y zstd - elif command -v apt-get > /dev/null; then - echo apt-get detected - - apt-get -y update - apt-get -y install zstd ca-certificates openssl - update-ca-certificates - fi - - - name: cache restore build output - uses: actions/cache/restore@v5 - with: - path: ./build - key: ${{ runner.os }}-${{ github.run_id }} - fail-on-cache-miss: true # If we can't restore the cache, then we won't have a launcher binary to run below - enableCrossOsArchive: true - - - name: Launcher Version - working-directory: build - shell: bash - run: | - ./launcher --version - thisVersion=$(./launcher --version 2>/dev/null | grep "version" | awk '{print $4}') - baseVersion="${{ needs.version_baseline.outputs.version }}" - if [[ "$thisVersion" != "$baseVersion" ]]; then - printf "launcher version %s does not match baseline version %s" "$thisVersion" "$baseVersion" - exit 1 - fi - - - name: Download Osquery - working-directory: build - run: ./launcher download-osquery --directory . - - - name: Osquery Version - working-directory: build - run: ./osqueryd --version - - - name: Launcher Doctor - working-directory: build - run: ./launcher doctor + - name: OS Info + run: | + echo uname: + uname -a + + for f in /etc/*release; do echo -e "\n\n$f:"; cat "$f" || true; done + + # zstd is needed so we can restore cache -- see https://github.com/actions/cache/issues/1455#issuecomment-2328358604; + # ca-certificates and openssl are needed to download osquery. + - name: Install dependencies + run: | + if grep NAME /etc/os-release | grep -q centos; then + echo CentOS detected + + sed -i 's/mirror.centos.org/vault.centos.org/g' /etc/yum.repos.d/CentOS-*.repo + sed -i 's/^#.*baseurl=http/baseurl=http/g' /etc/yum.repos.d/CentOS-*.repo + sed -i 's/^mirrorlist=http/#mirrorlist=http/g' /etc/yum.repos.d/CentOS-*.repo + + dnf install -y zstd + elif command -v apt-get > /dev/null; then + echo apt-get detected + + apt-get -y update + apt-get -y install zstd ca-certificates openssl + update-ca-certificates + fi + + - name: cache restore build output + uses: actions/cache/restore@v5 + with: + path: ./build + key: ${{ runner.os }}-${{ github.run_id }} + fail-on-cache-miss: true # If we can't restore the cache, then we won't have a launcher binary to run below + enableCrossOsArchive: true + + - name: Launcher Version + working-directory: build + shell: bash + run: | + ./launcher --version + thisVersion=$(./launcher --version 2>/dev/null | grep "version" | awk '{print $4}') + baseVersion="${{ needs.version_baseline.outputs.version }}" + if [[ "$thisVersion" != "$baseVersion" ]]; then + printf "launcher version %s does not match baseline version %s" "$thisVersion" "$baseVersion" + exit 1 + fi + + - name: Download Osquery + working-directory: build + run: ./launcher download-osquery --directory . + + - name: Osquery Version + working-directory: build + run: ./osqueryd --version + + - name: Launcher Doctor + working-directory: build + run: ./launcher doctor # If the prior exec tests suceeded, this grabs the cached things, and moves them to artifacts. We ought # be able to do this entirely on ubuntu, so let's try! @@ -521,20 +518,20 @@ jobs: - exec_testing - container_exec_testing steps: - - name: cache restore build output - uses: actions/cache/restore@v5 - with: - path: ./build - key: ${{ matrix.artifactos }}-${{ github.run_id }} - fail-on-cache-miss: true # If we can't restore the cache, then we won't have anything to upload - enableCrossOsArchive: true - - - name: Upload Build - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifactos }}-build - path: build/ - if-no-files-found: error + - name: cache restore build output + uses: actions/cache/restore@v5 + with: + path: ./build + key: ${{ matrix.artifactos }}-${{ github.run_id }} + fail-on-cache-miss: true # If we can't restore the cache, then we won't have anything to upload + enableCrossOsArchive: true + + - name: Upload Build + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifactos }}-build + path: build/ + if-no-files-found: error package_builder_test: name: package_builder @@ -547,57 +544,57 @@ jobs: - macos-14 - windows-latest steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # need a full checkout for `git describe` - - - uses: actions/setup-go@v6 - with: - go-version-file: './go.mod' - check-latest: true - cache: false - id: go - - - id: go-cache-paths - shell: bash - run: | - echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" - echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - - - name: Go Build Cache - uses: actions/cache@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - - - name: Go Mod Cache - uses: actions/cache@v5 - with: - path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - - - run: make deps - - - id: build - run: make package-builder - - - name: package - id: run-package-builder - run: ${{ steps.build.outputs.binary }} make --i-am-a-kolide-customer --debug --hostname=localhost --enroll_secret=secret --launcher_version=nightly --osquery_version=nightly --output_dir=./ - - - name: Test install macOS - if: ${{ contains(matrix.os, 'macos') }} - run: | - # Check that we can install - sudo installer -dumplog -pkg ./launcher.darwin-launchd-pkg.pkg -target / - # Quick check that at least a couple of the files we expect now exist - if [ ! -f /Library/LaunchDaemons/com.launcher.launcher.plist ]; then echo "missing launchd entry" && exit 1; fi - if [ ! -f /usr/local/launcher/osquery.app/Contents/MacOS/osqueryd ]; then echo "missing osqueryd binary" && exit 1; fi - if [ ! -L /usr/local/launcher/bin/osqueryd ]; then echo "missing osquery symlink" && exit 1; fi - if [ ! -e /usr/local/launcher/bin/osqueryd ]; then echo "osquery symlink is present but broken" && exit 1; fi - if [ ! -f /usr/local/launcher/Kolide.app/Contents/MacOS/launcher ]; then echo "missing launcher binary" && exit 1; fi - if [ ! -L /usr/local/launcher/bin/launcher ]; then echo "missing launcher symlink" && exit 1; fi - if [ ! -e /usr/local/launcher/bin/launcher ]; then echo "launcher symlink is present but broken" && exit 1; fi + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # need a full checkout for `git describe` + + - uses: actions/setup-go@v6 + with: + go-version-file: "./go.mod" + check-latest: true + cache: false + id: go + + - id: go-cache-paths + shell: bash + run: | + echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" + echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" + + - name: Go Build Cache + uses: actions/cache@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-build }} + key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} + + - name: Go Mod Cache + uses: actions/cache@v5 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + + - run: make deps + + - id: build + run: make package-builder + + - name: package + id: run-package-builder + run: ${{ steps.build.outputs.binary }} make --i-am-a-kolide-customer --debug --hostname=localhost --enroll_secret=secret --launcher_version=nightly --osquery_version=nightly --output_dir=./ + + - name: Test install macOS + if: ${{ contains(matrix.os, 'macos') }} + run: | + # Check that we can install + sudo installer -dumplog -pkg ./launcher.darwin-launchd-pkg.pkg -target / + # Quick check that at least a couple of the files we expect now exist + if [ ! -f /Library/LaunchDaemons/com.launcher.launcher.plist ]; then echo "missing launchd entry" && exit 1; fi + if [ ! -f /usr/local/launcher/osquery.app/Contents/MacOS/osqueryd ]; then echo "missing osqueryd binary" && exit 1; fi + if [ ! -L /usr/local/launcher/bin/osqueryd ]; then echo "missing osquery symlink" && exit 1; fi + if [ ! -e /usr/local/launcher/bin/osqueryd ]; then echo "osquery symlink is present but broken" && exit 1; fi + if [ ! -f /usr/local/launcher/Kolide.app/Contents/MacOS/launcher ]; then echo "missing launcher binary" && exit 1; fi + if [ ! -L /usr/local/launcher/bin/launcher ]; then echo "missing launcher symlink" && exit 1; fi + if [ ! -e /usr/local/launcher/bin/launcher ]; then echo "launcher symlink is present but broken" && exit 1; fi # This job is here as a github status check -- it allows us to move # the merge dependency from being on all the jobs to this single diff --git a/pkg/windows/windowsupdate/windowsupdate_test.go b/pkg/windows/windowsupdate/windowsupdate_test.go index 6c8c257458..e69329bee0 100644 --- a/pkg/windows/windowsupdate/windowsupdate_test.go +++ b/pkg/windows/windowsupdate/windowsupdate_test.go @@ -6,119 +6,11 @@ package windowsupdate import ( "testing" - comshim "github.com/NozomiNetworks/go-comshim" "github.com/kolide/launcher/v2/ee/tables/ci" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func initCOM(t *testing.T) { - t.Helper() - require.NoError(t, comshim.TryAdd(1), "initializing COM") - t.Cleanup(comshim.Done) -} - -func initCOMBench(b *testing.B) { - b.Helper() - require.NoError(b, comshim.TryAdd(1), "initializing COM") - b.Cleanup(comshim.Done) -} - -func TestNewUpdateSession(t *testing.T) { - t.Parallel() - initCOM(t) - - session, err := NewUpdateSession() - require.NoError(t, err, "NewUpdateSession") - defer session.Release() - - // ClientApplicationID is a string (may be empty on a default session) - assert.IsType(t, "", session.ClientApplicationID) - // ReadOnly should be a bool; default sessions are not read-only - assert.False(t, session.ReadOnly) -} - -func TestCreateUpdateSearcher(t *testing.T) { - t.Parallel() - initCOM(t) - - session, err := NewUpdateSession() - require.NoError(t, err, "NewUpdateSession") - defer session.Release() - - searcher, err := session.CreateUpdateSearcher() - require.NoError(t, err, "CreateUpdateSearcher") - - // ServerSelection is an enum: ssDefault(0), ssManagedServer(1), ssWindowsUpdate(2), ssOthers(3) - assert.GreaterOrEqual(t, searcher.ServerSelection, int32(0)) - assert.LessOrEqual(t, searcher.ServerSelection, int32(3)) - - assert.IsType(t, "", searcher.ServiceID) -} - -func TestGetTotalHistoryCount(t *testing.T) { - t.Parallel() - initCOM(t) - - session, err := NewUpdateSession() - require.NoError(t, err, "NewUpdateSession") - defer session.Release() - - searcher, err := session.CreateUpdateSearcher() - require.NoError(t, err, "CreateUpdateSearcher") - - count, err := searcher.GetTotalHistoryCount() - require.NoError(t, err, "GetTotalHistoryCount") - assert.GreaterOrEqual(t, count, int32(0), "history count should be non-negative") -} - -func TestQueryHistorySmall(t *testing.T) { - t.Parallel() - initCOM(t) - - session, err := NewUpdateSession() - require.NoError(t, err, "NewUpdateSession") - defer session.Release() - - searcher, err := session.CreateUpdateSearcher() - require.NoError(t, err, "CreateUpdateSearcher") - - totalCount, err := searcher.GetTotalHistoryCount() - require.NoError(t, err, "GetTotalHistoryCount") - - if totalCount == 0 { - t.Skip("no update history entries on this machine, skipping") - } - - // Query a small number of entries to keep the test fast - queryCount := totalCount - if queryCount > 3 { - queryCount = 3 - } - - entries, err := searcher.QueryHistory(0, queryCount) - require.NoError(t, err, "QueryHistory") - require.Len(t, entries, int(queryCount)) - - for i, entry := range entries { - assert.NotEmpty(t, entry.Title, "entry[%d].Title should not be empty", i) - - // OperationResultCode enum: orcNotStarted(0), orcInProgress(1), orcSucceeded(2), - // orcSucceededWithErrors(3), orcFailed(4), orcAborted(5) - assert.GreaterOrEqual(t, entry.ResultCode, int32(0), "entry[%d].ResultCode", i) - assert.LessOrEqual(t, entry.ResultCode, int32(5), "entry[%d].ResultCode", i) - - // UpdateOperation enum: uoInstallation(1), uoUninstallation(2) - assert.GreaterOrEqual(t, entry.Operation, int32(1), "entry[%d].Operation", i) - assert.LessOrEqual(t, entry.Operation, int32(2), "entry[%d].Operation", i) - - // UpdateIdentity should be populated - if assert.NotNil(t, entry.UpdateIdentity, "entry[%d].UpdateIdentity", i) { - assert.NotEmpty(t, entry.UpdateIdentity.UpdateID, "entry[%d].UpdateIdentity.UpdateID", i) - } - } -} - // BenchmarkQueryHistory exercises the full COM lifecycle path in a loop: // session creation, searcher creation, history query with real VARIANT // extraction and IDispatch Release. The non-golang-B/op metric captures From b3e6fde3150a280e79bdcf8a0d24785bb7881373 Mon Sep 17 00:00:00 2001 From: seph Date: Fri, 6 Mar 2026 09:41:47 -0500 Subject: [PATCH 3/6] fix benchmark --- pkg/windows/windowsupdate/windowsupdate_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/windows/windowsupdate/windowsupdate_test.go b/pkg/windows/windowsupdate/windowsupdate_test.go index e69329bee0..aa5c22de0d 100644 --- a/pkg/windows/windowsupdate/windowsupdate_test.go +++ b/pkg/windows/windowsupdate/windowsupdate_test.go @@ -6,11 +6,17 @@ package windowsupdate import ( "testing" + comshim "github.com/NozomiNetworks/go-comshim" "github.com/kolide/launcher/v2/ee/tables/ci" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func initCOMBench(b *testing.B) { + b.Helper() + require.NoError(b, comshim.TryAdd(1), "initializing COM") + b.Cleanup(comshim.Done) +} + // BenchmarkQueryHistory exercises the full COM lifecycle path in a loop: // session creation, searcher creation, history query with real VARIANT // extraction and IDispatch Release. The non-golang-B/op metric captures @@ -27,7 +33,6 @@ func BenchmarkQueryHistory(b *testing.B) { require.NoError(b, err) totalCount, err := searcher.GetTotalHistoryCount() require.NoError(b, err) - session.Release() if totalCount == 0 { b.Skip("no update history entries on this machine") @@ -52,8 +57,6 @@ func BenchmarkQueryHistory(b *testing.B) { entries, err := searcher.QueryHistory(0, queryCount) require.NoError(b, err) require.NotEmpty(b, entries) - - session.Release() } // 64 KiB per op is generous — a leaking implementation easily exceeds From 0eb3acea470a244db444fa2088f7e5e7932f409a Mon Sep 17 00:00:00 2001 From: seph Date: Fri, 6 Mar 2026 11:13:29 -0500 Subject: [PATCH 4/6] report --- pkg/windows/windowsupdate/windowsupdate_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/windows/windowsupdate/windowsupdate_test.go b/pkg/windows/windowsupdate/windowsupdate_test.go index aa5c22de0d..9c4a632f3b 100644 --- a/pkg/windows/windowsupdate/windowsupdate_test.go +++ b/pkg/windows/windowsupdate/windowsupdate_test.go @@ -59,6 +59,8 @@ func BenchmarkQueryHistory(b *testing.B) { require.NotEmpty(b, entries) } + ci.ReportNonGolangMemoryUsage(b, baselineStats) + // 64 KiB per op is generous — a leaking implementation easily exceeds // this after the benchmark framework scales up b.N. ci.RequireNonGolangMemoryBelowThreshold(b, baselineStats, 64*1024) From 5b9454d0924b6571fc3c848b0ef2674d180346b8 Mon Sep 17 00:00:00 2001 From: seph Date: Fri, 6 Mar 2026 11:56:14 -0500 Subject: [PATCH 5/6] lint --- ee/localserver/dt4a_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/localserver/dt4a_test.go b/ee/localserver/dt4a_test.go index 4d6682b958..4d450e0686 100644 --- a/ee/localserver/dt4a_test.go +++ b/ee/localserver/dt4a_test.go @@ -452,7 +452,7 @@ func Test_requestDt4aInfoHandler_badRequest(t *testing.T) { }) // Make a request to our handler - request := httptest.NewRequestWithContext(t.Context(), tt.httpMethod, "/dt4a", tt.requestBody) + request := httptest.NewRequestWithContext(t.Context(), (tt.httpMethod, "/dt4a", tt.requestBody) request.Header.Set("origin", tt.requestOrigin) responseRecorder := httptest.NewRecorder() ls.requestDt4aInfoHandler().ServeHTTP(responseRecorder, request) From 842ebcd2c3b177b42d89cda566041a0ac27162c9 Mon Sep 17 00:00:00 2001 From: seph Date: Fri, 6 Mar 2026 12:51:55 -0500 Subject: [PATCH 6/6] typo --- ee/localserver/dt4a_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/localserver/dt4a_test.go b/ee/localserver/dt4a_test.go index 4d450e0686..4d6682b958 100644 --- a/ee/localserver/dt4a_test.go +++ b/ee/localserver/dt4a_test.go @@ -452,7 +452,7 @@ func Test_requestDt4aInfoHandler_badRequest(t *testing.T) { }) // Make a request to our handler - request := httptest.NewRequestWithContext(t.Context(), (tt.httpMethod, "/dt4a", tt.requestBody) + request := httptest.NewRequestWithContext(t.Context(), tt.httpMethod, "/dt4a", tt.requestBody) request.Header.Set("origin", tt.requestOrigin) responseRecorder := httptest.NewRecorder() ls.requestDt4aInfoHandler().ServeHTTP(responseRecorder, request)