diff --git a/internals/testintegration/pebble_another_test.go b/internals/testintegration/pebble_another_test.go deleted file mode 100644 index b4aee4f4..00000000 --- a/internals/testintegration/pebble_another_test.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build integration - -package testintegration_test - -import ( - "testing" - - . "github.com/canonical/pebble/internals/testintegration" -) - -func TestPebbleSomethingElse(t *testing.T) { - pebbleDir := t.TempDir() - CreateLayer(t, pebbleDir, "001-simple-layer.yaml", DefaultLayerYAML) - _ = PebbleRun(t, pebbleDir) - // do something -} diff --git a/internals/testintegration/pebble_run_test.go b/internals/testintegration/pebble_run_test.go deleted file mode 100644 index 168b55cc..00000000 --- a/internals/testintegration/pebble_run_test.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build integration - -package testintegration_test - -import ( - "fmt" - "os" - "testing" - - . "github.com/canonical/pebble/internals/testintegration" -) - -func TestMain(m *testing.M) { - if err := Setup(); err != nil { - fmt.Println("Setup failed with error:", err) - os.Exit(1) - } - - exitVal := m.Run() - os.Exit(exitVal) -} - -func TestPebbleRunWithSimpleLayer(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) - - logs := PebbleRun(t, pebbleDir) - - expected := []string{ - "Started daemon", - "Service \"demo-service\" starting", - "Service \"demo-service2\" starting", - "Started default services with change", - } - - if foundAll, notFound := AllKeywordsFoundInLogs(logs, expected); !foundAll { - t.Errorf("Expected keywords not found in logs: %v", notFound) - } -} diff --git a/internals/testintegration/utils.go b/internals/testintegration/utils.go deleted file mode 100644 index e40cbbf6..00000000 --- a/internals/testintegration/utils.go +++ /dev/null @@ -1,105 +0,0 @@ -//go:build integration - -package testintegration - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" -) - -var DefaultLayerYAML string = ` -services: - demo-service: - override: replace - command: sleep 1000 - startup: enabled -`[1:] - -func Setup() error { - cmd := exec.Command("go", "build", "./cmd/pebble") - cmd.Dir = getRootDir() - return cmd.Run() -} - -func getRootDir() string { - wd, _ := os.Getwd() - return filepath.Join(wd, "../../") -} - -func AllKeywordsFoundInLogs(logs []string, keywords []string) (bool, []string) { - var notFound []string - - for _, keyword := range keywords { - keywordFound := false - for _, log := range logs { - if strings.Contains(log, keyword) { - keywordFound = true - break - } - } - if !keywordFound { - notFound = append(notFound, keyword) - } - } - - return len(notFound) == 0, notFound -} - -func CreateLayer(t *testing.T, pebbleDir string, layerFileName string, layerYAML string) { - 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) - } -} - -func PebbleRun(t *testing.T, pebbleDir string) []string { - cmd := exec.Command("./pebble", "run") - cmd.Dir = getRootDir() - cmd.Env = append(os.Environ(), fmt.Sprintf("PEBBLE=%s", pebbleDir)) - - stderrPipe, err := cmd.StderrPipe() - if err != nil { - t.Fatalf("Error creating stderr pipe: %v", err) - } - - err = cmd.Start() - defer cmd.Process.Kill() - if err != nil { - t.Fatalf("Error starting 'pebble run': %v", err) - } - - var logs []string - - lastOutputTime := time.Now() - - go func() { - scanner := bufio.NewScanner(stderrPipe) - for scanner.Scan() { - lastOutputTime = time.Now() - line := scanner.Text() - logs = append(logs, line) - } - }() - - for { - time.Sleep(100 * time.Millisecond) - if time.Since(lastOutputTime) > 1*time.Second { - break - } - } - - return logs -} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..623fc890 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,31 @@ +# Pebble Integration Tests + +## Run Tests + +```bash +go test -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: + +```json +{ + "gopls": { + "build.buildFlags": [ + "-tags=integration" + ] + } +} +``` diff --git a/tests/main_test.go b/tests/main_test.go new file mode 100644 index 00000000..387e872a --- /dev/null +++ b/tests/main_test.go @@ -0,0 +1,67 @@ +//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_test + +import ( + "fmt" + "os" + "os/exec" + "testing" + "time" + + . "github.com/canonical/pebble/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) + os.Exit(1) + } + + exitVal := m.Run() + os.Exit(exitVal) +} + +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", + } + if err := WaitForLogs(logsCh, expected, time.Second*3); err != nil { + t.Errorf("Error waiting for logs: %v", err) + } +} diff --git a/tests/utils.go b/tests/utils.go new file mode 100644 index 00000000..803d2f8a --- /dev/null +++ b/tests/utils.go @@ -0,0 +1,132 @@ +//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" +) + +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) + } +} + +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 +} + +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) +}