Delve — Testing#

Test metrics#

  • Test files: 53 (including 1 fixture-internal test and 1 fuzz test)
  • Total Go files: 506
  • Ratio (test files / source files): ~12% (53 / 453 source files)
  • Test frameworks: Stdlib testing only — no testify, gomock, ginkgo, or any assertion library

Test organization#

  • Placement: Mixed — most test files use package xxx_test (external black-box style) for the large integration tests and package xxx (white-box) for unit-level tests. The dominant pattern is _test package for service-layer tests (package service_test, package dap) and same-package for proc-level tests.
  • Helper packages:
    • pkg/proc/test (imported as protest) — the primary test support library. Contains:
      • BuildFixture(t, name, flags) — compiles a named fixture program from _fixtures/ on demand with configurable BuildFlags (inlining, optimization, CGO, PIE, plugin, trimpath, DWZ compression, DWARF strip). Fixtures are cached by (name, flags) pair and compiled concurrently.
      • RunTestsWithFixtures(m) — must be called from TestMain; enables fixture compilation and cleans up temp binaries after all tests run.
      • AllowRecording(t) / MustHaveRecordingAllowed(t) — opt-in / opt-out mechanism for rr backend testing.
      • DefaultTestBackend(testBackend *string) — resolves the PROCTEST env var to select the debug backend.
      • MustSupportFunctionCalls(t, backend) — skips tests on platforms that don’t yet support function call injection.
      • FindFixturesDir() — walks up 10 directory levels to find _fixtures/.
      • skipOn(t, reason, conditions...) / skipUnlessOn(t, reason, conditions...) — runtime skip helpers that combine GOOS, GOARCH, testBackend, and buildMode checks.
    • service/dap/daptest/ — a DAP test client (client.go, resp.go) that wraps net.Conn and the go-dap codec to send DAP requests and assert responses in tests of the DAP server.
  • Fixtures: 175 .go programs in _fixtures/, each targeting a specific debugger scenario (variable evaluation, stepping, goroutines, CGO, plugins, eBPF, etc.). Fixtures are compiled at test time — never pre-built binaries — which ensures they are always compiled with the current Go toolchain and debug info.

Test patterns#

Table-driven tests#

  • Prevalence: Heavy — 197 occurrences of t.Run, testCases, or tc.name patterns in test files.
  • Style: Anonymous or named struct slices — typically testcases := []struct{ name string; ... }{ ... } followed by for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { ... }) }. Example in variables_test.go:116: a 30+ entry table covering every Go type for variable evaluation.
  • Example: pkg/proc/variables_test.go:116TestVariableEvaluation uses a []struct{ name, st, value, length, cap, childrenlen } table with one entry per Go type. pkg/proc/evalop/evalop_test.go — table-driven opcode depth-check tests. service/dap/server_test.go:172 — subtests parameterized over stop modes (client connected, client disconnected, etc.).

Multi-backend parameterization#

  • Mechanism: The -backend CLI flag (defaulting to PROCTEST env var or "native") selects which backend (native, lldb, rr) is used when withTestProcess / withTestClient sets up a test target. Tests that cannot run under rr call protest.MustHaveRecordingAllowed(t) to skip; tests that opt in call protest.AllowRecording(t).
  • Significance: The same ~400 test functions in pkg/proc/proc_test.go, variables_test.go, and service/test/integration2_test.go run against all three backends in CI. This is the mechanism that validates backend parity.
  • Example: proc_test.go:108–130startTestProcessArgs switch on testBackend routes to native.Launch, gdbserial.LLDBLaunch, or gdbserial.RecordAndReplay.

Mocking approach#

  • Strategy: None — Delve’s tests use no mocking whatsoever. There are no generated mocks, no gomock controllers, and no fake implementations of proc.Process or proc.ProcessInternal. Tests exercise the real process backend against real compiled binaries. The only “fake” is the DAP test client (daptest/), which is a real client that speaks the real protocol over a pipe.
  • Rationale: Debugger correctness requires testing against real OS processes, real DWARF debug info, and real CPU state. A mock ProcessInternal would be useless for validating ptrace behavior or register reads.

Integration tests#

  • Present: Yes — two large integration test files:
    • service/test/integration2_test.go (3,491 lines) — exercises the full JSON-RPC 2.0 stack: starts a real rpccommon.NewServer over a net.Pipe(), creates an rpc2.NewClient, and drives breakpoints, stepping, variable evaluation, goroutine listing, etc. through the public service API.
    • service/dap/server_test.go (8,655 lines) — exercises the full DAP stack: starts a dap.NewServer, uses daptest.NewClient to send initialize/launch/setBreakpoints/continue/etc. DAP requests, and asserts the JSON responses. This is the largest test file in the project.
  • How: In-process, via net.Pipe() — no Docker, no external services, no testcontainers. The test spins up the server goroutine and client in the same process, connected by a pipe.
  • Separation: Not separated into a distinct package or build tag — they live alongside unit tests but are organized by layer (service/ vs pkg/).

Platform-specific tests#

  • Files: proc_linux_test.go, proc_darwin_test.go, proc_darwin_amd64_test.go, proc_amd64_test.go, proc_unix_test.go, proc_general_test.go, debugger_unix_test.go, sameuser_linux_test.go — mirroring the filename-based conditional compilation used in production code.
  • Build tags: Only 3 test files use //go:build tags: proc_unix_test.go (linux || darwin), ebpf/helpers_test.go (linux && amd64 && cgo && go1.16), debugger_unix_test.go (!windows). All other platform restrictions use runtime skipOn() calls, keeping the tests in a single build.
  • skipOn pattern: skipOn(t, "broken", "linux", "386", "pie") — takes a reason string and 1-N conditions that are AND-matched against GOOS, GOARCH, testBackend, and buildMode. This is more expressive than build tags for conditions that combine OS, arch, and backend.

Fuzz testing#

  • Present: pkg/proc/variables_fuzz_test.goFuzzEvalExpression fuzzes the variable loader and expression evaluator. Requires a one-time setup step (-fuzzevalexpressionsetup) to compile a fixture binary and capture a core dump, then runs go test -fuzz FuzzEvalExpression replaying mutations against the frozen core dump. This is a high-value fuzz target: expression evaluation is a complex parser/interpreter operating on unsafe memory.

eBPF tests#

  • Special handling: eBPF tests (TestTraceEBPF*) require elevated Linux capabilities (CAP_BPF, CAP_PERFMON, CAP_SYS_RESOURCE) and must be run with sudo. The CLAUDE.md documents running these under Docker with --privileged for CI environments that cannot grant kernel capabilities to the test runner.

Test quality observations#

  • What’s done well:

    • Fixture compile-on-demand system (protest.BuildFixture) is an elegant solution to the hard problem of testing a debugger: tests need real compiled binaries with specific debug info characteristics (inlining on/off, optimization on/off, PIE, plugins). Fixtures are built at test time, cached by key, cleaned up automatically, and compile flags are first-class parameters.
    • Backend-agnostic test design: Writing a single test function that runs identically against native, lldb, and rr backends enforces a strong behavioral contract on all backends and catches regressions immediately. The AllowRecording / MustHaveRecordingAllowed double-opt-in pattern is pragmatic about the subset of tests that are compatible with record/replay.
    • No mocking discipline: The decision to never mock ProcessInternal means tests validate real system behavior. This trades test speed for correctness — appropriate for a debugger where correctness is paramount.
    • Table-driven variable tests: TestVariableEvaluation and TestEvalExpression exhaustively cover Go type system edge cases in tabular form, making it easy to add a new type assertion without writing a new function.
    • DAP server test coverage: server_test.go at 8,655 lines is extremely comprehensive — it tests every DAP request type, edge cases in attach/launch/stop/disconnect flows, and output event formatting. This is the most thorough protocol-level test coverage in the project.
    • Fuzz corpus for expression evaluator: Having a fuzz test (FuzzEvalExpression) for the most complex and security-relevant subsystem (parsing and executing DWARF expressions against process memory) is best practice for a tool that handles untrusted binaries.
  • What could improve:

    • No mocking means slow tests: Every test that exercises variable evaluation starts a real OS process. Tests in proc_test.go (6,171 lines) are real process integration tests, not unit tests. Fast iteration is expensive. There is no unit test layer for the expression evaluator that doesn’t require a live process.
    • Test file size: server_test.go at 8,655 lines and proc_test.go at 6,171 lines are enormous. Navigation and review are challenging. Sub-package or file splitting would improve discoverability without changing test quality.
    • Limited CI file visibility: Only a CI config for Windows ARM64 and a release workflow are visible in .github/workflows/. The main Linux/macOS CI matrix is not in the repository (possibly in a separate CI system or omitted from the analyzed snapshot), which makes it hard to observe the full test matrix.
  • Patterns worth emulating:

    • The protest fixture system: Any project that needs to test behavior against compiled artifacts (compilers, linkers, analyzers, debuggers) should adopt this pattern — compile-on-demand with flag parameterization, lifecycle management in TestMain, cleanup via deferred path registration.
    • Multi-backend test parameterization via flags: Running the same test suite against multiple backends with a flag (not build tags) is more flexible and catches divergence earlier than maintaining separate test suites.
    • The skipOn(reason, conditions...) helper: More ergonomic than build tags for multi-dimensional skip conditions. The reason string is invaluable for understanding why a test is skipped, especially when conditions reference issue URLs.
    • Protocol-level testing with an in-process client: daptest.NewClient over net.Pipe() tests the full serialization/deserialization path and response ordering without network overhead or external process management. This pattern generalizes to any protocol server test.