Skip to content

Commit 3dc8734

Browse files
authored
watch: add end-to-end test (docker#10801)
Add an end-to-end test that covers the core watch functionality, i.e. CRUD on files & directories. Signed-off-by: Milas Bowman <[email protected]>
1 parent 852e192 commit 3dc8734

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

pkg/e2e/fixtures/watch/compose.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
x-dev: &x-dev
2+
watch:
3+
- action: sync
4+
path: ./data
5+
target: /app/data
6+
ignore:
7+
- '*.foo'
8+
- ./ignored
9+
10+
services:
11+
alpine:
12+
build:
13+
dockerfile_inline: |-
14+
FROM alpine
15+
RUN mkdir -p /app/data
16+
init: true
17+
command: sleep infinity
18+
x-develop: *x-dev
19+
busybox:
20+
build:
21+
dockerfile_inline: |-
22+
FROM busybox
23+
RUN mkdir -p /app/data
24+
init: true
25+
command: sleep infinity
26+
x-develop: *x-dev
27+
debian:
28+
build:
29+
dockerfile_inline: |-
30+
FROM debian
31+
RUN mkdir -p /app/data
32+
init: true
33+
command: sleep infinity
34+
x-develop: *x-dev

pkg/e2e/fixtures/watch/data/hello.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world

pkg/e2e/watch_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
Copyright 2023 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2e
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
"sync/atomic"
25+
"testing"
26+
27+
"github.com/distribution/distribution/v3/uuid"
28+
"github.com/stretchr/testify/require"
29+
"gotest.tools/v3/assert"
30+
"gotest.tools/v3/assert/cmp"
31+
"gotest.tools/v3/icmd"
32+
"gotest.tools/v3/poll"
33+
)
34+
35+
func TestWatch(t *testing.T) {
36+
services := []string{"alpine", "busybox", "debian"}
37+
for _, svcName := range services {
38+
t.Run(svcName, func(t *testing.T) {
39+
t.Helper()
40+
doTest(t, svcName)
41+
})
42+
}
43+
}
44+
45+
// NOTE: these tests all share a single Compose file but are safe to run concurrently
46+
func doTest(t *testing.T, svcName string) {
47+
tmpdir := t.TempDir()
48+
dataDir := filepath.Join(tmpdir, "data")
49+
writeDataFile := func(name string, contents string) {
50+
t.Helper()
51+
dest := filepath.Join(dataDir, name)
52+
require.NoError(t, os.MkdirAll(filepath.Dir(dest), 0o700))
53+
t.Logf("writing %q to %q", contents, dest)
54+
require.NoError(t, os.WriteFile(dest, []byte(contents+"\n"), 0o600))
55+
}
56+
57+
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
58+
CopyFile(t, filepath.Join("fixtures", "watch", "compose.yaml"), composeFilePath)
59+
60+
projName := "e2e-watch-" + svcName
61+
env := []string{
62+
"COMPOSE_FILE=" + composeFilePath,
63+
"COMPOSE_PROJECT_NAME=" + projName,
64+
}
65+
66+
cli := NewParallelCLI(t, WithEnv(env...))
67+
68+
cleanup := func() {
69+
cli.RunDockerComposeCmd(t, "down", svcName, "--timeout=0", "--remove-orphans", "--volumes")
70+
}
71+
cleanup()
72+
t.Cleanup(cleanup)
73+
74+
cli.RunDockerComposeCmd(t, "up", svcName, "--wait", "--build")
75+
76+
cmd := cli.NewDockerComposeCmd(t, "--verbose", "alpha", "watch", svcName)
77+
// stream output since watch runs in the background
78+
cmd.Stdout = os.Stdout
79+
cmd.Stderr = os.Stderr
80+
r := icmd.StartCmd(cmd)
81+
require.NoError(t, r.Error)
82+
t.Cleanup(func() {
83+
// IMPORTANT: watch doesn't exit on its own, don't leak processes!
84+
if r.Cmd.Process != nil {
85+
_ = r.Cmd.Process.Kill()
86+
}
87+
})
88+
var testComplete atomic.Bool
89+
go func() {
90+
// if the process exits abnormally before the test is done, fail the test
91+
if err := r.Cmd.Wait(); err != nil && !testComplete.Load() {
92+
assert.Check(t, cmp.Nil(err))
93+
}
94+
}()
95+
96+
require.NoError(t, os.Mkdir(dataDir, 0o700))
97+
98+
checkFileContents := func(path string, contents string) poll.Check {
99+
return func(pollLog poll.LogT) poll.Result {
100+
if r.Cmd.ProcessState != nil {
101+
return poll.Error(fmt.Errorf("watch process exited early: %s", r.Cmd.ProcessState))
102+
}
103+
res := icmd.RunCmd(cli.NewDockerComposeCmd(t, "exec", svcName, "cat", path))
104+
if strings.Contains(res.Stdout(), contents) {
105+
return poll.Success()
106+
}
107+
return poll.Continue(res.Combined())
108+
}
109+
}
110+
111+
waitForFlush := func() {
112+
sentinelVal := uuid.Generate().String()
113+
writeDataFile("wait.txt", sentinelVal)
114+
poll.WaitOn(t, checkFileContents("/app/data/wait.txt", sentinelVal))
115+
}
116+
117+
t.Logf("Writing to a file until Compose watch is up and running")
118+
poll.WaitOn(t, func(t poll.LogT) poll.Result {
119+
writeDataFile("hello.txt", "hello world")
120+
return checkFileContents("/app/data/hello.txt", "hello world")(t)
121+
})
122+
123+
t.Logf("Modifying file contents")
124+
writeDataFile("hello.txt", "hello watch")
125+
poll.WaitOn(t, checkFileContents("/app/data/hello.txt", "hello watch"))
126+
127+
t.Logf("Deleting file")
128+
require.NoError(t, os.Remove(filepath.Join(dataDir, "hello.txt")))
129+
waitForFlush()
130+
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/hello.txt").
131+
Assert(t, icmd.Expected{
132+
ExitCode: 1,
133+
Err: "No such file or directory",
134+
},
135+
)
136+
137+
t.Logf("Writing to ignored paths")
138+
writeDataFile("data.foo", "ignored")
139+
writeDataFile(filepath.Join("ignored", "hello.txt"), "ignored")
140+
waitForFlush()
141+
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/data.foo").
142+
Assert(t, icmd.Expected{
143+
ExitCode: 1,
144+
Err: "No such file or directory",
145+
},
146+
)
147+
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/ignored").
148+
Assert(t, icmd.Expected{
149+
ExitCode: 1,
150+
Err: "No such file or directory",
151+
},
152+
)
153+
154+
t.Logf("Creating subdirectory")
155+
require.NoError(t, os.Mkdir(filepath.Join(dataDir, "subdir"), 0o700))
156+
waitForFlush()
157+
cli.RunDockerComposeCmd(t, "exec", svcName, "stat", "/app/data/subdir")
158+
159+
t.Logf("Writing to file in subdirectory")
160+
writeDataFile(filepath.Join("subdir", "file.txt"), "a")
161+
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "a"))
162+
163+
t.Logf("Writing to file multiple times")
164+
writeDataFile(filepath.Join("subdir", "file.txt"), "x")
165+
writeDataFile(filepath.Join("subdir", "file.txt"), "y")
166+
writeDataFile(filepath.Join("subdir", "file.txt"), "z")
167+
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "z"))
168+
writeDataFile(filepath.Join("subdir", "file.txt"), "z")
169+
writeDataFile(filepath.Join("subdir", "file.txt"), "y")
170+
writeDataFile(filepath.Join("subdir", "file.txt"), "x")
171+
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "x"))
172+
173+
t.Logf("Deleting directory")
174+
require.NoError(t, os.RemoveAll(filepath.Join(dataDir, "subdir")))
175+
waitForFlush()
176+
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/subdir").
177+
Assert(t, icmd.Expected{
178+
ExitCode: 1,
179+
Err: "No such file or directory",
180+
},
181+
)
182+
183+
testComplete.Store(true)
184+
}

0 commit comments

Comments
 (0)