You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

308 lines
11 KiB

package diagnostics
import (
"context"
"errors"
"fmt"
"net"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
)
type fakeExecutor struct {
path string
args []string
maxOutput int
result Execution
err error
}
type waitingExecutor struct{}
type executorCall struct {
path string
args []string
}
type sequenceExecutor struct {
calls []executorCall
results []Execution
index int
}
func (e *sequenceExecutor) Execute(_ context.Context, path string, args []string, _ int) (Execution, error) {
e.calls = append(e.calls, executorCall{path: path, args: append([]string(nil), args...)})
if e.index >= len(e.results) {
return Execution{}, errors.New("unexpected diagnostic execution")
}
result := e.results[e.index]
e.index++
return result, nil
}
func (waitingExecutor) Execute(ctx context.Context, _ string, _ []string, _ int) (Execution, error) {
<-ctx.Done()
return Execution{}, ctx.Err()
}
func (e *fakeExecutor) Execute(_ context.Context, path string, args []string, maxOutput int) (Execution, error) {
e.path = path
e.args = append([]string(nil), args...)
e.maxOutput = maxOutput
return e.result, e.err
}
func TestProcessRunnerUsesFixedExecutableAndArguments(t *testing.T) {
executor := &fakeExecutor{result: Execution{Output: "ok", Bytes: 2, ExitCode: 0}}
runner, err := NewProcessRunner(ProcessRunnerOptions{
PingPath: "/opt/tera/bin/ping",
TraceroutePath: "/opt/tera/bin/traceroute",
Executor: executor,
})
if err != nil {
t.Fatal(err)
}
result, err := runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "192.0.2.1", TimeoutSeconds: 30, MaxOutputBytes: 4096})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if executor.path != "/opt/tera/bin/ping" || !reflect.DeepEqual(executor.args, []string{"-c", "4", "-s", "56", "-W", "1", "-w", "5", "192.0.2.1"}) {
t.Fatalf("command = %q %#v", executor.path, executor.args)
}
if executor.maxOutput != 4096 || result.Reachable == nil || !*result.Reachable {
t.Fatalf("execution limit/result = %d %+v", executor.maxOutput, result)
}
}
func TestProcessRunnerUsesTTLIncrementingPingTraceroute(t *testing.T) {
executor := &sequenceExecutor{results: []Execution{
{Output: "From 192.0.2.1 icmp_seq=1 Time to live exceeded", ExitCode: 1},
{Output: "64 bytes from 192.0.2.10: icmp_seq=1 ttl=63 time=1.25 ms", ExitCode: 0},
}}
runner, err := NewProcessRunner(ProcessRunnerOptions{
PingPath: "/opt/tera/bin/ping",
TraceroutePath: "/opt/tera/bin/traceroute",
Executor: executor,
})
if err != nil {
t.Fatal(err)
}
result, err := runner.Run(context.Background(), Request{Kind: KindTraceroute, Target: "192.0.2.10", MaxHops: 24, TimeoutSeconds: 77, MaxOutputBytes: 4096})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
wantCalls := []executorCall{
{path: "/opt/tera/bin/ping", args: []string{"-c", "1", "-s", "56", "-W", "1", "-t", "1", "192.0.2.10"}},
{path: "/opt/tera/bin/ping", args: []string{"-c", "1", "-s", "56", "-W", "1", "-t", "2", "192.0.2.10"}},
}
if !reflect.DeepEqual(executor.calls, wantCalls) {
t.Fatalf("commands = %#v", executor.calls)
}
if result.Reachable == nil || !*result.Reachable || result.MaxHops != 24 || result.ExitCode != 0 {
t.Fatalf("result = %+v, want reachable traceroute", result)
}
if !strings.Contains(result.Output, " 1 192.0.2.1") || !strings.Contains(result.Output, " 2 192.0.2.10 1.25 ms") {
t.Fatalf("output = %q", result.Output)
}
if operationResult := result.OperationResult(77, 4096); operationResult["max_hops"] != 24 {
t.Fatalf("operation result = %#v, want max_hops 24", operationResult)
}
}
func TestProcessRunnerPingTracerouteStopsOnHostUnreachable(t *testing.T) {
executor := &sequenceExecutor{results: []Execution{
{Output: "From 192.0.2.1 icmp_seq=1 Destination Host Unreachable", ExitCode: 1},
}}
runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor})
if err != nil {
t.Fatal(err)
}
result, err := runner.Run(context.Background(), Request{Kind: KindTraceroute, Target: "192.0.2.10", MaxHops: 10, TimeoutSeconds: 35, MaxOutputBytes: 1024})
if err != nil {
t.Fatal(err)
}
if len(executor.calls) != 1 || result.Reachable == nil || *result.Reachable || result.ExitCode != 1 || !strings.Contains(result.Output, "!H") {
t.Fatalf("calls/result = %#v %+v", executor.calls, result)
}
}
func TestProcessRunnerTreatsDiagnosticExitAsResult(t *testing.T) {
executor := &fakeExecutor{result: Execution{Output: "unreachable", Bytes: 11, ExitCode: 1}}
runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor})
if err != nil {
t.Fatal(err)
}
result, err := runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "example.test", TimeoutSeconds: 5, MaxOutputBytes: 1024})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if result.ExitCode != 1 || result.Reachable == nil || *result.Reachable {
t.Fatalf("result = %+v, want unreachable exit 1", result)
}
}
func TestProcessRunnerAppliesRequestedPingCount(t *testing.T) {
executor := &fakeExecutor{result: Execution{Output: "ok", Bytes: 2, ExitCode: 0}}
runner, err := NewProcessRunner(ProcessRunnerOptions{
PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor,
})
if err != nil {
t.Fatal(err)
}
result, err := runner.Run(context.Background(), Request{
Kind: KindNetworkTest, Target: "192.0.2.1", Count: 5, TimeoutSeconds: 7, MaxOutputBytes: 1024,
})
if err != nil {
t.Fatal(err)
}
wantArgs := []string{"-c", "5", "-s", "56", "-W", "1", "-w", "6", "192.0.2.1"}
if !reflect.DeepEqual(executor.args, wantArgs) || result.Count != 5 {
t.Fatalf("args/result = %#v %+v, want count 5", executor.args, result)
}
if operationResult := result.OperationResult(7, 1024); operationResult["count"] != 5 {
t.Fatalf("operation result = %#v, want count 5", operationResult)
}
}
func TestValidateRequestRejectsInvalidPingCount(t *testing.T) {
for _, count := range []int{-1, 21} {
request := Request{Kind: KindNetworkTest, Target: "192.0.2.1", Count: count, TimeoutSeconds: 5, MaxOutputBytes: 1024}
if err := ValidateRequest(request); err == nil {
t.Fatalf("ValidateRequest() accepted ping count %d", count)
}
}
}
func TestValidateRequestRejectsInvalidTracerouteMaxHops(t *testing.T) {
for _, maxHops := range []int{-1, MaximumTracerouteMaxHops + 1} {
request := Request{Kind: KindTraceroute, Target: "192.0.2.1", MaxHops: maxHops, TimeoutSeconds: 30, MaxOutputBytes: 1024}
if err := ValidateRequest(request); err == nil {
t.Fatalf("ValidateRequest() accepted traceroute max hops %d", maxHops)
}
}
}
func TestProcessRunnerEnforcesRequestTimeout(t *testing.T) {
runner, err := NewProcessRunner(ProcessRunnerOptions{
PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: waitingExecutor{},
})
if err != nil {
t.Fatal(err)
}
started := time.Now()
_, err = runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "192.0.2.1", TimeoutSeconds: 1, MaxOutputBytes: 1024})
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Run() error = %v, want deadline exceeded", err)
}
if elapsed := time.Since(started); elapsed > 2*time.Second {
t.Fatalf("Run() timeout took %s", elapsed)
}
}
func TestValidateRequestRejectsOptionAndShellLikeTargets(t *testing.T) {
for _, target := range []string{"-c", "host;reboot", "$(id)", "host name"} {
request := Request{Kind: KindNetworkTest, Target: target, TimeoutSeconds: 5, MaxOutputBytes: 1024}
if err := ValidateRequest(request); err == nil {
t.Fatalf("ValidateRequest() accepted %q", target)
}
}
}
func TestProcessRunnerTCPConnectionTestConnectsToRequestedPort(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
port := listener.Addr().(*net.TCPAddr).Port
runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute"})
if err != nil {
t.Fatal(err)
}
result, err := runner.Run(context.Background(), Request{
Kind: KindTCPConnectionTest, Target: "127.0.0.1", Port: port, TimeoutSeconds: 2, MaxOutputBytes: 1024,
})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if result.Port != port || result.Reachable == nil || !*result.Reachable || result.ExitCode != 0 || result.OutputTruncated {
t.Fatalf("result = %+v", result)
}
operationResult := result.OperationResult(2, 1024)
if operationResult["adapter"] != "socket" || operationResult["port"] != port {
t.Fatalf("operation result = %#v", operationResult)
}
}
func TestValidateRequestRequiresTCPPort(t *testing.T) {
for _, port := range []int{0, 65536} {
request := Request{Kind: KindTCPConnectionTest, Target: "127.0.0.1", Port: port, TimeoutSeconds: 5, MaxOutputBytes: 1024}
if err := ValidateRequest(request); err == nil {
t.Fatalf("ValidateRequest() accepted TCP port %d", port)
}
}
}
func TestOSExecutorBoundsCombinedOutputAndReturnsExitCode(t *testing.T) {
if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "output" {
fmt.Print(strings.Repeat("x", 4096))
os.Exit(3)
}
t.Setenv("TERA_DIAGNOSTICS_HELPER", "output")
execution, err := (OSExecutor{}).Execute(context.Background(), os.Args[0], []string{"-test.run=TestOSExecutorBoundsCombinedOutputAndReturnsExitCode"}, 1024)
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if execution.ExitCode != 3 || execution.Bytes != 1024 || len(execution.Output) != 1024 || !execution.Truncated {
t.Fatalf("execution = %+v", execution)
}
}
func TestOSExecutorReportsLiveOutput(t *testing.T) {
if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "stream" {
fmt.Print("first reply\n")
time.Sleep(50 * time.Millisecond)
fmt.Print("second reply\n")
return
}
t.Setenv("TERA_DIAGNOSTICS_HELPER", "stream")
var mu sync.Mutex
reports := make([]OutputProgress, 0, 2)
ctx := WithOutputReporter(context.Background(), func(progress OutputProgress) {
mu.Lock()
defer mu.Unlock()
reports = append(reports, progress)
})
execution, err := (OSExecutor{}).Execute(ctx, os.Args[0], []string{"-test.run=TestOSExecutorReportsLiveOutput"}, 1024)
if err != nil {
t.Fatal(err)
}
mu.Lock()
defer mu.Unlock()
if len(reports) == 0 || !strings.Contains(reports[len(reports)-1].Output, "first reply") || !strings.Contains(reports[len(reports)-1].Output, "second reply") {
t.Fatalf("live reports = %#v", reports)
}
if execution.Output != reports[len(reports)-1].Output {
t.Fatalf("final output = %q, last live output = %q", execution.Output, reports[len(reports)-1].Output)
}
}
func TestOSExecutorHonorsContextTimeout(t *testing.T) {
if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "sleep" {
time.Sleep(5 * time.Second)
os.Exit(0)
}
t.Setenv("TERA_DIAGNOSTICS_HELPER", "sleep")
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := (OSExecutor{}).Execute(ctx, os.Args[0], []string{"-test.run=TestOSExecutorHonorsContextTimeout"}, 1024)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Execute() error = %v, want deadline exceeded", err)
}
}