This guide covers setting up a development environment, running tests, and following the project's conventions.
- Go 1.26+ -- required for workspace support and language features
- Git -- version control
- Make -- build automation
- govulncheck (optional but recommended) -- vulnerability scanning
Install govulncheck:
go install golang.org/x/vuln/cmd/govulncheck@latestgit clone https://github.com/scrutineer/scrutineer.git
cd scrutineerThe project includes a pre-push hook that enforces code quality before pushing:
git config core.hooksPath .githooksThis activates the pre-push hook at .githooks/pre-push, which runs:
gofmt-- all files must be formattedgo vet ./...-- static analysis for all modulesgovulncheck ./...-- vulnerability scanning (skipped if not installed)go test -race -coverprofile=coverage.out ./...-- tests with race detection- Coverage gate -- minimum 98% coverage per module
The project uses Go workspaces (go.work) for multi-module development. The workspace file references all nine modules:
go 1.26.2
use (
./cmd/scrutineer
./connector/browser
./connector/cli
./connector/grpc
./connector/http
./connector/ssh
./core
./fuzz
./loadtest
)
With the workspace in place, cross-module imports resolve locally. You can edit any module and all dependent modules see the changes immediately.
make testThis runs go test -race -coverprofile=coverage.out ./... in each module.
cd core && go test -race ./...
cd connector/http && go test -race ./...cd core && go test -race -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go tool cover -html=coverage.out # opens in browsermake coverageThis runs all tests and then checks that each module meets the 98% coverage threshold. If any module falls below 98%, the command fails with an error message like:
FAIL: connector/http coverage 96.5% < 98%
All code must be formatted with gofmt. Check formatting:
gofmt -l .Format all files:
make fmtmake vetThis runs go vet ./... in every module.
make vulnThis runs govulncheck ./... in every module. Requires govulncheck to be installed.
Run everything the pre-push hook checks:
make precommitThis runs: fmt, vet, vuln, test, coverage.
The project requires a minimum of 98% code coverage, with 100% as the target. Every feature must have:
- Happy path unit tests -- normal operation
- Sad path unit tests -- error conditions, edge cases, invalid input
- Integration tests -- component interaction
- End-to-end tests -- full workflow validation
When adding new code, always add corresponding tests. The pre-push hook will reject pushes that drop below 98%.
The core module has zero external dependencies. It imports only Go standard library packages. This is a strict rule -- never add external imports to any package under core/.
Each connector is an independent module in connector/<name>/. Connectors:
- Import
core/connectorfor the interface and types - May import approved external dependencies (see below)
- Must not import other connectors
- Must not be imported by the core module
| Dependency | Used By | Reason |
|---|---|---|
golang.org/x/crypto/ssh |
connector/ssh |
SSH protocol implementation |
google.golang.org/grpc |
connector/grpc |
gRPC protocol implementation |
google.golang.org/protobuf |
connector/grpc |
Protocol buffer handling |
All other external dependencies are prohibited. If you need functionality not in the standard library, implement it from scratch.
loadtestdepends oncore/connectorandconnector/ssh(for distributed testing)fuzzdepends oncore/connector(for executing fuzz inputs)- Neither should depend on specific connector implementations beyond their stated needs
- Create the directory under the module (e.g.,
core/newpackage/) - Add a
doc.goor main.gofile with the package declaration - Write implementation and tests
- Ensure tests pass with race detection:
go test -race ./... - Ensure coverage meets 98%
- Create the directory (e.g.,
connector/newconn/) - Initialize the module:
cd connector/newconn && go mod init github.com/scrutineer/scrutineer/connector/newconn - Add the module to
go.work:use ( ./connector/newconn // ... existing modules ) - Add the module to the
MODULESlist inMakefile - Update
.githooks/pre-pushif it maintains a separate module list - Implement the connector (see Extending Scrutineer)
- Register it in
cmd/scrutineer/main.go - Write comprehensive tests
| Command | Description |
|---|---|
make build |
Build the scrutineer binary into bin/scrutineer |
make cross |
Cross-compile for all 6 platform/arch combinations |
make test |
Run tests with race detection in all modules |
make coverage |
Run tests and enforce 98% coverage gate |
make fmt |
Format all Go files with gofmt |
make vet |
Run go vet on all modules |
make vuln |
Run govulncheck on all modules |
make clean |
Remove binaries and coverage files |
make precommit |
Run all checks (fmt, vet, vuln, test, coverage) |
make all |
Run fmt, vet, test, build |
govulncheck scans for known vulnerabilities in your dependencies:
# Install (one-time)
go install golang.org/x/vuln/cmd/govulncheck@latest
# Run on all modules
make vuln
# Run on a single module
cd connector/grpc && govulncheck ./...govulncheck is the only permitted golang.org/x/ tool. It runs as an external CLI -- it is never imported as a library.
Define interfaces before implementations. Use Go's struct + interface patterns for OO design:
// Interface in core
type Reporter interface {
OnSuiteStart(suite SuiteInfo)
OnTestEnd(test TestInfo, result TestResult)
Flush(w io.Writer) error
}
// Implementation
type ANSIReporter struct { ... }
var _ Reporter = (*ANSIReporter)(nil) // compile-time checkUse the functional options pattern for configurable constructors:
type Option func(*Engine)
func WithParallelism(n int) Option {
return func(e *Engine) { e.parallelism = n }
}
func New(opts ...Option) *Engine { ... }Return clear, actionable errors. Wrap errors with context:
return fmt.Errorf("redis: get key %q: %w", key, err)Use sentinel errors or typed errors where callers need to inspect error types.
Place tests alongside the code they test:
core/assertion/
assertion.go
assertion_test.go
contains.go
contains_test.go
All exported types, functions, and interfaces must have documentation comments. The first sentence should be a complete sentence starting with the name of the thing being documented:
// Registry maps connector names to their factories.
type Registry struct { ... }
// Register adds a connector factory under the given name.
// Returns an error if name is already registered.
func (r *Registry) Register(name string, f Factory) error { ... }- Versioning Policy -- release process and semver rules
- Architecture Overview -- system design
- Extending Scrutineer -- adding connectors and assertions