Rearchitecture step 2 of 6. Sequence: #54 → 2 → #56 → #48 → #18 → #41.
Problem
die() calls os.Exit from the middle of library-shaped code, so every error path in the program terminates the process:
func die(rc int, format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "\nError: "+format, args...)
os.Exit(rc)
}
It is reached from mustParseLogLevel, mustParseHostMappings, and three sites inside the cobra Run closure. There is no seam between "decide what went wrong" and "terminate" — which means no error path can be exercised without spawning a subprocess. None of them are.
This is the architectural reason coverage sits at 39%, and it is upstream of several filed bugs: there was no cheap place to put validation, so validation didn't get written.
Suggested fix
The standard Go shape — a testable core, with process termination pushed to the edge:
Before:
func main() {
cmd := &cobra.Command{ /* … */ } // die() → os.Exit() from anywhere inside
if err := cmd.ExecuteContext(context.Background()); err != nil {
die(103, "%s", err)
}
}
After:
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}
func run(args []string, stdout, stderr io.Writer) int {
// … returns an exit code; never calls os.Exit
}
die becomes an error value carrying a code, not a call that ends the world:
type exitError struct {
code int
msg string
}
Why this is step 2
It unblocks nearly everything else:
Scope
This is a mechanical restructuring with no behaviour change intended — same exit codes, same messages, same stream for each. Any behaviour change that falls out should be filed separately rather than folded in, so the diff stays reviewable.
Related
Problem
die()callsos.Exitfrom the middle of library-shaped code, so every error path in the program terminates the process:It is reached from
mustParseLogLevel,mustParseHostMappings, and three sites inside the cobraRunclosure. There is no seam between "decide what went wrong" and "terminate" — which means no error path can be exercised without spawning a subprocess. None of them are.This is the architectural reason coverage sits at 39%, and it is upstream of several filed bugs: there was no cheap place to put validation, so validation didn't get written.
Suggested fix
The standard Go shape — a testable core, with process termination pushed to the edge:
Before:
After:
diebecomes an error value carrying a code, not a call that ends the world:Why this is step 2
It unblocks nearly everything else:
run()with args and capture the writers, no subprocess, no build step in the test path.--max-timeaccepts 0 and negatives) is currently unwritten because validation had nowhere to go.run()'s inputs.Scope
This is a mechanical restructuring with no behaviour change intended — same exit codes, same messages, same stream for each. Any behaviour change that falls out should be filed separately rather than folded in, so the diff stays reviewable.
Related