From c82222b17fabccc50e0775d8924f9510ba70da3e Mon Sep 17 00:00:00 2001 From: Dawei Huang Date: Mon, 13 Jul 2026 14:10:55 -0500 Subject: [PATCH 1/2] [ci]: Make pure package validation deterministic Propagate failures from looped pure.mk targets, replace privileged nsenter tests with a deterministic command seam, and keep file tests portable across TMPDIR settings. The previous workflow could skip command behavior or return the status of a later successful shell command, producing false-green validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Dawei Huang --- pkg/exec/command.go | 4 +- pkg/exec/command_test.go | 273 ++++++++++++++++++++++--------------- pkg/gnoi/file/file_test.go | 49 +++---- pure.mk | 53 ++++--- 4 files changed, 214 insertions(+), 165 deletions(-) diff --git a/pkg/exec/command.go b/pkg/exec/command.go index bc8e913a0..35ac5b07b 100644 --- a/pkg/exec/command.go +++ b/pkg/exec/command.go @@ -18,6 +18,8 @@ const ( defaultTimeout = 30 * time.Second ) +var execCommandContext = exec.CommandContext + // RunHostCommandOptions provides configuration options for RunHostCommand type RunHostCommandOptions struct { // Timeout specifies the maximum duration for command execution @@ -78,7 +80,7 @@ func RunHostCommand(ctx context.Context, command string, args []string, opts *Ru nsenterArgs := buildNsenterArgs(opts, command, args) // Create command - cmd := exec.CommandContext(ctx, "nsenter", nsenterArgs...) + cmd := execCommandContext(ctx, "nsenter", nsenterArgs...) // Set working directory if specified if opts.WorkingDir != "" { diff --git a/pkg/exec/command_test.go b/pkg/exec/command_test.go index 19ffb33dd..dc4dcba2a 100644 --- a/pkg/exec/command_test.go +++ b/pkg/exec/command_test.go @@ -2,145 +2,196 @@ package exec import ( "context" - "runtime" + "fmt" + "os" + "os/exec" + "path/filepath" + "reflect" "strings" "testing" "time" ) -func TestRunHostCommand(t *testing.T) { - // Skip tests if not running on Linux - if runtime.GOOS != "linux" { - t.Skip("nsenter tests can only run on Linux") +const commandHelperEnv = "GO_WANT_COMMAND_HELPER" + +func TestCommandHelperProcess(t *testing.T) { + if os.Getenv(commandHelperEnv) != "1" { + return } - // Skip if nsenter is not available - if !IsNsenterAvailable() { - t.Skip("nsenter is not available on this system") + separator := -1 + for i, arg := range os.Args { + if arg == "--" { + separator = i + break + } + } + if separator == -1 || separator+1 >= len(os.Args) { + os.Exit(2) } - // Check if we have permission to use nsenter (requires root or CAP_SYS_ADMIN) - // Try a simple test command first - testResult, _ := RunHostCommand(context.Background(), "true", nil, nil) - if testResult != nil && testResult.Error != nil && strings.Contains(testResult.Stderr, "Permission denied") { - t.Skip("Insufficient permissions to run nsenter tests (need root or CAP_SYS_ADMIN)") + switch os.Args[separator+1] { + case "success": + fmt.Fprintln(os.Stdout, "helper stdout") + fmt.Fprintln(os.Stderr, "helper stderr") + os.Exit(0) + case "failure": + fmt.Fprintln(os.Stderr, "helper failure") + os.Exit(23) + case "sleep": + time.Sleep(10 * time.Second) + os.Exit(0) + default: + os.Exit(2) } +} - tests := []struct { - name string - command string - args []string - opts *RunHostCommandOptions - wantErr bool - check func(t *testing.T, result *CommandResult) - }{ - { - name: "simple echo command", - command: "echo", - args: []string{"hello", "world"}, - opts: nil, - wantErr: false, - check: func(t *testing.T, result *CommandResult) { - if result.ExitCode != 0 { - t.Errorf("expected exit code 0, got %d", result.ExitCode) - } - if !strings.Contains(result.Stdout, "hello world") { - t.Errorf("expected output to contain 'hello world', got %s", result.Stdout) - } - }, - }, - { - name: "command with timeout", - command: "sleep", - args: []string{"0.1"}, - opts: &RunHostCommandOptions{ - Timeout: 1 * time.Second, - }, - wantErr: false, - check: func(t *testing.T, result *CommandResult) { - if result.ExitCode != 0 { - t.Errorf("expected exit code 0, got %d", result.ExitCode) - } - }, - }, - { - name: "empty command", - command: "", - args: nil, - opts: nil, - wantErr: true, - check: nil, - }, - { - name: "non-existent command", - command: "this-command-does-not-exist", - args: nil, - opts: nil, - wantErr: false, - check: func(t *testing.T, result *CommandResult) { - if result.ExitCode == 0 { - t.Errorf("expected non-zero exit code for non-existent command") - } - }, - }, - { - name: "command with custom namespaces", - command: "pwd", - args: nil, - opts: &RunHostCommandOptions{ - Namespaces: []string{"pid", "net"}, - }, - wantErr: false, - check: func(t *testing.T, result *CommandResult) { - if result.ExitCode != 0 { - t.Errorf("expected exit code 0, got %d", result.ExitCode) - } - }, - }, +type commandInvocation struct { + name string + args []string +} + +func useCommandHelper(t *testing.T, behavior string) *commandInvocation { + t.Helper() + + originalExecCommand := execCommandContext + invocation := &commandInvocation{} + execCommandContext = func(ctx context.Context, name string, args ...string) *exec.Cmd { + invocation.name = name + invocation.args = append([]string(nil), args...) + + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestCommandHelperProcess$", "--", behavior) + cmd.Env = append(os.Environ(), commandHelperEnv+"=1") + return cmd } + t.Cleanup(func() { + execCommandContext = originalExecCommand + }) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := RunHostCommand(context.Background(), tt.command, tt.args, tt.opts) + return invocation +} - if (err != nil) != tt.wantErr { - t.Errorf("RunHostCommand() error = %v, wantErr %v", err, tt.wantErr) - return - } +func TestRunHostCommand(t *testing.T) { + invocation := useCommandHelper(t, "success") - if tt.check != nil && result != nil { - tt.check(t, result) - } - }) + result, err := RunHostCommand(context.Background(), "echo", []string{"hello", "world"}, nil) + if err != nil { + t.Fatalf("RunHostCommand() error = %v", err) + } + if result.Error != nil { + t.Fatalf("RunHostCommand() command error = %v", result.Error) + } + if result.ExitCode != 0 { + t.Errorf("RunHostCommand() exit code = %d, want 0", result.ExitCode) + } + if result.Stdout != "helper stdout\n" { + t.Errorf("RunHostCommand() stdout = %q, want %q", result.Stdout, "helper stdout\n") + } + if result.Stderr != "helper stderr\n" { + t.Errorf("RunHostCommand() stderr = %q, want %q", result.Stderr, "helper stderr\n") + } + if invocation.name != "nsenter" { + t.Errorf("RunHostCommand() executable = %q, want %q", invocation.name, "nsenter") + } + wantArgs := []string{ + "--target", "1", + "--pid", "--mount", "--uts", "--ipc", "--net", + "--", + "echo", "hello", "world", + } + if !reflect.DeepEqual(invocation.args, wantArgs) { + t.Errorf("RunHostCommand() args = %q, want %q", invocation.args, wantArgs) } } -func TestRunHostCommandSimple(t *testing.T) { - // Skip tests if not running on Linux - if runtime.GOOS != "linux" { - t.Skip("nsenter tests can only run on Linux") +func TestRunHostCommandNonZeroExit(t *testing.T) { + useCommandHelper(t, "failure") + + result, err := RunHostCommand(context.Background(), "false", nil, nil) + if err != nil { + t.Fatalf("RunHostCommand() error = %v", err) } + if result.Error == nil { + t.Fatal("RunHostCommand() command error = nil, want non-nil") + } + if result.ExitCode != 23 { + t.Errorf("RunHostCommand() exit code = %d, want 23", result.ExitCode) + } + if result.Stderr != "helper failure\n" { + t.Errorf("RunHostCommand() stderr = %q, want %q", result.Stderr, "helper failure\n") + } +} - // Skip if nsenter is not available - if !IsNsenterAvailable() { - t.Skip("nsenter is not available on this system") +func TestRunHostCommandStartFailure(t *testing.T) { + originalExecCommand := execCommandContext + execCommandContext = func(ctx context.Context, _ string, _ ...string) *exec.Cmd { + return exec.CommandContext(ctx, filepath.Join(t.TempDir(), "missing-command")) } + t.Cleanup(func() { + execCommandContext = originalExecCommand + }) - // Check permissions - testResult, _ := RunHostCommand(context.Background(), "true", nil, nil) - if testResult != nil && testResult.Error != nil && strings.Contains(testResult.Stderr, "Permission denied") { - t.Skip("Insufficient permissions to run nsenter tests") + result, err := RunHostCommand(context.Background(), "echo", nil, nil) + if err != nil { + t.Fatalf("RunHostCommand() error = %v", err) } + if result.Error == nil { + t.Fatal("RunHostCommand() command error = nil, want non-nil") + } +} + +func TestRunHostCommandTimeout(t *testing.T) { + useCommandHelper(t, "sleep") + + start := time.Now() + result, err := RunHostCommand(context.Background(), "sleep", []string{"10"}, &RunHostCommandOptions{ + Timeout: 20 * time.Millisecond, + }) + if err != nil { + t.Fatalf("RunHostCommand() error = %v", err) + } + if result.Error == nil { + t.Fatal("RunHostCommand() command error = nil, want timeout error") + } + if elapsed := time.Since(start); elapsed >= time.Second { + t.Errorf("RunHostCommand() elapsed = %v, want less than 1s", elapsed) + } +} + +func TestRunHostCommandRejectsEmptyCommand(t *testing.T) { + result, err := RunHostCommand(context.Background(), "", nil, nil) + if err == nil { + t.Fatal("RunHostCommand() error = nil, want non-nil") + } + if result != nil { + t.Errorf("RunHostCommand() result = %#v, want nil", result) + } +} + +func TestRunHostCommandSimple(t *testing.T) { + useCommandHelper(t, "success") - // Test simple command execution output, err := RunHostCommandSimple("echo", "test") if err != nil { - t.Errorf("RunHostCommandSimple() error = %v", err) - return + t.Fatalf("RunHostCommandSimple() error = %v", err) } + if output != "helper stdout\n" { + t.Errorf("RunHostCommandSimple() = %q, want %q", output, "helper stdout\n") + } +} + +func TestRunHostCommandSimpleFailure(t *testing.T) { + useCommandHelper(t, "failure") - if !strings.Contains(output, "test") { - t.Errorf("expected output to contain 'test', got %s", output) + output, err := RunHostCommandSimple("false") + if err == nil { + t.Fatal("RunHostCommandSimple() error = nil, want non-nil") + } + if output != "" { + t.Errorf("RunHostCommandSimple() output = %q, want empty", output) + } + if !strings.Contains(err.Error(), "helper failure") { + t.Errorf("RunHostCommandSimple() error = %q, want helper stderr", err) } } diff --git a/pkg/gnoi/file/file_test.go b/pkg/gnoi/file/file_test.go index 071e030bd..95809af62 100644 --- a/pkg/gnoi/file/file_test.go +++ b/pkg/gnoi/file/file_test.go @@ -26,6 +26,12 @@ import ( "google.golang.org/grpc/status" ) +func transferTestPath(t *testing.T, name string) (logical, physical string) { + t.Helper() + logicalDir, physicalDir := useTempHostRoot(t) + return filepath.Join(logicalDir, name), filepath.Join(physicalDir, name) +} + func TestHandleTransferToRemote_Success(t *testing.T) { // Create test HTTP server testContent := []byte("test firmware content") @@ -35,9 +41,7 @@ func TestHandleTransferToRemote_Success(t *testing.T) { })) defer server.Close() - // Create temp directory for output - tempDir := t.TempDir() - localPath := filepath.Join(tempDir, "firmware.bin") + localPath, physicalPath := transferTestPath(t, "firmware.bin") // Create request req := &gnoi_file_pb.TransferToRemoteRequest{ @@ -69,7 +73,7 @@ func TestHandleTransferToRemote_Success(t *testing.T) { } // Verify file was downloaded - content, err := os.ReadFile(localPath) + content, err := os.ReadFile(physicalPath) if err != nil { t.Fatalf("Failed to read downloaded file: %v", err) } @@ -138,9 +142,9 @@ func TestHandleTransferToRemote_EmptyLocalPath(t *testing.T) { } func TestHandleTransferToRemote_EmptyURL(t *testing.T) { - tempDir := t.TempDir() + localPath, _ := transferTestPath(t, "test.bin") req := &gnoi_file_pb.TransferToRemoteRequest{ - LocalPath: filepath.Join(tempDir, "test.bin"), + LocalPath: localPath, RemoteDownload: &common.RemoteDownload{ Path: "", Protocol: common.RemoteDownload_HTTP, @@ -160,9 +164,9 @@ func TestHandleTransferToRemote_EmptyURL(t *testing.T) { } func TestHandleTransferToRemote_UnsupportedProtocol(t *testing.T) { - tempDir := t.TempDir() + localPath, _ := transferTestPath(t, "test.bin") req := &gnoi_file_pb.TransferToRemoteRequest{ - LocalPath: filepath.Join(tempDir, "test.bin"), + LocalPath: localPath, RemoteDownload: &common.RemoteDownload{ Path: "https://example.com/file", Protocol: common.RemoteDownload_HTTPS, @@ -188,9 +192,9 @@ func TestHandleTransferToRemote_DownloadFails(t *testing.T) { })) defer server.Close() - tempDir := t.TempDir() + localPath, _ := transferTestPath(t, "test.bin") req := &gnoi_file_pb.TransferToRemoteRequest{ - LocalPath: filepath.Join(tempDir, "test.bin"), + LocalPath: localPath, RemoteDownload: &common.RemoteDownload{ Path: server.URL, Protocol: common.RemoteDownload_HTTP, @@ -220,8 +224,7 @@ func TestHandleTransferToRemote_HashVerification(t *testing.T) { })) defer server.Close() - tempDir := t.TempDir() - localPath := filepath.Join(tempDir, "test.txt") + localPath, _ := transferTestPath(t, "test.txt") req := &gnoi_file_pb.TransferToRemoteRequest{ LocalPath: localPath, @@ -253,9 +256,7 @@ func TestHandleTransferToRemote_NestedDirectories(t *testing.T) { })) defer server.Close() - tempDir := t.TempDir() - // Path with nested directories - localPath := filepath.Join(tempDir, "a", "b", "c", "file.bin") + localPath, physicalPath := transferTestPath(t, filepath.Join("a", "b", "c", "file.bin")) req := &gnoi_file_pb.TransferToRemoteRequest{ LocalPath: localPath, @@ -272,7 +273,7 @@ func TestHandleTransferToRemote_NestedDirectories(t *testing.T) { } // Verify file was created - if _, err := os.Stat(localPath); os.IsNotExist(err) { + if _, err := os.Stat(physicalPath); os.IsNotExist(err) { t.Error("File was not created in nested directory") } } @@ -327,9 +328,9 @@ func TestHandleTransferToRemote_ContextCancellation(t *testing.T) { })) defer server.Close() - tempDir := t.TempDir() + localPath, _ := transferTestPath(t, "test.bin") req := &gnoi_file_pb.TransferToRemoteRequest{ - LocalPath: filepath.Join(tempDir, "test.bin"), + LocalPath: localPath, RemoteDownload: &common.RemoteDownload{ Path: server.URL, Protocol: common.RemoteDownload_HTTP, @@ -1455,9 +1456,9 @@ func TestHandleTransferToRemote_EdgeCases(t *testing.T) { })) defer server.Close() - tempDir := t.TempDir() + localPath, _ := transferTestPath(t, "large.bin") req := &gnoi_file_pb.TransferToRemoteRequest{ - LocalPath: filepath.Join(tempDir, "large.bin"), + LocalPath: localPath, RemoteDownload: &common.RemoteDownload{ Path: server.URL, Protocol: common.RemoteDownload_HTTP, @@ -1484,9 +1485,9 @@ func TestHandleTransferToRemote_EdgeCases(t *testing.T) { })) defer slowServer.Close() - tempDir := t.TempDir() + localPath, _ := transferTestPath(t, "timeout.bin") req := &gnoi_file_pb.TransferToRemoteRequest{ - LocalPath: filepath.Join(tempDir, "timeout.bin"), + LocalPath: localPath, RemoteDownload: &common.RemoteDownload{ Path: slowServer.URL, Protocol: common.RemoteDownload_HTTP, @@ -1854,9 +1855,9 @@ func TestHandleTransferToRemote_HighCoverage(t *testing.T) { })) defer server.Close() - tempDir := t.TempDir() + localPath, _ := transferTestPath(t, "cleanup_test.bin") req := &gnoi_file_pb.TransferToRemoteRequest{ - LocalPath: filepath.Join(tempDir, "cleanup_test.bin"), + LocalPath: localPath, RemoteDownload: &common.RemoteDownload{ Path: server.URL, Protocol: common.RemoteDownload_HTTP, diff --git a/pure.mk b/pure.mk index 39d359357..0b2a3a154 100644 --- a/pure.mk +++ b/pure.mk @@ -51,7 +51,7 @@ PACKAGES ?= $(PURE_PACKAGES) clean: @echo "Cleaning pure build artifacts..." $(GO) clean -cache -testcache - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ rm -f $$pkg/coverage.out $$pkg/coverage.html; \ done @@ -59,9 +59,9 @@ clean: .PHONY: fmt-check fmt-check: @echo "Checking Go code formatting for pure packages..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ echo "Checking $$pkg..."; \ - files=$$($(GOROOT)/bin/gofmt -l $$pkg/*.go 2>/dev/null || true); \ + files=$$($(GOROOT)/bin/gofmt -l $$pkg/*.go); \ if [ -n "$$files" ]; then \ echo "The following files need formatting in $$pkg:"; \ echo "$$files"; \ @@ -75,41 +75,39 @@ fmt-check: .PHONY: fmt fmt: @echo "Formatting Go code for pure packages..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ echo "Formatting $$pkg..."; \ - $(GOROOT)/bin/gofmt -w $$pkg/*.go 2>/dev/null || true; \ + $(GOROOT)/bin/gofmt -w $$pkg/*.go; \ done # Vet - static analysis .PHONY: vet vet: @echo "Running go vet on pure packages..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ echo "Vetting $$pkg..."; \ - cd $$pkg && $(GO) vet ./...; \ - cd - >/dev/null; \ + (cd $$pkg && $(GO) vet ./...); \ done # Test - run all tests with coverage .PHONY: test test: @echo "Running tests for pure packages..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ echo ""; \ echo "=== Testing $$pkg ==="; \ - cd $$pkg && $(GO) test -gcflags="all=-N -l" -v -race -coverprofile=coverage.out -covermode=atomic ./...; \ - if [ -f coverage.out ]; then \ + (cd $$pkg && $(GO) test -gcflags="all=-N -l" -v -race -coverprofile=coverage.out -covermode=atomic ./...); \ + if [ -f $$pkg/coverage.out ]; then \ echo "Coverage for $$pkg:"; \ - $(GO) tool cover -func=coverage.out; \ + (cd $$pkg && $(GO) tool cover -func=coverage.out); \ fi; \ - cd - >/dev/null; \ done # Generate coverage files for Azure pipeline integration .PHONY: azure-coverage azure-coverage: @echo "Generating coverage files for Azure pipeline..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ echo "Testing $$pkg..."; \ pkgname=$$(echo $$pkg | tr '/' '-'); \ $(GO) test -gcflags="all=-N -l" -race -coverprofile=coverage-pure-$$pkgname.txt -covermode=atomic -v ./$$pkg; \ @@ -120,12 +118,11 @@ azure-coverage: .PHONY: test-coverage test-coverage: test @echo "Generating HTML coverage reports..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ if [ -f $$pkg/coverage.out ]; then \ echo "Generating coverage report for $$pkg..."; \ - cd $$pkg && $(GO) tool cover -html=coverage.out -o coverage.html; \ + (cd $$pkg && $(GO) tool cover -html=coverage.out -o coverage.html); \ echo "Coverage report generated: $$pkg/coverage.html"; \ - cd - >/dev/null; \ fi; \ done @@ -133,7 +130,7 @@ test-coverage: test .PHONY: coverage-xml coverage-xml: test @echo "Generating XML coverage report for Azure..." - @if command -v gocov >/dev/null 2>&1 && command -v gocov-xml >/dev/null 2>&1; then \ + @set -e; if command -v gocov >/dev/null 2>&1 && command -v gocov-xml >/dev/null 2>&1; then \ echo "Converting coverage to XML format..."; \ rm -f coverage-*.out; \ for pkg in $(PACKAGES); do \ @@ -143,8 +140,9 @@ coverage-xml: test fi; \ done; \ if ls coverage-*.out >/dev/null 2>&1; then \ - gocov convert coverage-*.out | gocov-xml -source $(shell pwd) > coverage.xml; \ - rm -f coverage-*.out; \ + trap 'rm -f coverage-*.out coverage-pure.json' EXIT; \ + gocov convert coverage-*.out > coverage-pure.json; \ + gocov-xml -source $(shell pwd) < coverage-pure.json > coverage.xml; \ echo "XML coverage report generated: coverage.xml"; \ else \ echo "No coverage files found"; \ @@ -159,10 +157,9 @@ coverage-xml: test .PHONY: build-test build-test: @echo "Testing build of pure packages..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ echo "Building $$pkg..."; \ - cd $$pkg && $(GO) build -v ./...; \ - cd - >/dev/null; \ + (cd $$pkg && $(GO) build -v ./...); \ done # Lint check using basic go tools @@ -174,10 +171,9 @@ lint: fmt-check .PHONY: bench bench: @echo "Running benchmarks for pure packages..." - @for pkg in $(PACKAGES); do \ + @set -e; for pkg in $(PACKAGES); do \ echo "Benchmarking $$pkg..."; \ - cd $$pkg && $(GO) test -bench=. -benchmem ./...; \ - cd - >/dev/null; \ + (cd $$pkg && $(GO) test -bench=. -benchmem ./...); \ done # Module verification @@ -196,11 +192,10 @@ mod-verify: .PHONY: security security: @echo "Running security scan on pure packages..." - @if command -v gosec >/dev/null 2>&1; then \ + @set -e; if command -v gosec >/dev/null 2>&1; then \ for pkg in $(PACKAGES); do \ echo "Scanning $$pkg..."; \ - cd $$pkg && gosec ./...; \ - cd - >/dev/null; \ + (cd $$pkg && gosec ./...); \ done; \ else \ echo "gosec not available, skipping security scan"; \ From 011a041f51d5db4d7e10b0af1d574b791cb4fc23 Mon Sep 17 00:00:00 2001 From: Dawei Huang Date: Mon, 13 Jul 2026 22:57:43 -0500 Subject: [PATCH 2/2] [ci]: Discover canonical pure packages Derive the pure package set from internal, pkg, and cmd so new canonical packages cannot be omitted. Run each package exactly once and align the JUnit compiler flags with the gomonkey-based tests now included. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Dawei Huang --- pure.mk | 59 +++++++++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/pure.mk b/pure.mk index 0b2a3a154..2af6ed6f8 100644 --- a/pure.mk +++ b/pure.mk @@ -1,44 +1,22 @@ -# pure.mk - Simple CI for pure packages without SONiC dependencies +# pure.mk - Simple CI for canonical Go packages without SONiC dependencies # Usage: make -f pure.mk ci # -# This makefile supports testing packages that don't require CGO or SONiC dependencies. -# Add new pure packages to PURE_PACKAGES below. -# -# Goal: Eventually all packages should be pure unless they absolutely -# require CGO dependencies. All CGO/SONiC dependencies should be properly quarantined. +# Every Go package under internal/, pkg/, and cmd/ must remain pure. Packages +# outside those canonical roots may depend on the SONiC build environment. # Go configuration GO ?= go GOROOT ?= $(shell $(GO) env GOROOT) -# Pure packages (no CGO/SONiC dependencies) -# Add new packages here as they become pure-compatible. -PURE_PACKAGES := \ - internal/exec \ - pkg/gnoi/debug \ - pkg/bypass \ - internal/diskspace \ - internal/hash \ - internal/download \ - internal/firmware \ - pkg/interceptors \ - pkg/server/operational-handler \ - pkg/gnoi/file \ - pkg/exec \ - pkg/gnoi/os \ - pkg/gnoi/oras \ - pkg/hostfs \ - pkg/gnoi/system - -# Future packages to make pure: -# TODO: sonic-gnmi-standalone/pkg/workflow -# TODO: sonic-gnmi-standalone/pkg/client/config -# TODO: sonic-gnmi-standalone/internal/checksum -# TODO: sonic-gnmi-standalone/internal/download -# TODO: common_utils (parts that don't need CGO) -# TODO: gnoi_client/config -# TODO: transl_utils (isolate from translib dependencies) -# TODO: pkg/interceptors/dpuproxy (needs gRPC infrastructure mocking) +# Discover every package under the canonical pure roots. This makes purity a +# path-based invariant instead of an allowlist that can omit new packages. +PURE_ROOTS := internal pkg cmd +PURE_PACKAGES := $(shell \ + for root in $(PURE_ROOTS); do \ + if [ -d "$$root" ]; then \ + find "$$root" -type f -name '*.go' -print; \ + fi; \ + done | sed 's|/[^/]*$$||' | sort -u) # You can test specific packages by setting PACKAGES=pkg/specific/package PACKAGES ?= $(PURE_PACKAGES) @@ -86,7 +64,7 @@ vet: @echo "Running go vet on pure packages..." @set -e; for pkg in $(PACKAGES); do \ echo "Vetting $$pkg..."; \ - (cd $$pkg && $(GO) vet ./...); \ + (cd $$pkg && $(GO) vet .); \ done # Test - run all tests with coverage @@ -96,7 +74,7 @@ test: @set -e; for pkg in $(PACKAGES); do \ echo ""; \ echo "=== Testing $$pkg ==="; \ - (cd $$pkg && $(GO) test -gcflags="all=-N -l" -v -race -coverprofile=coverage.out -covermode=atomic ./...); \ + (cd $$pkg && $(GO) test -gcflags="all=-N -l" -v -race -coverprofile=coverage.out -covermode=atomic .); \ if [ -f $$pkg/coverage.out ]; then \ echo "Coverage for $$pkg:"; \ (cd $$pkg && $(GO) tool cover -func=coverage.out); \ @@ -159,7 +137,7 @@ build-test: @echo "Testing build of pure packages..." @set -e; for pkg in $(PACKAGES); do \ echo "Building $$pkg..."; \ - (cd $$pkg && $(GO) build -v ./...); \ + (cd $$pkg && $(GO) build -v .); \ done # Lint check using basic go tools @@ -173,7 +151,7 @@ bench: @echo "Running benchmarks for pure packages..." @set -e; for pkg in $(PACKAGES); do \ echo "Benchmarking $$pkg..."; \ - (cd $$pkg && $(GO) test -bench=. -benchmem ./...); \ + (cd $$pkg && $(GO) test -bench=. -benchmem .); \ done # Module verification @@ -195,7 +173,7 @@ security: @set -e; if command -v gosec >/dev/null 2>&1; then \ for pkg in $(PACKAGES); do \ echo "Scanning $$pkg..."; \ - (cd $$pkg && gosec ./...); \ + (cd $$pkg && gosec .); \ done; \ else \ echo "gosec not available, skipping security scan"; \ @@ -258,7 +236,8 @@ junit-xml: clean @export PATH=$(PATH):$(shell $(GO) env GOPATH)/bin && \ gotestsum --junitfile test-results/junit-pure.xml \ --format testname \ - -- -v -race -coverprofile=test-results/coverage-pure.txt \ + -- -gcflags="all=-N -l" -v -race \ + -coverprofile=test-results/coverage-pure.txt \ -covermode=atomic \ $(addprefix ./,$(PACKAGES)) @echo "Converting coverage to Cobertura XML format..."