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 11 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
23 changes: 23 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Integration Tests

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
test:
runs-on: ubuntu-latest
name: tests

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22

- name: Run tests
run: go test -count=1 -tags=integration ./tests/
7 changes: 7 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,13 @@ ok github.com/canonical/pebble/cmd/pebble 0.165s
...
```

Pebble also has a suite of integration tests for testing things like `pebble run`. To run them, use the "integration" build constraint:

```
$ go test -count=1 -tags=integration ./tests/
ok github.com/canonical/pebble/tests 4.774s
```

## Docs

We use [`sphinx`](https://www.sphinx-doc.org/en/master/) to build the docs with styles preconfigured by the [Canonical Documentation Starter Pack](https://github.com/canonical/sphinx-docs-starter-pack).
Expand Down
25 changes: 25 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Pebble Integration Tests

IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
This directory holds a suite of integration tests for end-to-end tests of things like pebble run. They use the standard go test runner, but are only executed if you set the integration build constraint.

## Run Tests

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

## Developing

### Visual Studio Code Settings

For the VSCode Go and gopls extention to work properly with files containing build tags, add the following:

```json
{
"gopls": {
"build.buildFlags": [
"-tags=integration"
]
}
}
```
171 changes: 171 additions & 0 deletions tests/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//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 (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"github.com/canonical/pebble/internals/servicelog"
)

// TestMain builds the pebble binary before running the integration tests.
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("Cannot build pebble binary:", err)
os.Exit(1)
}

exitCode := m.Run()
os.Exit(exitCode)
}

func createLayer(t *testing.T, pebbleDir, layerFileName, layerYAML string) {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
t.Helper()

layersDir := filepath.Join(pebbleDir, "layers")
err := os.MkdirAll(layersDir, 0o755)
if err != nil {
t.Fatalf("Cannot create layers directory: %v", err)
}

layerPath := filepath.Join(layersDir, layerFileName)
err = os.WriteFile(layerPath, []byte(layerYAML), 0o755)
if err != nil {
t.Fatalf("Cannot create layers file: %v", err)
}
}

func pebbleRun(t *testing.T, pebbleDir string, args ...string) (stdoutCh chan servicelog.Entry, stderrCh chan servicelog.Entry) {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
t.Helper()

stdoutCh = make(chan servicelog.Entry)
stderrCh = make(chan servicelog.Entry)

cmd := exec.Command("../pebble", append([]string{"run"}, args...)...)
cmd.Env = append(os.Environ(), "PEBBLE="+pebbleDir)

stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
t.Fatalf("Cannot create stdout pipe: %v", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
t.Fatalf("Cannot create stderr pipe: %v", err)
}

err = cmd.Start()
if err != nil {
t.Fatalf("Error starting 'pebble run': %v", err)
}

stopStdout := make(chan struct{}, 1)
stopStderr := make(chan struct{}, 1)

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()
stopStdout <- struct{}{}
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
stopStderr <- struct{}{}
})

readLogs := func(parser *servicelog.Parser, ch chan servicelog.Entry, stop <-chan struct{}) {
for parser.Next() {
if err := parser.Err(); err != nil {
t.Errorf("Cannot parse Pebble logs: %v", err)
}
select {
case ch <- parser.Entry():
case <-stop:
return
}
}
}

// Both stderr and stdout are needed, because pebble logs to stderr
// while with "--verbose", services output to stdout.
stderrParser := servicelog.NewParser(stderrPipe, 4*1024)
stdoutParser := servicelog.NewParser(stdoutPipe, 4*1024)

go readLogs(stdoutParser, stdoutCh, stopStdout)
go readLogs(stderrParser, stderrCh, stopStderr)

return stdoutCh, stderrCh
}

func waitForLog(t *testing.T, logsCh <-chan servicelog.Entry, expectedService, expectedLog string, timeout time.Duration) {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
t.Helper()

timeoutCh := time.After(timeout)
for {
select {
case log, ok := <-logsCh:
if !ok {
t.Error("channel closed before all expected logs were received")
}

if log.Service == expectedService && strings.Contains(log.Message, expectedLog) {
return
}

case <-timeoutCh:
t.Fatalf("timed out after %v waiting for log %s", 3*time.Second, expectedLog)
}
}
}

func waitForFile(t *testing.T, file string, timeout time.Duration) {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
t.Helper()

timeoutCh := time.After(timeout)
ticker := time.NewTicker(time.Millisecond)
for {
select {
case <-timeoutCh:
t.Fatalf("timeout waiting for file %s", file)

case <-ticker.C:
stat, err := os.Stat(file)
if err == nil && stat.Mode().IsRegular() {
return
}
}
}
}

func runPebbleCommand(t *testing.T, pebbleDir string, args ...string) string {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
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)
}

return string(output)
}
Loading
Loading