test(go): step 6 — integration tests covering all exit codes and output

Extract run(args, stdout, stderr) int from main() for in-process
testability. Fix TLS failure classification (tlsErr now captured from
TLSHandshakeDone hook). Add run_test.go with 14 table-driven in-process
tests and cli_test.go with TestMain + 3 subprocess smoke tests. All
servers use httptest; .invalid TLD for deterministic DNS failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 01:00:20 +02:00
parent c323d879d0
commit a9534ec2c1
8 changed files with 727 additions and 95 deletions

View File

@@ -61,6 +61,7 @@ func Measure(url string, opts Options) Result {
wroteRequest time.Time
firstByte time.Time
dnsErr error
tlsErr error
)
trace := &httptrace.ClientTrace{
@@ -74,9 +75,12 @@ func Measure(url string, opts Options) Result {
connectStart = time.Now()
}
},
ConnectDone: func(_, _ string, _ error) { connectDone = time.Now() },
ConnectDone: func(_, _ string, _ error) { connectDone = time.Now() },
TLSHandshakeStart: func() { tlsStart = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { tlsDone = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, err error) {
tlsDone = time.Now()
tlsErr = err
},
WroteRequest: func(_ httptrace.WroteRequestInfo) { wroteRequest = time.Now() },
GotFirstResponseByte: func() { firstByte = time.Now() },
}
@@ -100,7 +104,7 @@ func Measure(url string, opts Options) Result {
if err != nil {
end := time.Now()
r.Err = err
r.FailPhase = classifyErr(err, dnsErr, tlsStart, tlsDone)
r.FailPhase = classifyErr(err, dnsErr, tlsErr, tlsStart)
r.Total = Phase{Duration: end.Sub(start), Present: true}
r.DNS = makePhase(dnsStart, dnsDone)
r.Connect = makePhase(connectStart, connectDone)
@@ -137,7 +141,10 @@ func makePhase(start, end time.Time) Phase {
return Phase{Duration: end.Sub(start), Present: true}
}
func classifyErr(err, dnsErr error, tlsStart, tlsDone time.Time) string {
// classifyErr maps a Do() error to the phase that caused it.
// Order: dns → timeout → tls → connect.
// Timeout during TLS still classifies as timeout, not tls.
func classifyErr(err, dnsErr, tlsErr error, tlsStart time.Time) string {
if dnsErr != nil {
return "dns"
}
@@ -152,8 +159,11 @@ func classifyErr(err, dnsErr error, tlsStart, tlsDone time.Time) string {
if errors.As(err, &netErr) && netErr.Timeout() {
return "timeout"
}
// TLS started but handshake never completed
if !tlsStart.IsZero() && tlsDone.IsZero() {
if tlsErr != nil {
return "tls"
}
// TLS started but handshake was interrupted (e.g. context cancelled mid-handshake)
if !tlsStart.IsZero() {
return "tls"
}
return "connect"