Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add integration tests for "pebble run" #497

Merged
merged 15 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Pebble Integration Tests

IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
## Run Tests

```bash
go test -tags=integration ./tests/
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
```

## 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"
]
}
}
```
68 changes: 68 additions & 0 deletions tests/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//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 <http://www.gnu.org/licenses/>.

package tests_test

import (
"fmt"
"os"
"os/exec"
"testing"
"time"

. "github.com/canonical/pebble/tests"
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
)

// TestMain does extra setup before executing tests.
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
func TestMain(m *testing.M) {
goBuild := exec.Command("go", "build", "-o", "../pebble", "../cmd/pebble")
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
if err := goBuild.Run(); err != nil {
fmt.Println("Setup failed with error:", err)
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
os.Exit(1)
}

exitVal := m.Run()
os.Exit(exitVal)
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
}

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:]
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved

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)
}
}
135 changes: 135 additions & 0 deletions tests/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//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 <http://www.gnu.org/licenses/>.

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) {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
t.Helper()

layersDir := filepath.Join(pebbleDir, "layers")
err := os.MkdirAll(layersDir, 0755)
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
t.Fatalf("Error creating layers directory: pipe: %v", err)
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
}

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))
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved

t.Cleanup(func() {
err := cmd.Process.Signal(os.Interrupt)
if err != nil {
t.Errorf("Error sending SIGINT/Ctrl+C to pebble: %v", err)
}
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
})

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)
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
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 {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
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:
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading