From 6b5e33f2bc057366791458f94fd3de696d16fe5d Mon Sep 17 00:00:00 2001 From: Tiexin Guo Date: Thu, 12 Sep 2024 22:45:32 +0800 Subject: [PATCH] tests: add integration tests for pebble run --- tests/main_test.go | 150 ++++++++++++++++++++++++++++++++++++-- tests/run_test.go | 175 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 313 insertions(+), 12 deletions(-) diff --git a/tests/main_test.go b/tests/main_test.go index cbdf6e24..07b8841d 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -17,10 +17,13 @@ package tests import ( + "errors" "fmt" + "net" "os" "os/exec" "path/filepath" + "strings" "testing" "time" @@ -51,7 +54,16 @@ func createLayer(t *testing.T, pebbleDir string, layerFileName string, layerYAML layerPath := filepath.Join(layersDir, layerFileName) err = os.WriteFile(layerPath, []byte(layerYAML), 0o755) if err != nil { - t.Fatalf("Error creating layers file: %v", err) + t.Fatalf("Cannot create layers file: %v", err) + } +} + +func createIdentitiesFile(t *testing.T, pebbleDir string, identitiesFileName string, identitiesYAML string) { + t.Helper() + + identitiesPath := filepath.Join(pebbleDir, identitiesFileName) + if err := os.WriteFile(identitiesPath, []byte(identitiesYAML), 0o755); err != nil { + t.Fatalf("Cannot create layers file: %v", err) } } @@ -71,9 +83,13 @@ func pebbleRun(t *testing.T, pebbleDir string, args ...string) <-chan servicelog cmd.Wait() }) + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + t.Fatalf("Cannot create stdout pipe: %v", err) + } stderrPipe, err := cmd.StderrPipe() if err != nil { - t.Fatalf("Error creating stderr pipe: %v", err) + t.Fatalf("Cannot create stderr pipe: %v", err) } err = cmd.Start() @@ -81,27 +97,100 @@ func pebbleRun(t *testing.T, pebbleDir string, args ...string) <-chan servicelog t.Fatalf("Error starting 'pebble run': %v", err) } + done := make(chan struct{}) go func() { defer close(logsCh) - parser := servicelog.NewParser(stderrPipe, 4*1024) - for parser.Next() { - if err := parser.Err(); err != nil { - t.Errorf("Cannot parse Pebble logs: %v", err) + + readLogs := func(parser *servicelog.Parser) { + for parser.Next() { + if err := parser.Err(); err != nil { + t.Errorf("Cannot parse Pebble logs: %v", err) + } + select { + case logsCh <- parser.Entry(): + case <-done: + return + } } - logsCh <- parser.Entry() } + + // Both stderr and stdout are needed, because pebble logs to stderr + // while with "--verbose", services otuput to stdout. + stderrParser := servicelog.NewParser(stderrPipe, 4*1024) + stdoutParser := servicelog.NewParser(stdoutPipe, 4*1024) + + // Channel to signal completion and close logsCh + done := make(chan struct{}) + defer close(done) + + go readLogs(stderrParser) + go readLogs(stdoutParser) + + // Wait for both parsers to finish + <-done + <-done }() return logsCh } +func waitForLogs(logsCh <-chan servicelog.Entry, expectedLogs []string, timeout time.Duration) error { + receivedLogs := make(map[string]struct{}) + + timeoutCh := time.After(timeout) + for { + select { + case log, ok := <-logsCh: + if !ok { + return errors.New("channel closed before all expected logs were received") + } + + for _, expectedLog := range expectedLogs { + if _, ok := receivedLogs[expectedLog]; !ok && containsSubstring(log.Message, expectedLog) { + receivedLogs[expectedLog] = struct{}{} + break + } + } + + allLogsReceived := true + for _, log := range expectedLogs { + if _, ok := receivedLogs[log]; !ok { + allLogsReceived = false + break + } + } + + if allLogsReceived { + return nil + } + + case <-timeoutCh: + missingLogs := []string{} + for _, log := range expectedLogs { + if _, ok := receivedLogs[log]; !ok { + missingLogs = append(missingLogs, log) + } + } + return errors.New("timed out waiting for log: " + strings.Join(missingLogs, ", ")) + } + } +} + +func containsSubstring(s, substr string) bool { + return strings.Contains(s, substr) +} + func waitForServices(t *testing.T, pebbleDir string, expectedServices []string, timeout time.Duration) { + t.Helper() + for _, service := range expectedServices { waitForService(t, pebbleDir, service, timeout) } } func waitForService(t *testing.T, pebbleDir string, service string, timeout time.Duration) { + t.Helper() + serviceFilePath := filepath.Join(pebbleDir, service) timeoutCh := time.After(timeout) ticker := time.NewTicker(time.Millisecond) @@ -120,3 +209,50 @@ func waitForService(t *testing.T, pebbleDir string, service string, timeout time } } } + +func isPortUsedByProcess(t *testing.T, port string, processName string) bool { + t.Helper() + + conn, err := net.Listen("tcp", ":"+port) + if err == nil { + conn.Close() + return false + } + if conn != nil { + conn.Close() + } + + cmd := exec.Command("lsof", "-i", ":"+port) + output, err := cmd.Output() + if err != nil { + t.Errorf("Error running lsof command: %v", err) + return false + } + + outputStr := string(output) + if strings.Contains(outputStr, processName) { + return true + } + + return false +} + +func runPebbleCmdAndCheckOutput(t *testing.T, pebbleDir string, expectedOutput []string, args ...string) { + t.Helper() + + cmd := exec.Command("../pebble", args...) + cmd.Env = append(os.Environ(), "PEBBLE="+pebbleDir) + + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("error executing pebble command: %v", err) + } + + outputStr := string(output) + + for _, expected := range expectedOutput { + if !strings.Contains(outputStr, expected) { + t.Errorf("Expected output %s not found in command output", expected) + } + } +} diff --git a/tests/run_test.go b/tests/run_test.go index 300c768d..c5406199 100644 --- a/tests/run_test.go +++ b/tests/run_test.go @@ -18,28 +18,29 @@ package tests import ( "fmt" + "os" "path/filepath" "strings" "testing" "time" ) -func TestPebbleRunNormal(t *testing.T) { +func TestNormal(t *testing.T) { pebbleDir := t.TempDir() layerYAML := ` services: svc1: override: replace - command: {{.svc1Cmd}} + command: /bin/sh -c "{{.svc1Cmd}}" startup: enabled svc2: override: replace - command: {{.svc2Cmd}} + command: /bin/sh -c "{{.svc2Cmd}}" startup: enabled ` - svc1Cmd := fmt.Sprintf("touch %s ; sleep 1000", filepath.Join(pebbleDir, "svc1")) - svc2Cmd := fmt.Sprintf("touch %s ; sleep 1000", filepath.Join(pebbleDir, "svc2")) + svc1Cmd := fmt.Sprintf("touch %s; sleep 1000", filepath.Join(pebbleDir, "svc1")) + svc2Cmd := fmt.Sprintf("touch %s; sleep 1000", filepath.Join(pebbleDir, "svc2")) layerYAML = strings.Replace(layerYAML, "{{.svc1Cmd}}", svc1Cmd, -1) layerYAML = strings.Replace(layerYAML, "{{.svc2Cmd}}", svc2Cmd, -1) @@ -50,3 +51,167 @@ services: expectedServices := []string{"svc1", "svc2"} waitForServices(t, pebbleDir, expectedServices, time.Second*3) } + +func TestCreateDirs(t *testing.T) { + tmpDir := t.TempDir() + pebbleDir := filepath.Join(tmpDir, "PEBBLE_HOME") + + logsCh := pebbleRun(t, pebbleDir, "--create-dirs") + expectedLogs := []string{"Started daemon"} + if err := waitForLogs(logsCh, expectedLogs, time.Second*3); err != nil { + t.Errorf("Error waiting for logs: %v", err) + } + + if _, err := os.Stat(pebbleDir); err != nil { + t.Errorf("pebble run --create-dirs failed: %v", err) + } +} + +func TestHold(t *testing.T) { + pebbleDir := t.TempDir() + + layerYAML := ` +services: + svc1: + override: replace + command: /bin/sh -c "{{.svc1Cmd}}" + startup: enabled +` + svc1Cmd := fmt.Sprintf("touch %s ; sleep 1000", filepath.Join(pebbleDir, "svc1")) + layerYAML = strings.Replace(layerYAML, "{{.svc1Cmd}}", svc1Cmd, -1) + + createLayer(t, pebbleDir, "001-simple-layer.yaml", layerYAML) + + logsCh := pebbleRun(t, pebbleDir, "--hold") + expectedLogs := []string{"Started daemon"} + if err := waitForLogs(logsCh, expectedLogs, time.Second*3); err != nil { + t.Errorf("Error waiting for logs: %v", err) + } + + // Sleep a second before checking services because immediate check + // can't guarantee that svc1 is not started shortly after the log "Started daemon". + time.Sleep(time.Second) + + _, err := os.Stat(filepath.Join(pebbleDir, "svc1")) + if err == nil { + t.Error("pebble run --hold failed, services are still started") + } else { + if !os.IsNotExist(err) { + t.Errorf("Error checking service %s: %v", "svc1", err) + fmt.Printf("Error checking the file: %v\n", err) + } + } +} + +func TestHttpPort(t *testing.T) { + pebbleDir := t.TempDir() + + port := "4000" + logsCh := pebbleRun(t, pebbleDir, "--http=:"+port) + expectedLogs := []string{"Started daemon"} + if err := waitForLogs(logsCh, expectedLogs, time.Second*3); err != nil { + t.Errorf("Error waiting for logs: %v", err) + } + + if !isPortUsedByProcess(t, port, "pebble") { + t.Errorf("Pebble is not listening on port %s", port) + } +} + +func TestVerbose(t *testing.T) { + pebbleDir := t.TempDir() + + layerYAML := ` +services: + svc1: + override: replace + command: /bin/sh -c "{{.svc1Cmd}}" + startup: enabled +` + layersFileName := "001-simple-layer.yaml" + svc1Cmd := fmt.Sprintf("cat %s; sleep 1000", filepath.Join(pebbleDir, "layers", layersFileName)) + layerYAML = strings.Replace(layerYAML, "{{.svc1Cmd}}", svc1Cmd, -1) + + createLayer(t, pebbleDir, layersFileName, layerYAML) + + logsCh := pebbleRun(t, pebbleDir, "--verbose") + expectedLogs := []string{ + "Started daemon", + "services:", + "svc1:", + "override: replace", + "startup: enabled", + "command: /bin/sh -c", + } + if err := waitForLogs(logsCh, expectedLogs, time.Second*3); err != nil { + t.Errorf("Error waiting for logs: %v", err) + } +} + +func TestArgs(t *testing.T) { + pebbleDir := t.TempDir() + + layerYAML := ` +services: + svc1: + override: replace + command: /bin/sh + startup: enabled +` + layersFileName := "001-simple-layer.yaml" + svc1Cmd := fmt.Sprintf("cat %s; sleep 1000", filepath.Join(pebbleDir, "layers", layersFileName)) + layerYAML = strings.Replace(layerYAML, "{{.svc1Cmd}}", svc1Cmd, -1) + + createLayer(t, pebbleDir, layersFileName, layerYAML) + + logsCh := pebbleRun(t, pebbleDir, "--verbose", + "--args", + "svc1", + "-c", + fmt.Sprintf("cat %s; sleep 1000", filepath.Join(pebbleDir, "layers", layersFileName)), + ) + expectedLogs := []string{ + "Started daemon", + "services:", + "svc1:", + "override: replace", + "startup: enabled", + "command: /bin/sh", + } + if err := waitForLogs(logsCh, expectedLogs, time.Second*3); err != nil { + t.Errorf("Error waiting for logs: %v", err) + } +} + +func TestIdentities(t *testing.T) { + pebbleDir := t.TempDir() + + identitiesYAML := ` +identities: + bob: + access: admin + local: + user-id: 42 + alice: + access: read + local: + user-id: 2000 +` + identitiesFileName := "idents-add.yaml" + createIdentitiesFile(t, pebbleDir, identitiesFileName, identitiesYAML) + + logsCh := pebbleRun(t, pebbleDir, "--identities="+filepath.Join(pebbleDir, identitiesFileName)) + expectedLogs := []string{ + "Started daemon", + "POST /v1/services", + } + if err := waitForLogs(logsCh, expectedLogs, time.Second*3); err != nil { + t.Errorf("Error waiting for logs: %v", err) + } + + expectedOutput := []string{"access: admin", "local:", "user-id: 42"} + runPebbleCmdAndCheckOutput(t, pebbleDir, expectedOutput, "identity", "bob") + + expectedOutput = []string{"access: read", "local:", "user-id: 2000"} + runPebbleCmdAndCheckOutput(t, pebbleDir, expectedOutput, "identity", "alice") +}