|
1 | 1 | package generator |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "bufio" |
5 | | - "encoding/json" |
6 | | - "fmt" |
7 | 4 | "io" |
8 | 5 | "log" |
9 | | - "net/http" |
10 | | - "os" |
11 | | - "strings" |
12 | | - "sync/atomic" |
13 | 6 | "testing" |
| 7 | + "testing/synctest" |
14 | 8 | "time" |
15 | 9 |
|
16 | 10 | docker "github.com/fsouza/go-dockerclient" |
17 | | - dockertest "github.com/fsouza/go-dockerclient/testing" |
18 | 11 | "github.com/nginx-proxy/docker-gen/internal/config" |
19 | | - "github.com/nginx-proxy/docker-gen/internal/context" |
20 | | - "github.com/nginx-proxy/docker-gen/internal/dockerclient" |
| 12 | + "github.com/stretchr/testify/assert" |
21 | 13 | ) |
22 | 14 |
|
23 | | -func TestGenerateFromEvents(t *testing.T) { |
| 15 | +func newStartEvent() *docker.APIEvents { |
| 16 | + return &docker.APIEvents{Type: "container", Action: "start"} |
| 17 | +} |
| 18 | + |
| 19 | +// TestNewDebounceChannel deterministically verifies debounce timing via testing/synctest's fake clock (replaces the flaky TestGenerateFromEvents, #238). |
| 20 | +func TestNewDebounceChannel(t *testing.T) { |
| 21 | + orig := log.Writer() |
24 | 22 | log.SetOutput(io.Discard) |
25 | | - containerID := "8dfafdbc3a40" |
26 | | - var counter atomic.Int32 |
27 | | - |
28 | | - eventsResponse := ` |
29 | | -{"Type":"container","Action":"start","Actor": {"ID":"8dfafdbc3a40"},"Time":1374067924} |
30 | | -{"Type":"container","Action":"stop","Actor": {"ID":"8dfafdbc3a40"},"Time":1374067966} |
31 | | -{"Type":"container","Action":"start","Actor": {"ID":"8dfafdbc3a40"},"Time":1374067970}` |
32 | | - infoResponse := `{"Containers":1,"Images":1,"Debug":false,"NFd":11,"NGoroutines":21,"MemoryLimit":true,"SwapLimit":false}` |
33 | | - versionResponse := `{"Version":"19.03.12","Os":"Linux","KernelVersion":"4.19.76-linuxkit","GoVersion":"go1.13.14","GitCommit":"48a66213fe","Arch":"amd64","ApiVersion":"1.40"}` |
34 | | - |
35 | | - server, _ := dockertest.NewServer("127.0.0.1:0", nil, nil) |
36 | | - server.CustomHandler("/events", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
37 | | - rsc := bufio.NewScanner(strings.NewReader(eventsResponse)) |
38 | | - for rsc.Scan() { |
39 | | - w.Write([]byte(rsc.Text())) |
40 | | - w.(http.Flusher).Flush() |
| 23 | + t.Cleanup(func() { log.SetOutput(orig) }) |
| 24 | + |
| 25 | + t.Run("passes events through when Min is zero", func(t *testing.T) { |
| 26 | + synctest.Test(t, func(t *testing.T) { |
| 27 | + input := make(chan *docker.APIEvents, 1) |
| 28 | + out := newDebounceChannel(input, &config.Wait{Min: 0, Max: 0}) |
| 29 | + |
| 30 | + ev := newStartEvent() |
| 31 | + input <- ev |
| 32 | + synctest.Wait() |
| 33 | + |
| 34 | + select { |
| 35 | + case got := <-out: |
| 36 | + assert.Same(t, ev, got) |
| 37 | + default: |
| 38 | + t.Fatal("expected the event to pass straight through") |
| 39 | + } |
| 40 | + }) |
| 41 | + }) |
| 42 | + |
| 43 | + t.Run("coalesces a burst and fires Min after the last event", func(t *testing.T) { |
| 44 | + synctest.Test(t, func(t *testing.T) { |
| 45 | + input := make(chan *docker.APIEvents) |
| 46 | + out := newDebounceChannel(input, &config.Wait{Min: 200 * time.Millisecond, Max: time.Second}) |
| 47 | + |
| 48 | + start := time.Now() |
| 49 | + var fires []time.Duration |
| 50 | + done := make(chan struct{}) |
| 51 | + go func() { |
| 52 | + for range out { |
| 53 | + fires = append(fires, time.Since(start)) |
| 54 | + } |
| 55 | + close(done) |
| 56 | + }() |
| 57 | + |
| 58 | + input <- newStartEvent() // t=0 |
| 59 | + time.Sleep(150 * time.Millisecond) |
| 60 | + input <- newStartEvent() // t=150ms (gap 150ms < Min) |
41 | 61 | time.Sleep(150 * time.Millisecond) |
42 | | - } |
43 | | - time.Sleep(500 * time.Millisecond) |
44 | | - })) |
45 | | - server.CustomHandler("/info", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
46 | | - w.Write([]byte(infoResponse)) |
47 | | - w.(http.Flusher).Flush() |
48 | | - })) |
49 | | - server.CustomHandler("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
50 | | - w.Write([]byte(versionResponse)) |
51 | | - w.(http.Flusher).Flush() |
52 | | - })) |
53 | | - server.CustomHandler("/containers/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
54 | | - result := []docker.APIContainers{ |
55 | | - { |
56 | | - ID: containerID, |
57 | | - Image: "base:latest", |
58 | | - Command: "/bin/sh", |
59 | | - Created: time.Now().Unix(), |
60 | | - Status: "running", |
61 | | - Ports: []docker.APIPort{}, |
62 | | - Names: []string{"/docker-gen-test"}, |
63 | | - }, |
64 | | - } |
65 | | - w.Header().Set("Content-Type", "application/json") |
66 | | - w.WriteHeader(http.StatusOK) |
67 | | - json.NewEncoder(w).Encode(result) |
68 | | - })) |
69 | | - server.CustomHandler(fmt.Sprintf("/containers/%s/json", containerID), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
70 | | - counter := counter.Add(1) |
71 | | - container := docker.Container{ |
72 | | - Name: "docker-gen-test", |
73 | | - ID: containerID, |
74 | | - Created: time.Now(), |
75 | | - Path: "/bin/sh", |
76 | | - Args: []string{}, |
77 | | - Config: &docker.Config{ |
78 | | - Hostname: "docker-gen", |
79 | | - AttachStdout: true, |
80 | | - AttachStderr: true, |
81 | | - Env: []string{fmt.Sprintf("COUNTER=%d", counter)}, |
82 | | - Cmd: []string{"/bin/sh"}, |
83 | | - Image: "base:latest", |
84 | | - }, |
85 | | - HostConfig: &docker.HostConfig{ |
86 | | - NetworkMode: "container:d246e2c9e3d465d96359c942e91de493f6d51a01ba33900d865180d64c34ee91", |
87 | | - }, |
88 | | - State: docker.State{ |
89 | | - Running: true, |
90 | | - Pid: 400, |
91 | | - ExitCode: 0, |
92 | | - StartedAt: time.Now(), |
93 | | - Health: docker.Health{ |
94 | | - Status: "healthy", |
95 | | - FailingStreak: 5, |
96 | | - Log: []docker.HealthCheck{}, |
97 | | - }, |
98 | | - }, |
99 | | - Image: "0ff407d5a7d9ed36acdf3e75de8cc127afecc9af234d05486be2981cdc01a38d", |
100 | | - NetworkSettings: &docker.NetworkSettings{ |
101 | | - IPAddress: "10.0.0.10", |
102 | | - IPPrefixLen: 24, |
103 | | - Gateway: "10.0.0.1", |
104 | | - Bridge: "docker0", |
105 | | - PortMapping: map[string]docker.PortMapping{}, |
106 | | - Ports: map[docker.Port][]docker.PortBinding{}, |
107 | | - }, |
108 | | - ResolvConfPath: "/etc/resolv.conf", |
109 | | - } |
110 | | - w.Header().Set("Content-Type", "application/json") |
111 | | - w.WriteHeader(http.StatusOK) |
112 | | - json.NewEncoder(w).Encode(container) |
113 | | - })) |
114 | | - |
115 | | - serverURL := fmt.Sprintf("tcp://%s", strings.TrimRight(strings.TrimPrefix(server.URL(), "http://"), "/")) |
116 | | - client, err := dockerclient.NewDockerClient(serverURL, false, "", "", "") |
117 | | - if err != nil { |
118 | | - t.Errorf("Failed to create client: %s", err) |
119 | | - } |
120 | | - client.SkipServerVersionCheck = true |
121 | | - |
122 | | - tmplFile, err := os.CreateTemp(os.TempDir(), "docker-gen-tmpl") |
123 | | - if err != nil { |
124 | | - t.Errorf("Failed to create temp file: %v\n", err) |
125 | | - } |
126 | | - defer func() { |
127 | | - tmplFile.Close() |
128 | | - os.Remove(tmplFile.Name()) |
129 | | - }() |
130 | | - err = os.WriteFile(tmplFile.Name(), []byte("{{range $key, $value := .}}{{$value.ID}}.{{$value.Env.COUNTER}}{{end}}"), 0644) |
131 | | - if err != nil { |
132 | | - t.Errorf("Failed to write to temp file: %v\n", err) |
133 | | - } |
134 | | - |
135 | | - var destFiles []*os.File |
136 | | - for i := 0; i < 4; i++ { |
137 | | - destFile, err := os.CreateTemp(os.TempDir(), "docker-gen-out") |
138 | | - if err != nil { |
139 | | - t.Errorf("Failed to create temp file: %v\n", err) |
140 | | - } |
141 | | - destFiles = append(destFiles, destFile) |
142 | | - } |
143 | | - defer func() { |
144 | | - for _, destFile := range destFiles { |
145 | | - destFile.Close() |
146 | | - os.Remove(destFile.Name()) |
147 | | - } |
148 | | - }() |
149 | | - |
150 | | - apiVersion, err := client.Version() |
151 | | - if err != nil { |
152 | | - t.Errorf("Failed to retrieve docker server version info: %v\n", err) |
153 | | - } |
154 | | - context.SetDockerEnv(apiVersion) // prevents a panic |
155 | | - |
156 | | - generator := &generator{ |
157 | | - Client: client, |
158 | | - Endpoint: serverURL, |
159 | | - Configs: config.ConfigFile{ |
160 | | - Config: []config.Config{ |
161 | | - { |
162 | | - Template: tmplFile.Name(), |
163 | | - Dest: destFiles[0].Name(), |
164 | | - Watch: false, |
165 | | - }, |
166 | | - { |
167 | | - Template: tmplFile.Name(), |
168 | | - Dest: destFiles[1].Name(), |
169 | | - Watch: true, |
170 | | - Wait: &config.Wait{Min: 0, Max: 0}, |
171 | | - }, |
172 | | - { |
173 | | - Template: tmplFile.Name(), |
174 | | - Dest: destFiles[2].Name(), |
175 | | - Watch: true, |
176 | | - Wait: &config.Wait{Min: 200 * time.Millisecond, Max: 250 * time.Millisecond}, |
177 | | - }, |
178 | | - { |
179 | | - Template: tmplFile.Name(), |
180 | | - Dest: destFiles[3].Name(), |
181 | | - Watch: true, |
182 | | - Wait: &config.Wait{Min: 250 * time.Millisecond, Max: 1 * time.Second}, |
183 | | - }, |
184 | | - }, |
185 | | - }, |
186 | | - retry: false, |
187 | | - } |
188 | | - |
189 | | - generator.generateFromEvents() |
190 | | - generator.wg.Wait() |
191 | | - |
192 | | - var ( |
193 | | - value []byte |
194 | | - expected string |
195 | | - ) |
196 | | - |
197 | | - // The counter is incremented in each output file in the following sequence: |
198 | | - // |
199 | | - // init 150ms 200ms 250ms 300ms 350ms 400ms 450ms 500ms 550ms 600ms 650ms 700ms |
200 | | - // ├──────╫──────┼──────┼──────╫──────┼──────┼──────╫──────┼──────┼──────┼──────┼──────┤ |
201 | | - // File0 ├─ 1 ║ ║ ║ |
202 | | - // File1 ├─ 2 ╟─ 5 ╟─ 6 ╟─ 8 |
203 | | - // File2 ├─ 3 ╟───── max (250ms) ──║───────────> 7 ╟─────── min (200ms) ─────> 9 |
204 | | - // File3 └─ 4 ╟──────────────────> ╟──────────────────> ╟─────────── min (250ms) ────────> 10 |
205 | | - // ┌───╨───┐ ┌───╨──┐ ┌───╨───┐ |
206 | | - // │ start │ │ stop │ │ start │ |
207 | | - // └───────┘ └──────┘ └───────┘ |
208 | | - |
209 | | - expectedCounters := []int{1, 8, 9, 10} |
210 | | - |
211 | | - for i, counter := range expectedCounters { |
212 | | - value, _ = os.ReadFile(destFiles[i].Name()) |
213 | | - expected = fmt.Sprintf("%s.%d", containerID, counter) |
214 | | - if string(value) != expected { |
215 | | - t.Errorf("expected: %s. got: %s", expected, value) |
216 | | - } |
217 | | - } |
| 62 | + input <- newStartEvent() // t=300ms (gap 150ms < Min) |
| 63 | + time.Sleep(time.Second) // advance the fake clock so the pending timer fires |
| 64 | + synctest.Wait() |
| 65 | + |
| 66 | + close(input) |
| 67 | + <-done |
| 68 | + |
| 69 | + // One coalesced event, fired Min (200ms) after the last event (t=300ms). |
| 70 | + assert.Equal(t, []time.Duration{500 * time.Millisecond}, fires) |
| 71 | + }) |
| 72 | + }) |
| 73 | + |
| 74 | + t.Run("Max caps the wait when events keep arriving", func(t *testing.T) { |
| 75 | + synctest.Test(t, func(t *testing.T) { |
| 76 | + input := make(chan *docker.APIEvents) |
| 77 | + out := newDebounceChannel(input, &config.Wait{Min: 200 * time.Millisecond, Max: 250 * time.Millisecond}) |
| 78 | + |
| 79 | + start := time.Now() |
| 80 | + var fires []time.Duration |
| 81 | + done := make(chan struct{}) |
| 82 | + go func() { |
| 83 | + for range out { |
| 84 | + fires = append(fires, time.Since(start)) |
| 85 | + } |
| 86 | + close(done) |
| 87 | + }() |
| 88 | + |
| 89 | + input <- newStartEvent() // t=0: minTimer->200ms, maxTimer->250ms |
| 90 | + time.Sleep(150 * time.Millisecond) |
| 91 | + input <- newStartEvent() // t=150ms: minTimer reset->350ms, maxTimer still 250ms |
| 92 | + time.Sleep(150 * time.Millisecond) // maxTimer fires at 250ms -> first output |
| 93 | + input <- newStartEvent() // t=300ms: new burst, minTimer->500ms |
| 94 | + time.Sleep(time.Second) // advance the fake clock so the pending timer fires (500ms) |
| 95 | + synctest.Wait() |
| 96 | + |
| 97 | + close(input) |
| 98 | + <-done |
| 99 | + |
| 100 | + // First output capped by Max at 250ms; second is Min after the t=300ms event. |
| 101 | + assert.Equal(t, []time.Duration{250 * time.Millisecond, 500 * time.Millisecond}, fires) |
| 102 | + }) |
| 103 | + }) |
218 | 104 | } |
0 commit comments