blob: 37a69f9a9cca6bc791d64f90badcc450630a55db [file] [log] [blame]
David Why Use Two When One Will Do Presotto7034d3b2014-05-30 17:08:46 -07001package config
2
3import (
4 "errors"
5 "fmt"
6 "io/ioutil"
7 "os"
8 "testing"
9 "time"
10)
11
12// A config file to be written to disk.
13var configFileA = `
14the:rain
15 in:spain
16falls: mainly
17 on : the
18# something ugly this way comes
19plain: today
20version : 1
21`
22
23// A map referring to the config in configFileA.
24var referenceA = map[string]string{
25 "the": "rain",
26 "in": "spain",
27 "falls": "mainly",
28 "on": "the",
29 "plain": "today",
30 "version": "1",
31}
32
33// Another config file to be written to disk.
34var configFileB = `
35the:rain
36 in:spain
37falls: mainly
38 on : my foot
39# something ugly this way comes
40version : 1.1
41`
42
43// A map referring to the config in configFileB.
44var referenceB = map[string]string{
45 "the": "rain",
46 "in": "spain",
47 "falls": "mainly",
48 "on": "my foot",
49 "version": "1.1",
50}
51
52func compare(actual map[string]string, reference map[string]string) error {
53 for k, rv := range reference {
54 av, ok := actual[k]
55 if !ok {
56 return fmt.Errorf("missing entry for key %q", k)
57 }
58 if av != rv {
59 return fmt.Errorf("bad value for key %q: expected %q, got %q", k, rv, av)
60 }
61 }
62 for k, av := range actual {
63 if _, ok := reference[k]; !ok {
64 return fmt.Errorf("unexpected pair %q : %q", k, av)
65 }
66 }
67 return nil
68}
69
70func compareConfigToReference(cs ConfigService, ref map[string]string) error {
71 actual, err := cs.GetAll()
72 if err != nil {
73 return err
74 }
75 return compare(actual, ref)
76}
77
78func compareFileToReference(file string, ref map[string]string) error {
79 c, err := readFile(file)
80 if err != nil {
81 return err
82 }
83 return compare(c.pairs, ref)
84}
85
86func waitForConsistency(cs ConfigService, ref map[string]string) error {
87 finalerr := errors.New("inconsistent")
David Why Use Two When One Will Do Presottoc54f8782014-09-29 11:48:26 -070088 for loops := 0; loops < 30; loops++ {
David Why Use Two When One Will Do Presotto7034d3b2014-05-30 17:08:46 -070089 actual, err := cs.GetAll()
90 if err == nil {
91 err := compare(actual, ref)
92 if err == nil {
93 return nil
94 }
95 finalerr = err
96 }
97 time.Sleep(500 * time.Millisecond)
98 }
David Why Use Two When One Will Do Presottoc54f8782014-09-29 11:48:26 -070099 return errors.New("timeout waiting for consistency: " + finalerr.Error())
David Why Use Two When One Will Do Presotto7034d3b2014-05-30 17:08:46 -0700100}
101
102func testConfig(fileA, fileB, fileD string) error {
103 // Write a config to fileA.
104 if err := ioutil.WriteFile(fileA, []byte(configFileA), 0644); err != nil {
105 return fmt.Errorf("writing initial %s: %s", fileA, err)
106 }
107
108 // Start a config service with a file and compare the parsed file to the reference.
109 csa, err := MDNSConfigService("foo", fileA, true)
110 if err != nil {
111 return fmt.Errorf("creating service %s", err)
112 }
113 defer csa.Stop()
114 if err := compareConfigToReference(csa, referenceA); err != nil {
115 return fmt.Errorf("confA ~ refA: %s", err)
116 }
117
118 // Create a second instance with a non existant file.
119 csb, err := MDNSConfigService("foo", fileB, true)
120 if err != nil {
121 return fmt.Errorf("creating service %s", err)
122 }
123 defer csb.Stop()
124
125 // Create a third passive instance with no file.
126 csc, err := MDNSConfigService("foo", "", true)
127 if err != nil {
128 return fmt.Errorf("creating service %s", err)
129 }
130 defer csc.Stop()
131
132 // Loop till the second instance gets a config or we time out.
133 if err := waitForConsistency(csb, referenceA); err != nil {
134 return fmt.Errorf("confB ~ refA: %s", err)
135 }
136
137 // Make sure that the new instance updated its file.
138 if err := compareFileToReference(fileB, referenceA); err != nil {
139 return fmt.Errorf("fileB ~ refA: %s", err)
140 }
141
142 // Rewrite/Reread the second instance's file, make sure the it rereads it correctly.
143 if err := ioutil.WriteFile(fileB, []byte(configFileB), 0644); err != nil {
144 return fmt.Errorf("writing fileB: %s", err)
145 }
146 if err := csb.Reread(); err != nil {
147 return fmt.Errorf("Rereading fileB: %s", err)
148 }
149 if err := compareConfigToReference(csb, referenceB); err != nil {
150 return fmt.Errorf("confB ~ refB: %s", err)
151 }
152 // Loop till the first instance changes its config or we time out.
153 if err := waitForConsistency(csa, referenceB); err != nil {
154 return fmt.Errorf("confA ~ refB: %s", err)
155 }
156
157 // Make sure that the first instance updated its file.
158 if err := compareFileToReference(fileA, referenceB); err != nil {
159 return fmt.Errorf("fileA ~ refB: %s", err)
160 }
161
162 // Loop till the passive instance changes its config or we time out.
163 if err := waitForConsistency(csc, referenceB); err != nil {
164 return fmt.Errorf("confC ~ refB: %s", err)
165 }
166
167 // Create an instance with an lower numbered version. Make sure it doesn't
168 // overcome the higher numbered one.
169 if err := ioutil.WriteFile(fileD, []byte(configFileA), 0644); err != nil {
170 return fmt.Errorf("writing initial %s: %s", fileD, err)
171 }
172 csd, err := MDNSConfigService("foo", fileD, true)
173 if err != nil {
174 return fmt.Errorf("creating service %s", err)
175 }
176 defer csd.Stop()
177 if err := waitForConsistency(csd, referenceB); err != nil {
178 return fmt.Errorf("confD ~ refB: %s", err)
179 }
180 if err := waitForConsistency(csc, referenceA); err == nil {
181 return errors.New("eventual consistency picked wrong version for C")
182 }
183
184 return nil
185}
186
187func createTempFile(t *testing.T, base string) string {
188 f, err := ioutil.TempFile("", base)
189 if err != nil {
190 t.Fatal("creating temp file: %s", err)
191 }
192 f.Close()
193 return f.Name()
194}
195
196func TestConfig(t *testing.T) {
197 // Create temporary files.
198 fileA := createTempFile(t, "testconfigA")
199 defer os.Remove(fileA)
200 fileB := createTempFile(t, "testconfigB")
201 defer os.Remove(fileB)
202 fileD := createTempFile(t, "testconfigD")
203 defer os.Remove(fileD)
204
205 if err := testConfig(fileA, fileB, fileD); err != nil {
206 t.Fatal(err)
207 }
208}
209
210func TestConfigStream(t *testing.T) {
211 // Create temporary file.
212 fileA := createTempFile(t, "testconfigA")
213 defer os.Remove(fileA)
214
215 // Start a cs service.
216 cs, err := MDNSConfigService("foo", fileA, true)
217 if err != nil {
218 t.Fatal("creating service %s", err)
219 }
220
221 // Watch a single key and all keys.
222 ws := cs.Watch("version")
223 wa := cs.WatchAll()
224
225 // Since there's no config yet, we should just get a
226 // deleted entry for the single key.
227 select {
228 case p := <-ws:
229 if !p.Nonexistant {
230 t.Fatal("expected Nonexistant: got %v", p)
231 }
232 case <-time.After(2 * time.Second):
233 t.Fatal("timeout waiting for single config var")
234 }
235
236 // Write a config to the file and read it.
237 if err := ioutil.WriteFile(fileA, []byte(configFileA), 0644); err != nil {
238 t.Fatal("writing initial %s: %s", fileA, err)
239 }
240 if err := cs.Reread(); err != nil {
241 t.Fatal("rereading config: %s", err)
242 }
243
244 // We should now have a version number.
245 select {
246 case p := <-ws:
247 if p.Nonexistant || p.Key != "version" || p.Value != "1" {
248 t.Fatal("expected [version, 1, false]: got %v", p)
249 }
250
251 case <-time.After(2 * time.Second):
252 t.Fatal("timeout waiting for single config var")
253 }
254
255 // And we should be streamed the whole version.
256 cmap := make(map[string]string, 0)
257L:
258 for {
259 select {
260 case p := <-wa:
261 if p.Nonexistant {
262 t.Fatal("unexpected %v", p)
263 }
264 cmap[p.Key] = p.Value
265 err := compare(referenceA, cmap)
266 if err == nil {
267 break L
268 }
269 case <-time.After(2 * time.Second):
270 t.Fatal("timeout waiting for all config vars")
271 }
272 }
273
274 // Change and reread the config file.
275 if err := ioutil.WriteFile(fileA, []byte(configFileB), 0644); err != nil {
276 t.Fatal("writing initial %s: %s", fileA, err)
277 }
278 if err := cs.Reread(); err != nil {
279 t.Fatal("rereading config: %s", err)
280 }
281
282 // The version should have changed.
283 select {
284 case p := <-ws:
285 if p.Nonexistant || p.Key != "version" || p.Value != "1.1" {
286 t.Fatal("expected [version, 1.1, false]: got %v", p)
287 }
288
289 case <-time.After(2 * time.Second):
290 t.Fatal("timeout waiting for single config var")
291 }
292
293 // And we should now be consistent with referenceB
294L2:
295 for {
296 select {
297 case p := <-wa:
298 if p.Nonexistant {
299 delete(cmap, p.Key)
300 break
301 }
302 cmap[p.Key] = p.Value
303 err := compare(referenceB, cmap)
304 if err == nil {
305 break L2
306 }
307 case <-time.After(2 * time.Second):
308 t.Fatal("timeout waiting for all config vars")
309 }
310 }
311}