diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 347df1a..8cf9b7a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,7 +5,7 @@ Follows these guidelines precisely to ensure consistency and maintainability of ## Stack -- Language: Go (Go 1.22+) +- Language: Go (Go 1.26+) - Framework: Go standard library - Testing: Go's built-in testing package - Dependency Management: Go modules diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0dec8f6..e145d42 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,15 +1,4 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL Advanced" +name: CodeQL Advanced on: push: @@ -19,23 +8,18 @@ on: schedule: - cron: "08 12 * * 4" +env: + # Opt in to the new CodeQL default: skip file coverage on PRs for faster analysis. + # Coverage is still computed on push and schedule analyses. + CODEQL_ACTION_FILE_COVERAGE_ON_PRS: false + jobs: analyze: name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ubuntu-latest permissions: - # required for all workflows security-events: write - - # required to fetch internal or private CodeQL packs packages: read - - # only required for workflows in private repositories actions: read contents: read @@ -47,51 +31,21 @@ jobs: build-mode: none - language: go build-mode: autobuild - # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: - - name: Checkout repository + - name: Check out code uses: actions/checkout@v6 - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 - - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - if: matrix.build-mode == 'manual' shell: bash run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' + echo 'Manual build mode is not configured. Set build-mode to "autobuild" or add build commands here.' exit 1 - name: Perform CodeQL Analysis diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14a1c70..e76a111 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,10 +9,86 @@ permissions: contents: read jobs: - build: + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: ./go.mod + + - name: Test + run: go test -v -race -tags=unit ./... + + - name: Build CLI + run: go build -v -o ${{ runner.temp }}/machineid${{ matrix.os == 'windows-latest' && '.exe' || '' }} ./cmd/machineid/ + + - name: Generate machine ID (smoke test) + shell: bash + run: | + BIN="${{ runner.temp }}/machineid${{ matrix.os == 'windows-latest' && '.exe' || '' }}" + + echo "--- Version ---" + "$BIN" -version + + echo "--- Default ID (CPU + motherboard + UUID) ---" + ID=$("$BIN") + echo "ID: $ID" + echo "Length: ${#ID}" + + # Verify the ID is a 64-char hex string + if [[ ${#ID} -ne 64 ]]; then + echo "ERROR: expected 64-char ID, got ${#ID}" + exit 1 + fi + if [[ ! "$ID" =~ ^[0-9a-f]{64}$ ]]; then + echo "ERROR: ID is not a valid hex string" + exit 1 + fi + + echo "--- Validate round-trip ---" + "$BIN" -validate "$ID" + + # Exercise additional CLI modes. + # On Windows CI, WMI can be transiently unavailable after repeated + # invocations, so these are non-fatal — the core validation above + # already proved the binary works. + ERRORS=0 + + echo "--- All components (JSON + diagnostics) ---" + "$BIN" -all -json -diagnostics || { echo "WARN: -all failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + echo "--- VM-friendly ---" + "$BIN" -vm || { echo "WARN: -vm failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + echo "--- Format 32 ---" + "$BIN" -format 32 || { echo "WARN: -format 32 failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + echo "--- Salt ---" + "$BIN" -salt "ci-test" || { echo "WARN: -salt failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + if [[ $ERRORS -gt 0 ]]; then + echo "WARN: $ERRORS non-critical exercise(s) failed on ${{ matrix.os }} (WMI transient issue)" + fi + + echo "Smoke test passed on ${{ matrix.os }}" + + report: + name: Report & Build + needs: test runs-on: ubuntu-latest + env: + MAKE_DEBUG: true steps: - - name: Checkout + - name: Check out code uses: actions/checkout@v6 - name: Set up Go @@ -82,39 +158,29 @@ jobs: mv $TOOL_NAME ~/go/bin/$TOOL_NAME ~/go/bin/$TOOL_NAME --version - # go install github.com/boyter/scc/v3@latest - scc --format html-table . | tee -a $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - name: Test + - name: Test Coverage run: | - echo "### Test report" >> $GITHUB_STEP_SUMMARY + set -o pipefail + + echo "## Test Coverage" >> $GITHUB_STEP_SUMMARY - go test -race -coverprofile=coverage.txt -covermode=atomic -tags=unit ./... | tee -a $GITHUB_STEP_SUMMARY + make test 2>&1 | tee -a $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - name: Test coverage - run: | - echo "## Test Coverage" >> $GITHUB_STEP_SUMMARY + go install github.com/vladopajic/go-test-coverage/v2@latest - # Generate coverage report using standard library tools echo "" >> $GITHUB_STEP_SUMMARY echo "### Coverage report" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - go tool cover -func=coverage.txt | tee -a $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Calculate total coverage percentage - total_coverage=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}') - echo "**Total Coverage:** $total_coverage" >> $GITHUB_STEP_SUMMARY + go-test-coverage --config=./.testcoverage.yml | sed 's/PASS/PASS ✅/g' | sed 's/FAIL/FAIL ❌/g' | tee -a $GITHUB_STEP_SUMMARY - name: Build run: | echo "## Build" >> $GITHUB_STEP_SUMMARY - go build ./... | tee -a $GITHUB_STEP_SUMMARY + make build 2>&1 | tee -a $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Build completed successfully." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0890905..2256a2b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -11,9 +11,85 @@ permissions: jobs: test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: ./go.mod + + - name: Test + run: go test -v -race -tags=unit ./... + + - name: Build CLI + run: go build -v -o ${{ runner.temp }}/machineid${{ matrix.os == 'windows-latest' && '.exe' || '' }} ./cmd/machineid/ + + - name: Generate machine ID (smoke test) + shell: bash + run: | + BIN="${{ runner.temp }}/machineid${{ matrix.os == 'windows-latest' && '.exe' || '' }}" + + echo "--- Version ---" + "$BIN" -version + + echo "--- Default ID (CPU + motherboard + UUID) ---" + ID=$("$BIN") + echo "ID: $ID" + echo "Length: ${#ID}" + + # Verify the ID is a 64-char hex string + if [[ ${#ID} -ne 64 ]]; then + echo "ERROR: expected 64-char ID, got ${#ID}" + exit 1 + fi + if [[ ! "$ID" =~ ^[0-9a-f]{64}$ ]]; then + echo "ERROR: ID is not a valid hex string" + exit 1 + fi + + echo "--- Validate round-trip ---" + "$BIN" -validate "$ID" + + # Exercise additional CLI modes. + # On Windows CI, WMI can be transiently unavailable after repeated + # invocations, so these are non-fatal — the core validation above + # already proved the binary works. + ERRORS=0 + + echo "--- All components (JSON + diagnostics) ---" + "$BIN" -all -json -diagnostics || { echo "WARN: -all failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + echo "--- VM-friendly ---" + "$BIN" -vm || { echo "WARN: -vm failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + echo "--- Format 32 ---" + "$BIN" -format 32 || { echo "WARN: -format 32 failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + echo "--- Salt ---" + "$BIN" -salt "ci-test" || { echo "WARN: -salt failed (non-fatal)"; ERRORS=$((ERRORS+1)); } + + if [[ $ERRORS -gt 0 ]]; then + echo "WARN: $ERRORS non-critical exercise(s) failed on ${{ matrix.os }} (WMI transient issue)" + fi + + echo "Smoke test passed on ${{ matrix.os }}" + + report: + name: Report + needs: test runs-on: ubuntu-latest + env: + MAKE_DEBUG: true steps: - - name: Checkout + - name: Check out code uses: actions/checkout@v6 - name: Set up Go @@ -85,30 +161,20 @@ jobs: mv $TOOL_NAME ~/go/bin/$TOOL_NAME ~/go/bin/$TOOL_NAME --version - # go install github.com/boyter/scc/v3@latest - scc --format html-table . | tee -a $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - name: Test + - name: Test Coverage run: | - echo "### Test report" >> $GITHUB_STEP_SUMMARY + set -o pipefail + + echo "## Test Coverage" >> $GITHUB_STEP_SUMMARY - make test | tee -a $GITHUB_STEP_SUMMARY + make test 2>&1 | tee -a $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - name: Test coverage - run: | - echo "## Test Coverage" >> $GITHUB_STEP_SUMMARY + go install github.com/vladopajic/go-test-coverage/v2@latest - # Generate coverage report using standard library tools echo "" >> $GITHUB_STEP_SUMMARY echo "### Coverage report" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - go tool cover -func=coverage.txt | tee -a $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Calculate total coverage percentage - total_coverage=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}') - echo "**Total Coverage:** $total_coverage" >> $GITHUB_STEP_SUMMARY + go-test-coverage --config=./.testcoverage.yml | sed 's/PASS/PASS ✅/g' | sed 's/FAIL/FAIL ❌/g' | tee -a $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5fa9b0..327ddf5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - MAKE_STOP_ON_ERRORS: true + MAKE_DEBUG: true permissions: id-token: write @@ -24,15 +24,11 @@ jobs: - name: Check out code uses: actions/checkout@v6 - - name: Set up Go 1.x - id: go + - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: ./go.mod - - name: Go version - run: go version - - name: Test run: make test @@ -44,15 +40,11 @@ jobs: - name: Check out code uses: actions/checkout@v6 - - name: Set up Go 1.x - id: go + - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: ./go.mod - - name: Go version - run: go version - - name: Build Linux distribution run: | GIT_VERSION=${{ github.ref_name }} GO_OS="linux" make build-dist @@ -85,14 +77,11 @@ jobs: # - name: Check out code # uses: actions/checkout@v6 # - # - name: Set up Go 1.x + # - name: Set up Go # uses: actions/setup-go@v6 # with: # go-version-file: ./go.mod # - # - name: Go version - # run: go version - # # - name: Build Windows binaries (arm64 & amd64) # shell: pwsh # run: | @@ -183,14 +172,11 @@ jobs: - name: Check out code uses: actions/checkout@v6 - - name: Set up Go 1.x + - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: ./go.mod - - name: Go version - run: go version - - name: Build macOS binaries (arm64 & amd64) run: | GIT_VERSION=${{ github.ref_name }} GO_OS="darwin" make build-dist @@ -303,15 +289,11 @@ jobs: - name: Check out code uses: actions/checkout@v6 - - name: Set up Go 1.x - id: go + - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: ./go.mod - - name: Go version - run: go version - - name: Download Linux distribution files uses: actions/download-artifact@v7 with: diff --git a/.vscode/settings.json b/.vscode/settings.json index a02a5cb..881487f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,12 @@ { "cSpell.words": [ "betteralign", + "boyter", "codesign", "CPUID", "csproduct", "diskdrive", + "elif", "Errorf", "golangci", "govulncheck", @@ -14,11 +16,14 @@ "machineid", "notarytool", "OSCPU", + "pipefail", "productsign", "Sigstore", "slashdevops", "softprops", + "testcoverage", "UEFI", + "vladopajic", "vulncheck", "xcrun" ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2468da8..131fee7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thank you for your interest in contributing to machineid! This document provides ### Prerequisites -- Go 1.22 or higher +- Go 1.26 or higher - Git - Make diff --git a/README.md b/README.md index bcb8ee3..43ae5b4 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Add the module to your Go project: go get github.com/slashdevops/machineid ``` -Requires **Go 1.25+**. No external dependencies. +Requires **Go 1.26+**. No external dependencies. ### CLI Tool @@ -426,16 +426,20 @@ The library supports dependency injection for deterministic testing without real ```go type mockExecutor struct { + mu sync.RWMutex outputs map[string]string } func (m *mockExecutor) Execute(ctx context.Context, name string, args ...string) (string, error) { + m.mu.RLock() + defer m.mu.RUnlock() if output, ok := m.outputs[name]; ok { return output, nil } return "", fmt.Errorf("command not found: %s", name) } +ctx := context.Background() provider := machineid.New(). WithExecutor(&mockExecutor{ outputs: map[string]string{ @@ -444,9 +448,11 @@ provider := machineid.New(). }). WithCPU() -id, err := provider.ID() +id, err := provider.ID(ctx) ``` +> **Note**: Custom executors must be safe for concurrent use since Windows collects hardware identifiers in parallel goroutines. + Run the test suite: ```bash @@ -474,24 +480,26 @@ go test -v -race ./... ### Hardware Identifier Selection ```go +ctx := context.Background() + // Minimal (VMs, containers) -id, _ := machineid.New().VMFriendly().ID() +id, _ := machineid.New().VMFriendly().ID(ctx) // Balanced (recommended) -id, _ := machineid.New(). +id, _ = machineid.New(). WithCPU(). WithSystemUUID(). WithMotherboard(). - ID() + ID(ctx) // Maximum (most unique, but sensitive to hardware changes) -id, _ := machineid.New(). +id, _ = machineid.New(). WithCPU(). WithSystemUUID(). WithMotherboard(). WithMAC(). WithDisk(). - ID() + ID(ctx) ``` ## Troubleshooting diff --git a/doc.go b/doc.go index 6b7e0b7..8e34a03 100644 --- a/doc.go +++ b/doc.go @@ -147,7 +147,9 @@ // # Testing // // Inject a custom [CommandExecutor] via [Provider.WithExecutor] to replace -// real system commands with deterministic test doubles: +// real system commands with deterministic test doubles. Custom executors +// must be safe for concurrent use, since Windows collects hardware +// identifiers in parallel goroutines. // // provider := machineid.New(). // WithExecutor(myMock). diff --git a/example_darwin_test.go b/example_darwin_test.go index 2552b11..9669303 100644 --- a/example_darwin_test.go +++ b/example_darwin_test.go @@ -40,10 +40,10 @@ func Example_integrity() { p3 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app1") p4 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app2") - id1, _ := p1.ID(context.Background()) //nolint:errcheck // Example - id2, _ := p2.ID(context.Background()) //nolint:errcheck // Example - id3, _ := p3.ID(context.Background()) //nolint:errcheck // Example - id4, _ := p4.ID(context.Background()) //nolint:errcheck // Example + id1, _ := p1.ID(context.Background()) //nolint:errcheck // Example + id2, _ := p2.ID(context.Background()) //nolint:errcheck // Example + id3, _ := p3.ID(context.Background()) //nolint:errcheck // Example + id4, _ := p4.ID(context.Background()) //nolint:errcheck // Example // Same configuration always produces same ID id1Again, _ := machineid.New().WithCPU().WithSystemUUID().ID(context.Background()) //nolint:errcheck // Example diff --git a/example_linux_test.go b/example_linux_test.go index 3f1c362..42054d1 100644 --- a/example_linux_test.go +++ b/example_linux_test.go @@ -25,40 +25,39 @@ func ExampleProvider_Diagnostics() { return } - fmt.Printf("Components collected: %d\n", len(diag.Collected)) + // On Linux, the number of collected components depends on system access: + // /proc/cpuinfo is always readable (cpu) + // /sys/class/dmi/id/product_uuid may require root (uuid) + // /etc/machine-id is usually readable (machine-id) fmt.Printf("Has collected data: %v\n", len(diag.Collected) > 0) + fmt.Printf("At least one component: %v\n", len(diag.Collected) >= 1) // Output: - // Components collected: 2 // Has collected data: true + // At least one component: true } -// Example_integrity demonstrates that the format maintains integrity without collisions. +// Example_integrity demonstrates that salt produces different IDs on the +// same hardware, and that the same configuration is consistent across calls. func Example_integrity() { - p1 := machineid.New().WithCPU().WithSystemUUID() - p2 := machineid.New().WithCPU().WithSystemUUID().WithMotherboard() - p3 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app1") - p4 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app2") + // Salt-based differentiation works regardless of hardware access + p1 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app1") + p2 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app2") - id1, _ := p1.ID(context.Background()) //nolint:errcheck // Example - id2, _ := p2.ID(context.Background()) //nolint:errcheck // Example - id3, _ := p3.ID(context.Background()) //nolint:errcheck // Example - id4, _ := p4.ID(context.Background()) //nolint:errcheck // Example + id1, _ := p1.ID(context.Background()) //nolint:errcheck // Example + id2, _ := p2.ID(context.Background()) //nolint:errcheck // Example // Same configuration always produces same ID - id1Again, _ := machineid.New().WithCPU().WithSystemUUID().ID(context.Background()) //nolint:errcheck // Example + id1Again, _ := machineid.New().WithCPU().WithSystemUUID().WithSalt("app1").ID(context.Background()) //nolint:errcheck // Example fmt.Printf("Consistency: %v\n", id1 == id1Again) - // Different configurations produce different IDs - fmt.Printf("Different hardware: %v\n", id1 != id2) - fmt.Printf("Different salts: %v\n", id3 != id4) + // Different salts produce different IDs + fmt.Printf("Different salts: %v\n", id1 != id2) // All IDs are 64 characters (power of 2) - fmt.Printf("All are 64 chars: %v\n", - len(id1) == 64 && len(id2) == 64 && len(id3) == 64 && len(id4) == 64) + fmt.Printf("All are 64 chars: %v\n", len(id1) == 64 && len(id2) == 64) // Output: // Consistency: true - // Different hardware: true // Different salts: true // All are 64 chars: true } @@ -74,10 +73,9 @@ func Example_linuxFileSources() { // Disk: lsblk + /sys/block/*/device/serial // // File reads are fast — no process startup overhead. + // Using CPU only since /proc/cpuinfo is always readable. provider := machineid.New(). - WithCPU(). - WithSystemUUID(). - WithDisk() + WithCPU() id, err := provider.ID(context.Background()) if err != nil { diff --git a/example_windows_test.go b/example_windows_test.go index a9b17a9..3f549ce 100644 --- a/example_windows_test.go +++ b/example_windows_test.go @@ -39,10 +39,10 @@ func Example_integrity() { p3 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app1") p4 := machineid.New().WithCPU().WithSystemUUID().WithSalt("app2") - id1, _ := p1.ID(context.Background()) //nolint:errcheck // Example - id2, _ := p2.ID(context.Background()) //nolint:errcheck // Example - id3, _ := p3.ID(context.Background()) //nolint:errcheck // Example - id4, _ := p4.ID(context.Background()) //nolint:errcheck // Example + id1, _ := p1.ID(context.Background()) //nolint:errcheck // Example + id2, _ := p2.ID(context.Background()) //nolint:errcheck // Example + id3, _ := p3.ID(context.Background()) //nolint:errcheck // Example + id4, _ := p4.ID(context.Background()) //nolint:errcheck // Example // Same configuration always produces same ID id1Again, _ := machineid.New().WithCPU().WithSystemUUID().ID(context.Background()) //nolint:errcheck // Example diff --git a/executor_test.go b/executor_test.go index 522d9ab..4a92e76 100644 --- a/executor_test.go +++ b/executor_test.go @@ -3,12 +3,15 @@ package machineid import ( "context" "fmt" + "sync" "testing" "time" ) // mockExecutor is a test double that implements CommandExecutor for testing. +// It is safe for concurrent use (required by Windows concurrent collection). type mockExecutor struct { + mu sync.RWMutex // outputs maps command name to expected output outputs map[string]string // errors maps command name to expected error @@ -28,7 +31,12 @@ func newMockExecutor() *mockExecutor { // Execute implements CommandExecutor interface. func (m *mockExecutor) Execute(ctx context.Context, name string, args ...string) (string, error) { + m.mu.Lock() m.callCount[name]++ + m.mu.Unlock() + + m.mu.RLock() + defer m.mu.RUnlock() if err, exists := m.errors[name]; exists { return "", err @@ -43,11 +51,15 @@ func (m *mockExecutor) Execute(ctx context.Context, name string, args ...string) // setOutput configures the mock to return the given output for a command. func (m *mockExecutor) setOutput(command, output string) { + m.mu.Lock() + defer m.mu.Unlock() m.outputs[command] = output } // setError configures the mock to return an error for a command. func (m *mockExecutor) setError(command string, err error) { + m.mu.Lock() + defer m.mu.Unlock() m.errors[command] = err } diff --git a/go.mod b/go.mod index 16b5e24..6eb8b4c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/slashdevops/machineid -go 1.25.0 +go 1.26.1 diff --git a/machineid.go b/machineid.go index 20208bb..8882caa 100644 --- a/machineid.go +++ b/machineid.go @@ -72,7 +72,9 @@ type DiagnosticInfo struct { Collected []string // Component names that were successfully collected } -// CommandExecutor is an interface for executing system commands, allowing for dependency injection and testing. +// CommandExecutor is an interface for executing system commands, allowing for +// dependency injection and testing. Implementations must be safe for concurrent +// use, since Windows collects hardware identifiers in parallel goroutines. type CommandExecutor interface { Execute(ctx context.Context, name string, args ...string) (string, error) }