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)
-}