Skip to content

Commit

Permalink
test: Add test fixtures and base e2e test
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Sul <[email protected]>
  • Loading branch information
mike-sul committed Sep 18, 2024
1 parent d6df74b commit 1cfb1b4
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 13 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ check: format

tidy-mod:
go mod tidy -go=$(MODVER)

# target should be run only in the dev container
test-e2e: $(exe)
go test -v ./...
25 changes: 15 additions & 10 deletions cmd/composectl/cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,24 @@ type (
CheckInstall bool
}

checkAppResult struct {
CheckAppResult struct {
MissingBlobs map[digest.Digest]compose.BlobInfo `json:"missing_blobs"`
TotalPullSize int64 `json:"total_pull_size"`
TotalStoreSize int64 `json:"total_store_size"`
TotalRuntimeSize int64 `json:"total_runtime_size"`
}

CheckAndInstallResult struct {
FetchCheck *CheckAppResult `json:"fetch_check"`
InstallCheck *InstallCheckResult `json:"install_check"`
}

appInstallCheckResult struct {
AppName string `json:"app_name"`
MissingImages []string `json:"missing_images"`
}

installCheckResult map[string]*appInstallCheckResult
InstallCheckResult map[string]*appInstallCheckResult
)

const (
Expand Down Expand Up @@ -90,7 +95,7 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) {
quietCheck = true
}
cr, ui, _ := checkApps(cmd.Context(), args, *opts.UsageWatermark, *opts.SrcStorePath, quietCheck)
var ir installCheckResult
var ir InstallCheckResult
var err error
if opts.CheckInstall {
ir, err = checkIfInstalled(cmd.Context(), args, *opts.SrcStorePath, config.DockerHost)
Expand All @@ -99,8 +104,8 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) {
if opts.Format == "json" {
aggregatedCheckRes :=
struct {
FetchCheck *checkAppResult `json:"fetch_check"`
InstallCheck *installCheckResult `json:"install_check"`
FetchCheck *CheckAppResult `json:"fetch_check"`
InstallCheck *InstallCheckResult `json:"install_check"`
}{
FetchCheck: cr,
InstallCheck: &ir,
Expand Down Expand Up @@ -128,7 +133,7 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) {
}
}

func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcStorePath string, quiet bool) (*checkAppResult, *compose.UsageInfo, []compose.App) {
func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcStorePath string, quiet bool) (*CheckAppResult, *compose.UsageInfo, []compose.App) {
if usageWatermark < MinUsageWatermark {
DieNotNil(fmt.Errorf("the specified usage watermark is lower than the minimum allowed; %d < %d", usageWatermark, MinUsageWatermark))
}
Expand All @@ -151,7 +156,7 @@ func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcSt

var apps []compose.App
blobsToPull := map[digest.Digest]compose.BlobInfo{}
checkRes := checkAppResult{MissingBlobs: blobsToPull}
checkRes := CheckAppResult{MissingBlobs: blobsToPull}

for _, appRef := range appRefs {
if !quiet {
Expand Down Expand Up @@ -229,12 +234,12 @@ func checkApps(ctx context.Context, appRefs []string, usageWatermark uint, srcSt
return &checkRes, ui, apps
}

func (cr *checkAppResult) print() {
func (cr *CheckAppResult) print() {
fmt.Printf("%d blobs to pull; total download size: %s, total store size: %s, total runtime size of missing blobs: %s, total required: %s\n",
len(cr.MissingBlobs), units.BytesSize(float64(cr.TotalPullSize)), units.BytesSize(float64(cr.TotalStoreSize)), units.BytesSize(float64(cr.TotalRuntimeSize)), units.BytesSize(float64(cr.TotalStoreSize+cr.TotalRuntimeSize)))
}

func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string, dockerHost string) (installCheckResult, error) {
func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string, dockerHost string) (InstallCheckResult, error) {
cli, err := compose.GetDockerClient(dockerHost)
if err != nil {
return nil, err
Expand All @@ -254,7 +259,7 @@ func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string
}
}

checkResult := installCheckResult{}
checkResult := InstallCheckResult{}
blobProvider := compose.NewStoreBlobProvider(path.Join(srcStorePath, "blobs", "sha256"))
for _, appRef := range appRefs {
app, _, err := v1.NewAppLoader().LoadAppTree(ctx, blobProvider, platforms.OnlyStrict(config.Platform), appRef)
Expand Down
6 changes: 3 additions & 3 deletions cmd/composectl/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type (
listOptions struct {
Format string
}
appJsonOutput struct {
AppJsonOutput struct {
Name string `json:"name"`
URI string `json:"uri"`
}
Expand All @@ -42,9 +42,9 @@ func listApps(cmd *cobra.Command, args []string, opts *listOptions) {
apps, err := cs.ListApps(cmd.Context())
DieNotNil(err)
if opts.Format == "json" {
var lsOutput []appJsonOutput
var lsOutput []AppJsonOutput
for _, app := range apps {
lsOutput = append(lsOutput, appJsonOutput{
lsOutput = append(lsOutput, AppJsonOutput{
Name: app.Name,
URI: app.String(),
})
Expand Down
215 changes: 215 additions & 0 deletions test/fixtures/composectl_cmds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package fixtures

import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
composectl "github.com/foundriesio/composeapp/cmd/composectl/cmd"
rand2 "math/rand"
"os"
"os/exec"
"path"
"testing"
"time"
)

var (
composeExec = os.Getenv("COMPOSECTL_EXE")
)

type (
App struct {
Name string
BaseUri string
PublishedUri string
Dir string
}
)

func NewApp(t *testing.T, composeDef string, appName ...string) *App {
var name string
if len(appName) > 0 {
name = appName[0]
} else {
name = randomString(5)
}
appDir := path.Join(t.TempDir(), name)
err := os.MkdirAll(appDir, 0o755)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(path.Join(appDir, "docker-compose.yml"), []byte(composeDef), 0o640)
if err != nil {
t.Fatal(err)
}
return &App{
Name: name,
BaseUri: "registry:5000/factory/" + name,
Dir: appDir,
}
}

func (a *App) Publish(t *testing.T) {
t.Run("publish app", func(t *testing.T) {
digestFile := path.Join(a.Dir, "digest.sha256")
tag, err := randomStringCrypto(7)
if err != nil {
t.Fatalf("failed to generate a random image tag value: %s\n", err)
}
runCmd(t, a.Dir, "publish", "-d", digestFile, a.BaseUri+":"+tag, "amd64")
if b, err := os.ReadFile(digestFile); err == nil {
a.PublishedUri = a.BaseUri + "@" + string(b)
} else {
t.Fatalf("failed to read the published app digest: %s\n", err)
}
fmt.Printf("published app uri: %s\n", a.PublishedUri)
})
}

func (a *App) Pull(t *testing.T) {
a.runCmd(t, "pull app", "pull", a.PublishedUri, "-u", "90")
}

func (a *App) Remove(t *testing.T) {
a.runCmd(t, "remove app", "rm", a.PublishedUri)
}

func (a *App) Install(t *testing.T) {
a.runCmd(t, "install app", "install", a.PublishedUri)
}

func (a *App) Uninstall(t *testing.T) {
a.runCmd(t, "uninstall app", "uninstall", a.Name)
}

func (a *App) Run(t *testing.T) {
a.runCmd(t, "run app", "run", a.Name)
}

func (a *App) Up(t *testing.T) {
t.Run("compose up", func(t *testing.T) {
homeDir, homeDirErr := os.UserHomeDir()
if homeDirErr != nil {
t.Errorf("failed to get home directory path: %s\n", homeDirErr)
}
composeRoot := path.Join(homeDir, ".composeapps/projects", a.Name)

c := exec.Command("docker", "compose", "up", "--remove-orphans", "-d")
c.Dir = composeRoot
output, err := c.CombinedOutput()
if err != nil {
t.Errorf("failed to run `docker compose up -d` command: %s\n", output)
}
})
}

func (a *App) Stop(t *testing.T) {
a.runCmd(t, "stop app", "stop", a.Name)
}

func (a *App) CheckFetched(t *testing.T) {
t.Run("list app", func(t *testing.T) {
output := runCmd(t, a.Dir, "ls", "--format", "json")
var lsOutput []composectl.AppJsonOutput
if err := json.Unmarshal(output, &lsOutput); err != nil {
t.Errorf("failed to unmarshal app list output: %s\n", err)
}
if a.PublishedUri != lsOutput[0].URI {
t.Errorf("app uri in the list output does not equal to the published app;"+
" published app uri: %s, app list uri: %s\n", a.PublishedUri, lsOutput[0].URI)
}
})
t.Run("check app", func(t *testing.T) {
output := runCmd(t, a.Dir, "check", "--local", a.PublishedUri, "--format", "json")
checkResult := composectl.CheckAndInstallResult{}
if err := json.Unmarshal(output, &checkResult); err != nil {
t.Errorf("failed to unmarshal check app result: %s\n", err)
}
if len(checkResult.FetchCheck.MissingBlobs) > 0 {
t.Errorf("There are missing app blobs: %+v\n", checkResult.FetchCheck.MissingBlobs)
}
})
}

func (a *App) CheckInstalled(t *testing.T) {
t.Run("check if installed", func(t *testing.T) {
output := runCmd(t, a.Dir, "check", "--local", "--install", a.PublishedUri, "--format", "json")
checkResult := composectl.CheckAndInstallResult{}
if err := json.Unmarshal(output, &checkResult); err != nil {
t.Errorf("failed to unmarshal check app result: %s\n", err)
}

if len(checkResult.FetchCheck.MissingBlobs) > 0 {
t.Errorf("there are missing app blobs: %+v\n", checkResult.FetchCheck.MissingBlobs)
}
if checkResult.InstallCheck == nil || len(*checkResult.InstallCheck) != 1 {
t.Errorf("invalid install check result: %+v\n", checkResult.InstallCheck)
}
installCheckRes, ok := (*checkResult.InstallCheck)[a.PublishedUri]
if !ok {
t.Errorf("no app in the install check result: %+v\n", *checkResult.InstallCheck)
}
if len(installCheckRes.MissingImages) > 0 {
t.Errorf("there are missing app images in docker store: %+v\n", installCheckRes.MissingImages)
}
})
}

func (a *App) CheckRunning(t *testing.T) {
t.Run("check if running", func(t *testing.T) {
output := runCmd(t, "", "ps", a.PublishedUri, "--format", "json")
var psOutput map[string]composectl.App
if err := json.Unmarshal(output, &psOutput); err != nil {
t.Errorf("failed to unmarshal app ps output: %s\n", err)
}
if len(psOutput) != 1 {
t.Errorf("expected one element in ps output, got: %d\n", len(psOutput))
}
appStatus, ok := psOutput[a.PublishedUri]
if !ok {
t.Errorf("no app URI in the ps output: %+v\n", psOutput)
}
if appStatus.State != "running" {
t.Errorf("app is not running, its state: %+s\n", appStatus.State)
}
})
}

func (a *App) runCmd(t *testing.T, desc string, args ...string) {
t.Run(desc, func(t *testing.T) {
runCmd(t, a.Dir, args...)
})
}

func runCmd(t *testing.T, appDir string, args ...string) []byte {
c := exec.Command(composeExec, args...)
if len(appDir) > 0 {
c.Dir = appDir
}
output, err := c.CombinedOutput()
if err != nil {
t.Fatalf("failed to run `%s` command: %s\n", args[0], output)
}
return output
}

func randomStringCrypto(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}

return base64.URLEncoding.EncodeToString(bytes)[:length], nil
}

func randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyz"
seededRand := rand2.New(rand2.NewSource(time.Now().UnixNano()))
b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}
29 changes: 29 additions & 0 deletions test/integration/smoke_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package e2e_tests

import (
"github.com/foundriesio/composeapp/test/fixtures"
"testing"
)

func TestSmoke(t *testing.T) {
appComposeDef := `
services:
busybox:
image: ghcr.io/foundriesio/busybox:1.36
command: sh -c "while true; do sleep 60; done"
`
app := fixtures.NewApp(t, appComposeDef)
app.Publish(t)

app.Pull(t)
defer app.Remove(t)
app.CheckFetched(t)

app.Install(t)
defer app.Uninstall(t)
app.CheckInstalled(t)

app.Run(t)
defer app.Stop(t)
app.CheckRunning(t)
}

0 comments on commit 1cfb1b4

Please sign in to comment.