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 9 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
29 changes: 29 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Integration Test
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved

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

jobs:
test:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
go: ['1.22']

name: Go ${{ matrix.go }}
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}

- name: Integration test
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
run: go test -count=1 -tags=integration ./tests/
23 changes: 23 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Pebble Integration Tests

IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
## 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"
]
}
}
```
177 changes: 177 additions & 0 deletions tests/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//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) (<-chan servicelog.Entry, <-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)
}

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

done := make(chan struct{})
go func() {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
defer close(stdoutCh)
defer close(stderrCh)

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

// Both stderr and stdout are needed, because pebble logs to stderr
// while with "--verbose", services otuput to stdout.
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
stderrParser := servicelog.NewParser(stderrPipe, 4*1024)
stdoutParser := servicelog.NewParser(stdoutPipe, 4*1024)

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

// Wait for both parsers to finish
<-done
<-done
}()

return stdoutCh, stderrCh
}

func waitForLog(t *testing.T, logsCh <-chan servicelog.Entry, expectedLog string, timeout time.Duration) {
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 strings.Contains(log.Message, expectedLog) {
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
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() {
os.Remove(file)
IronCore864 marked this conversation as resolved.
Show resolved Hide resolved
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