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>
106 lines
3.2 KiB
Go
106 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// binaryPath is set once by TestMain and used by all CLI smoke tests.
|
|
var binaryPath string
|
|
|
|
// TestMain builds the real binary once, runs all tests, then cleans up.
|
|
func TestMain(m *testing.M) {
|
|
tmp, err := os.MkdirTemp("", "latprobe-cli-*")
|
|
if err != nil {
|
|
panic("TestMain: MkdirTemp: " + err.Error())
|
|
}
|
|
|
|
binaryPath = filepath.Join(tmp, "latprobe")
|
|
out, err := exec.Command("go", "build", "-o", binaryPath, ".").CombinedOutput()
|
|
if err != nil {
|
|
panic("TestMain: go build failed:\n" + string(out))
|
|
}
|
|
|
|
code := m.Run()
|
|
os.RemoveAll(tmp)
|
|
os.Exit(code)
|
|
}
|
|
|
|
// cli runs the compiled binary and returns stdout, stderr, and the exit code.
|
|
func cli(args ...string) (stdout, stderr string, code int) {
|
|
cmd := exec.Command(binaryPath, args...)
|
|
var outBuf, errBuf strings.Builder
|
|
cmd.Stdout = &outBuf
|
|
cmd.Stderr = &errBuf
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if ex, ok := err.(*exec.ExitError); ok {
|
|
return outBuf.String(), errBuf.String(), ex.ExitCode()
|
|
}
|
|
}
|
|
return outBuf.String(), errBuf.String(), 0
|
|
}
|
|
|
|
// ── smoke tests ───────────────────────────────────────────────────────────────
|
|
|
|
// TestCLISuccess verifies the happy path against a real local server:
|
|
// all phases shown, status 200, exit 0.
|
|
func TestCLISuccess(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
stdout, stderr, code := cli(srv.URL)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0\nstdout: %s\nstderr: %s", code, stdout, stderr)
|
|
}
|
|
// httptest uses 127.0.0.1 — Go skips DNS for loopback, so no DNS row.
|
|
for _, want := range []string{"(200)", "TCP connect", "Total"} {
|
|
if !strings.Contains(stdout, want) {
|
|
t.Errorf("stdout missing %q\nstdout: %s", want, stdout)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCLIDNSFailure verifies that an NXDOMAIN resolves to exit 2 in the
|
|
// real binary (exercises the actual os.Exit path).
|
|
func TestCLIDNSFailure(t *testing.T) {
|
|
_, _, code := cli("https://no.such.host.for.cli.smoke.invalid")
|
|
if code != exitDNS {
|
|
t.Errorf("exit code = %d, want %d (dns)", code, exitDNS)
|
|
}
|
|
}
|
|
|
|
// TestCLIJSONDNSFailure checks that --json + a DNS failure produces valid JSON
|
|
// with a correctly classified error entry — exercising the full output pipeline.
|
|
func TestCLIJSONDNSFailure(t *testing.T) {
|
|
stdout, _, code := cli("--json", "https://no.such.host.json.cli.invalid")
|
|
if code != exitDNS {
|
|
t.Fatalf("exit code = %d, want %d\nstdout: %s", code, exitDNS, stdout)
|
|
}
|
|
|
|
var results []struct {
|
|
Succeeded int `json:"succeeded"`
|
|
Failed int `json:"failed"`
|
|
Errors []struct {
|
|
Phase string `json:"phase"`
|
|
} `json:"errors"`
|
|
}
|
|
if err := json.Unmarshal([]byte(stdout), &results); err != nil {
|
|
t.Fatalf("invalid JSON: %v\nstdout: %s", err, stdout)
|
|
}
|
|
if len(results) == 0 || len(results[0].Errors) == 0 {
|
|
t.Fatalf("expected non-empty errors in JSON\nstdout: %s", stdout)
|
|
}
|
|
if results[0].Errors[0].Phase != "dns" {
|
|
t.Errorf("error phase = %q, want \"dns\"", results[0].Errors[0].Phase)
|
|
}
|
|
}
|