diff --git a/tests/README.md b/tests/README.md index 623fc890..d7526c18 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,19 +3,11 @@ ## Run Tests ```bash -go test -tags=integration ./tests/ +go test -count=1 -tags=integration ./tests/ ``` ## Developing -### Clean Test Cache - -If you are adding tests and debugging, remember to clean test cache: - -```bash -go clean -testcache && go test -v -tags=integration ./tests/ -``` - ### Visual Studio Code Settings For the VSCode Go extention to work properly with files with build tags, add the following: diff --git a/tests/main_test.go b/tests/main_test.go index dd704c07..cbdf6e24 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -14,55 +14,109 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package tests_test +package tests import ( "fmt" "os" "os/exec" + "path/filepath" "testing" "time" - . "github.com/canonical/pebble/tests" + "github.com/canonical/pebble/internals/servicelog" ) -// TestMain does extra setup before executing tests. +// TestMain builds the pebble binary before running the integration tests. func TestMain(m *testing.M) { goBuild := exec.Command("go", "build", "-o", "../pebble", "../cmd/pebble") if err := goBuild.Run(); err != nil { - fmt.Println("Setup failed with error:", err) + fmt.Println("Cannot build pebble binary:", err) os.Exit(1) } - exitVal := m.Run() - os.Exit(exitVal) + exitCode := m.Run() + os.Exit(exitCode) } -func TestPebbleRunNormal(t *testing.T) { - pebbleDir := t.TempDir() - - layerYAML := ` -services: - demo-service: - override: replace - command: sleep 1000 - startup: enabled - demo-service2: - override: replace - command: sleep 1000 - startup: enabled -`[1:] - - CreateLayer(t, pebbleDir, "001-simple-layer.yaml", layerYAML) - - logsCh := PebbleRun(t, pebbleDir) - expected := []string{ - "Started daemon", - "Service \"demo-service\" starting", - "Service \"demo-service2\" starting", - "Started default services with change", +func createLayer(t *testing.T, pebbleDir string, layerFileName string, layerYAML string) { + t.Helper() + + layersDir := filepath.Join(pebbleDir, "layers") + err := os.MkdirAll(layersDir, 0o755) + if err != nil { + t.Fatalf("Cannot create layers directory: pipe: %v", err) + } + + layerPath := filepath.Join(layersDir, layerFileName) + err = os.WriteFile(layerPath, []byte(layerYAML), 0o755) + if err != nil { + t.Fatalf("Error creating layers file: %v", err) + } +} + +func pebbleRun(t *testing.T, pebbleDir string, args ...string) <-chan servicelog.Entry { + t.Helper() + + logsCh := make(chan servicelog.Entry) + + cmd := exec.Command("../pebble", append([]string{"run"}, args...)...) + cmd.Env = append(os.Environ(), "PEBBLE="+pebbleDir) + + t.Cleanup(func() { + err := cmd.Process.Signal(os.Interrupt) + if err != nil { + t.Errorf("Error sending SIGINT/Ctrl+C to pebble: %v", err) + } + cmd.Wait() + }) + + stderrPipe, err := cmd.StderrPipe() + if err != nil { + t.Fatalf("Error creating stderr pipe: %v", err) } - if err := WaitForLogs(logsCh, expected, time.Second*3); err != nil { - t.Errorf("Error waiting for logs: %v", err) + + err = cmd.Start() + if err != nil { + t.Fatalf("Error starting 'pebble run': %v", err) + } + + 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) + } + logsCh <- parser.Entry() + } + }() + + return logsCh +} + +func waitForServices(t *testing.T, pebbleDir string, expectedServices []string, timeout time.Duration) { + for _, service := range expectedServices { + waitForService(t, pebbleDir, service, timeout) + } +} + +func waitForService(t *testing.T, pebbleDir string, service string, timeout time.Duration) { + serviceFilePath := filepath.Join(pebbleDir, service) + timeoutCh := time.After(timeout) + ticker := time.NewTicker(time.Millisecond) + for { + select { + case <-timeoutCh: + t.Errorf("timeout waiting for service %s", service) + return + + case <-ticker.C: + stat, err := os.Stat(serviceFilePath) + if err == nil && stat.Mode().IsRegular() { + os.Remove(serviceFilePath) + return + } + } } } diff --git a/tests/run_test.go b/tests/run_test.go new file mode 100644 index 00000000..300c768d --- /dev/null +++ b/tests/run_test.go @@ -0,0 +1,52 @@ +//go:build integration + +// Copyright (c) 2024 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package tests + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestPebbleRunNormal(t *testing.T) { + pebbleDir := t.TempDir() + + layerYAML := ` +services: + svc1: + override: replace + command: {{.svc1Cmd}} + startup: enabled + svc2: + override: replace + command: {{.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")) + layerYAML = strings.Replace(layerYAML, "{{.svc1Cmd}}", svc1Cmd, -1) + layerYAML = strings.Replace(layerYAML, "{{.svc2Cmd}}", svc2Cmd, -1) + + createLayer(t, pebbleDir, "001-simple-layer.yaml", layerYAML) + + _ = pebbleRun(t, pebbleDir) + + expectedServices := []string{"svc1", "svc2"} + waitForServices(t, pebbleDir, expectedServices, time.Second*3) +} diff --git a/tests/utils.go b/tests/utils.go deleted file mode 100644 index b3ea5555..00000000 --- a/tests/utils.go +++ /dev/null @@ -1,135 +0,0 @@ -//go:build integration - -// Copyright (c) 2024 Canonical Ltd -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 3 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package tests - -import ( - "bufio" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" -) - -// CreateLayer creates a layer file in the Pebble dir using the name and content given. -func CreateLayer(t *testing.T, pebbleDir string, layerFileName string, layerYAML string) { - t.Helper() - - layersDir := filepath.Join(pebbleDir, "layers") - err := os.MkdirAll(layersDir, 0755) - if err != nil { - t.Fatalf("Error creating layers directory: pipe: %v", err) - } - - layerPath := filepath.Join(layersDir, layerFileName) - err = os.WriteFile(layerPath, []byte(layerYAML), 0755) - if err != nil { - t.Fatalf("Error creating layers file: %v", err) - } -} - -// PebbleRun runs the Pebble daemon and returns a channel for logs. -func PebbleRun(t *testing.T, pebbleDir string) <-chan string { - t.Helper() - - logsCh := make(chan string) - - cmd := exec.Command("../pebble", "run") - cmd.Env = append(os.Environ(), fmt.Sprintf("PEBBLE=%s", pebbleDir)) - - t.Cleanup(func() { - err := cmd.Process.Signal(os.Interrupt) - if err != nil { - t.Errorf("Error sending SIGINT/Ctrl+C to pebble: %v", err) - } - }) - - stderrPipe, err := cmd.StderrPipe() - if err != nil { - t.Fatalf("Error creating stderr pipe: %v", err) - } - - err = cmd.Start() - if err != nil { - t.Fatalf("Error starting 'pebble run': %v", err) - } - - go func() { - defer close(logsCh) - - scanner := bufio.NewScanner(stderrPipe) - for scanner.Scan() { - line := scanner.Text() - logsCh <- line - } - }() - - return logsCh -} - -// WaitForLogs reads from the channel (returned by PebbleRun) and checks if all expected logs are found within specified timeout duration. -func WaitForLogs(logsCh <-chan string, expectedLogs []string, timeout time.Duration) error { - receivedLogs := make(map[string]struct{}) - start := time.Now() - - 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, expectedLog) { - receivedLogs[expectedLog] = struct{}{} - break - } - } - - allLogsReceived := true - for _, log := range expectedLogs { - if _, ok := receivedLogs[log]; !ok { - allLogsReceived = false - break - } - } - - if allLogsReceived { - return nil - } - - default: - if time.Since(start) > timeout { - 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, ", ")) - } - time.Sleep(100 * time.Millisecond) - } - } -} - -func containsSubstring(s, substr string) bool { - return strings.Contains(s, substr) -}