func TestStart_runsCommandOnChange(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) consul.SetKV("foo/bar", []byte("one")) defer consul.Stop() config := testConfig(fmt.Sprintf(` consul = "%s" upcase = true prefix { path = "foo" } `, consul.HTTPAddr), t) f := test.CreateTempfile(nil, t) defer os.Remove(f.Name()) os.Remove(f.Name()) runner, err := NewRunner(config, []string{"sh", "-c", "echo $BAR > " + f.Name()}, true) if err != nil { t.Fatal(err) } runner.outStream, runner.errStream = ioutil.Discard, ioutil.Discard go runner.Start() defer runner.Stop() test.WaitForFileContents(f.Name(), []byte("one\n"), t) }
func testDedupManager(t *testing.T, templ []*Template) (*testutil.TestServer, *DedupManager) { consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) // Setup the configuration config := DefaultConfig() config.Consul = consul.HTTPAddr // Create the clientset clients, err := newClientSet(config) if err != nil { t.Fatalf("runner: %s", err) } // Setup a brain brain := NewBrain() // Create the dedup manager dedup, err := NewDedupManager(config, clients, brain, templ) if err != nil { t.Fatalf("err: %v", err) } return consul, dedup }
func NewTestServer(t *testing.T) *testutil.TestServer { // Create a server srv := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { if !testing.Verbose() { c.LogLevel = "err" } }) return srv }
func TestRun_merges(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() consul.SetKV("config/global/address", []byte("1.2.3.4")) consul.SetKV("config/global/port", []byte("5598")) consul.SetKV("config/redis/port", []byte("8000")) config := testConfig(fmt.Sprintf(` consul = "%s" upcase = true prefix { path = "config/global" } prefix { path = "config/redis" } `, consul.HTTPAddr), t) runner, err := NewRunner(config, []string{"env"}, true) if err != nil { t.Fatal(err) } outStream, errStream := new(bytes.Buffer), new(bytes.Buffer) runner.outStream, runner.errStream = outStream, errStream go runner.Start() defer runner.Stop() select { case err := <-runner.ErrCh: t.Fatal(err) case <-runner.ExitCh: expected := "ADDRESS=1.2.3.4" if !strings.Contains(outStream.String(), expected) { t.Fatalf("expected %q to include %q", outStream.String(), expected) } expected = "PORT=8000" if !strings.Contains(outStream.String(), expected) { t.Fatalf("expected %q to include %q", outStream.String(), expected) } } }
// Warning: this is a super fragile and time-dependent test. If it's failing, // check the demo Consul cluster and your own sanity before you assume your // code broke something... func TestRunner_quiescence(t *testing.T) { consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() in := test.CreateTempfile([]byte(` {{ range service "consul" "any" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in, t) out := test.CreateTempfile(nil, t) test.DeleteTempfile(out, t) config := testConfig(fmt.Sprintf(` consul = "%s" wait = "50ms:200ms" template { source = "%s" destination = "%s" } `, consul.HTTPAddr, in.Name(), out.Name()), t) runner, err := NewRunner(config, false, false) if err != nil { t.Fatal(err) } go runner.Start() defer runner.Stop() min := time.After(10 * time.Millisecond) max := time.After(500 * time.Millisecond) for { select { case <-min: if _, err = os.Stat(out.Name()); !os.IsNotExist(err) { t.Errorf("expected quiescence timer to not fire for yet") } continue case <-max: if _, err = os.Stat(out.Name()); os.IsNotExist(err) { t.Errorf("expected template to be rendered by now") } return } } }
func makeClientWithConfig(t *testing.T) (*consulapi.Client, *testutil.TestServer) { // Make client config conf := consulapi.DefaultConfig() // Create server server := testutil.NewTestServerConfig(t, nil) conf.Address = server.HTTPAddr // Create client client, err := consulapi.NewClient(conf) if err != nil { t.Fatalf("err: %v", err) } return client, server }
func TestRun_stdin(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() consul.SetKV("foo/bar/bar", []byte("baz")) config := testConfig(fmt.Sprintf(` consul = "%s" prefix { path = "foo/bar" } `, consul.HTTPAddr), t) runner, err := NewRunner(config, []string{"cat"}, true) if err != nil { t.Fatal(err) } outStream, errStream := gatedio.NewByteBuffer(), gatedio.NewByteBuffer() inStream := gatedio.NewByteBuffer() runner.outStream, runner.errStream = outStream, errStream runner.inStream = inStream go runner.Start() defer runner.Stop() if _, err := inStream.WriteString("foo"); err != nil { t.Fatal(err) } select { case err := <-runner.ErrCh: t.Fatal(err) case <-runner.ExitCh: expected := "foo" if !strings.Contains(outStream.String(), expected) { t.Fatalf("expected %q to include %q", outStream.String(), expected) } } }
func TestRun_pristine(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() consul.SetKV("foo/bar/bar", []byte("baz")) config := testConfig(fmt.Sprintf(` consul = "%s" pristine = true prefix { path = "foo/bar" } `, consul.HTTPAddr), t) runner, err := NewRunner(config, []string{"env"}, true) if err != nil { t.Fatal(err) } outStream, errStream := new(bytes.Buffer), new(bytes.Buffer) runner.outStream, runner.errStream = outStream, errStream go runner.Start() defer runner.Stop() select { case err := <-runner.ErrCh: t.Fatal(err) case <-runner.ExitCh: expected := "bar=baz" if !strings.Contains(outStream.String(), expected) { t.Fatalf("expected %q to include %q", outStream.String(), expected) } notExpected := "HOME=" if strings.Contains(outStream.String(), notExpected) { t.Fatalf("did not expect %q to include %q", outStream.String(), notExpected) } } }
// testConsulServer is a helper for creating a Consul server and returning the // appropriate configuration to connect to it. func testConsulServer(t *testing.T) (*ClientSet, *testutil.TestServer) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) clients := NewClientSet() if err := clients.CreateConsulClient(&CreateConsulClientInput{ Address: consul.HTTPAddr, }); err != nil { consul.Stop() t.Fatalf("clientset: %s", err) } return clients, consul }
func TestRun_overwrites(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() consul.SetKV("config/global/address", []byte("1.2.3.4")) config := testConfig(fmt.Sprintf(` consul = "%s" prefix { path = "config/global" } `, consul.HTTPAddr), t) runner, err := NewRunner(config, []string{"env"}, true) if err != nil { t.Fatal(err) } outStream, errStream := gatedio.NewByteBuffer(), gatedio.NewByteBuffer() runner.outStream, runner.errStream = outStream, errStream // Set the env to ensure it overwrites os.Setenv("address", "should_be_overwritten") defer os.Unsetenv("address") go runner.Start() defer runner.Stop() select { case err := <-runner.ErrCh: t.Fatal(err) case <-runner.ExitCh: expected := "address=1.2.3.4" if !strings.Contains(outStream.String(), expected) { t.Fatalf("expected %q to include %q", outStream.String(), expected) } } }
// testConsul returns a Syncer configured with an embedded Consul server. // // Callers must defer Syncer.Shutdown() and TestServer.Stop() // func testConsul(t *testing.T) (*Syncer, *testutil.TestServer) { // Create an embedded Consul server testconsul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { // If -v wasn't specified squelch consul logging if !testing.Verbose() { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard } }) // Configure Syncer to talk to the test server cconf := config.DefaultConsulConfig() cconf.Addr = testconsul.HTTPAddr cs, err := NewSyncer(cconf, nil, logger) if err != nil { t.Fatalf("Error creating Syncer: %v", err) } return cs, testconsul }
func TestRun_onceFlag(t *testing.T) { consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() consul.SetKV("foo", []byte("bar")) template := test.CreateTempfile([]byte(` {{key "foo"}} `), t) defer test.DeleteTempfile(template, t) out := test.CreateTempfile(nil, t) defer test.DeleteTempfile(out, t) outStream := gatedio.NewByteBuffer() cli := NewCLI(outStream, outStream) command := fmt.Sprintf("consul-template -consul %s -template %s:%s -once -log-level debug", consul.HTTPAddr, template.Name(), out.Name()) args := strings.Split(command, " ") ch := make(chan int, 1) go func() { ch <- cli.Run(args) }() select { case status := <-ch: if status != ExitCodeOK { t.Errorf("expected %d to eq %d", status, ExitCodeOK) t.Errorf("out: %s", outStream.String()) } case <-time.After(500 * time.Millisecond): t.Errorf("expected exit, did not exit after 500ms") t.Errorf("out: %s", outStream.String()) } }
func makeClientWithConfig( t *testing.T, cb1 configCallback, cb2 testutil.ServerConfigCallback) (*Client, *testutil.TestServer) { // Make client config conf := DefaultConfig() if cb1 != nil { cb1(conf) } // Create server server := testutil.NewTestServerConfig(t, cb2) conf.Address = server.HTTPAddr // Create client client, err := NewClient(conf) if err != nil { t.Fatalf("err: %v", err) } return client, server }
// testConsulServer is a helper for creating a Consul server and returning the // appropriate configuration to connect to it. func testConsulServer(t *testing.T) (*ClientSet, *testutil.TestServer) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) config := consulapi.DefaultConfig() config.Address = consul.HTTPAddr client, err := consulapi.NewClient(config) if err != nil { consul.Stop() t.Fatalf("consul api client err: %s", err) } clients := NewClientSet() if err := clients.Add(client); err != nil { consul.Stop() t.Fatalf("clientset err: %s", err) } return clients, consul }
func TestRun_vaultPrecedenceOverConsul(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() consul.SetKV("secret/foo/secret_foo_zip", []byte("baz")) vaultCore, _, token := vault.TestCoreUnsealed(t) ln, vaultAddr := http.TestServer(t, vaultCore) defer ln.Close() req := &logical.Request{ Operation: logical.WriteOperation, Path: "secret/foo", Data: map[string]interface{}{ "zip": "zap", "ding": "dong", }, ClientToken: token, } if _, err := vaultCore.HandleRequest(req); err != nil { t.Fatal(err) } vaultconfig := vaultapi.DefaultConfig() vaultconfig.Address = vaultAddr client, err := vaultapi.NewClient(vaultconfig) if err != nil { t.Fatal(err) } client.SetToken(token) // Create a new token - the core token is a root token and is therefore // not renewable secret, err := client.Auth().Token().Create(&vaultapi.TokenCreateRequest{ Lease: "1h", }) if err != nil { t.Fatal(err) } config := testConfig(fmt.Sprintf(` consul = "%s" vault { address = "%s" token = "%s" ssl { enabled = false } } secret { path = "secret/foo" } prefix { path = "secret/foo" } `, consul.HTTPAddr, vaultAddr, secret.Auth.ClientToken), t) runner, err := NewRunner(config, []string{"env"}, true) if err != nil { t.Fatal(err) } outStream, errStream := new(bytes.Buffer), new(bytes.Buffer) runner.outStream, runner.errStream = outStream, errStream go runner.Start() defer runner.Stop() select { case err := <-runner.ErrCh: t.Fatal(err) case <-runner.ExitCh: case <-time.After(250 * time.Millisecond): } expected := "secret_foo_zip=zap" if !strings.Contains(outStream.String(), expected) { t.Errorf("expected %q to include %q", outStream.String(), expected) } expected = "secret_foo_ding=dong" if !strings.Contains(outStream.String(), expected) { t.Errorf("expected %q to include %q", outStream.String(), expected) } }
func TestSyncerChaos(t *testing.T) { // Create an embedded Consul server testconsul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { // If -v wasn't specified squelch consul logging if !testing.Verbose() { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard } }) defer testconsul.Stop() // Configure Syncer to talk to the test server cconf := config.DefaultConsulConfig() cconf.Addr = testconsul.HTTPAddr clientSyncer, err := NewSyncer(cconf, nil, logger) if err != nil { t.Fatalf("Error creating Syncer: %v", err) } defer clientSyncer.Shutdown() execSyncer, err := NewSyncer(cconf, nil, logger) if err != nil { t.Fatalf("Error creating Syncer: %v", err) } defer execSyncer.Shutdown() clientService := &structs.Service{Name: "nomad-client"} services := map[ServiceKey]*structs.Service{ GenerateServiceKey(clientService): clientService, } if err := clientSyncer.SetServices("client", services); err != nil { t.Fatalf("error setting client service: %v", err) } const execn = 100 const reapern = 2 errors := make(chan error, 100) wg := sync.WaitGroup{} // Start goroutines to concurrently SetServices for i := 0; i < execn; i++ { wg.Add(1) go func(i int) { defer wg.Done() domain := ServiceDomain(fmt.Sprintf("exec-%d", i)) services := map[ServiceKey]*structs.Service{} for ii := 0; ii < 10; ii++ { s := &structs.Service{Name: fmt.Sprintf("exec-%d-%d", i, ii)} services[GenerateServiceKey(s)] = s if err := execSyncer.SetServices(domain, services); err != nil { select { case errors <- err: default: } return } time.Sleep(1) } }(i) } // SyncServices runs a timer started by Syncer.Run which we don't use // in this test, so run SyncServices concurrently wg.Add(1) go func() { defer wg.Done() for i := 0; i < execn; i++ { if err := execSyncer.SyncServices(); err != nil { select { case errors <- err: default: } return } time.Sleep(100) } }() wg.Add(1) go func() { defer wg.Done() if err := clientSyncer.ReapUnmatched([]ServiceDomain{"nomad-client"}); err != nil { select { case errors <- err: default: } return } }() // Reap all but exec-0-* wg.Add(1) go func() { defer wg.Done() for i := 0; i < execn; i++ { if err := execSyncer.ReapUnmatched([]ServiceDomain{"exec-0", ServiceDomain(fmt.Sprintf("exec-%d", i))}); err != nil { select { case errors <- err: default: } } time.Sleep(100) } }() go func() { wg.Wait() close(errors) }() for err := range errors { if err != nil { t.Errorf("error setting service from executor goroutine: %v", err) } } // Do a final ReapUnmatched to get consul back into a deterministic state if err := execSyncer.ReapUnmatched([]ServiceDomain{"exec-0"}); err != nil { t.Fatalf("error doing final reap: %v", err) } // flattenedServices should be fully populated as ReapUnmatched doesn't // touch Syncer's internal state expected := map[string]struct{}{} for i := 0; i < execn; i++ { for ii := 0; ii < 10; ii++ { expected[fmt.Sprintf("exec-%d-%d", i, ii)] = struct{}{} } } for _, s := range execSyncer.flattenedServices() { _, ok := expected[s.Name] if !ok { t.Errorf("%s unexpected", s.Name) } delete(expected, s.Name) } if len(expected) > 0 { left := []string{} for s := range expected { left = append(left, s) } sort.Strings(left) t.Errorf("Couldn't find %d names in flattened services:\n%s", len(expected), strings.Join(left, "\n")) } // All but exec-0 and possibly some of exec-99 should have been reaped { services, err := execSyncer.client.Agent().Services() if err != nil { t.Fatalf("Error getting services: %v", err) } expected := []int{} for k, service := range services { if service.Service == "consul" { continue } i := -1 ii := -1 fmt.Sscanf(service.Service, "exec-%d-%d", &i, &ii) switch { case i == -1 || ii == -1: t.Errorf("invalid service: %s -> %s", k, service.Service) case i != 0 || ii > 9: t.Errorf("unexpected service: %s -> %s", k, service.Service) default: expected = append(expected, ii) } } if len(expected) != 10 { t.Errorf("expected 0-9 but found: %#q", expected) } } }
// Warning: this is a super fragile and time-dependent test. If it's failing, // check the demo Consul cluster and your own sanity before you assume your // code broke something... func TestRunner_quiescenceIntegrated(t *testing.T) { consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() in, out := make([]*os.File, 2), make([]*os.File, 2) for i := 0; i < 2; i++ { in[i] = test.CreateTempfile([]byte(` {{ range service "consul" "any" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in[i], t) out[i] = test.CreateTempfile(nil, t) test.DeleteTempfile(out[i], t) } config := testConfig(fmt.Sprintf(` consul = "%s" wait = "100ms:200ms" template { source = "%s" destination = "%s" } template { source = "%s" destination = "%s" wait = "300ms:400ms" } `, consul.HTTPAddr, in[0].Name(), out[0].Name(), in[1].Name(), out[1].Name()), t) runner, err := NewRunner(config, false, false) if err != nil { t.Fatal(err) } go runner.Start() defer runner.Stop() // Watch for the appearance of the first template, which needs to at // least take 100 ms. We don't have enough certainty with Consul's // interactions with us to put tighter bounds. start := time.Now() for { dur := time.Now().Sub(start) _, err = os.Stat(out[0].Name()) if !os.IsNotExist(err) { if dur < 100*time.Millisecond { t.Fatalf("template appeared too quickly, %9.6f", dur.Seconds()) } break } if dur > 500*time.Millisecond { t.Fatalf("template should have appeared") } time.Sleep(1 * time.Millisecond) } // Now we know that the previous template just got rendered, so there // should have been tick() call on the second template. This is a clean // time base to check from and we can use tighter bounds here. start = time.Now() checks := []struct { eventCh <-chan time.Time fileExist bool }{ {time.After(1 * time.Millisecond), false}, {time.After(250 * time.Millisecond), false}, {time.After(350 * time.Millisecond), true}, } for { for idx, check := range checks { select { case <-check.eventCh: _, err = os.Stat(out[1].Name()) if os.IsNotExist(err) == check.fileExist { t.Errorf("check %d failed", idx) } if idx == len(checks)-1 { return } default: } } time.Sleep(1 * time.Millisecond) } }
func TestRunner_execRestart(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() tmpl := test.CreateTempfile([]byte(`{{ key "foo" }}`), t) defer test.DeleteTempfile(tmpl, t) out := test.CreateTempfile(nil, t) defer test.DeleteTempfile(out, t) // Create a tiny bash script for us to run as a "program" script := test.CreateTempfile([]byte(strings.TrimSpace(` #!/usr/bin/env bash while true; do : # Hang done `)), t) if err := os.Chmod(script.Name(), 0700); err != nil { t.Fatal(err) } defer test.DeleteTempfile(script, t) config := testConfig(fmt.Sprintf(` consul = "%s" template { source = "%s" destination = "%s" } exec { command = "%s" kill_timeout = "10ms" # Faster tests } `, consul.HTTPAddr, tmpl.Name(), out.Name(), script.Name()), t) runner, err := NewRunner(config, true, false) if err != nil { t.Fatal(err) } go runner.Start() defer runner.Stop() doneCh := make(chan struct{}, 1) go func() { for { if runner.child != nil { close(doneCh) return } time.Sleep(50 * time.Millisecond) } }() select { case err := <-runner.ErrCh: t.Fatal(err) case <-doneCh: // Childprocess is started, we can send it signals now case <-time.After(2 * time.Second): t.Fatal("child process should have started") } // Grab the current child pid - this will help us confirm the child was // restarted on template change. opid := runner.child.Pid() // Change a dependent value in Consul, which will force the runner to cycle. consul.SetKV("foo", []byte("bar")) // Give the runner time to do its thing. time.Sleep(1 * time.Second) npid := runner.child.Pid() if opid == npid { t.Errorf("expected %d to be different from %d", opid, npid) } }
func TestRunner_execReload(t *testing.T) { t.Parallel() consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() out := test.CreateTempfile(nil, t) defer test.DeleteTempfile(out, t) tmpl := test.CreateTempfile([]byte(`{{ key "foo" }}`), t) defer test.DeleteTempfile(tmpl, t) // Create a tiny bash script for us to run as a "program" script := test.CreateTempfile([]byte(strings.TrimSpace(fmt.Sprintf(` #!/usr/bin/env bash trap "echo 'one' >> %s" SIGUSR1 trap "echo 'two' >> %s" SIGUSR2 while true; do : # Hang done `, out.Name(), out.Name()))), t) if err := os.Chmod(script.Name(), 0700); err != nil { t.Fatal(err) } defer test.DeleteTempfile(script, t) config := testConfig(fmt.Sprintf(` consul = "%s" template { source = "%s" } exec { command = "%s" reload_signal = "sigusr1" kill_signal = "sigusr2" # We used SIGUSR2 to check, so there's force-kill shortly to make the # test faster. kill_timeout = "10ms" } `, consul.HTTPAddr, tmpl.Name(), script.Name()), t) runner, err := NewRunner(config, true, false) if err != nil { t.Fatal(err) } go runner.Start() defer runner.Stop() doneCh := make(chan struct{}, 1) go func() { for { if runner.child != nil { close(doneCh) return } time.Sleep(50 * time.Millisecond) } }() select { case err := <-runner.ErrCh: t.Fatal(err) case <-doneCh: // Childprocess is started, we can send it signals now case <-time.After(2 * time.Second): t.Fatal("child process should have started") } // Grab the current child pid - this will help us confirm the child was not // restarted on template change. opid := runner.child.Pid() // Change a dependent value in Consul, which will force the runner to cycle. consul.SetKV("foo", []byte("bar")) // Check that the reload signal was sent. test.WaitForFileContents(out.Name(), []byte("one\n"), t) npid := runner.child.Pid() if opid != npid { t.Errorf("expected %d to be the same as %d", opid, npid) } // Kill the child to check that the kill signal is properly sent. runner.child.Stop() test.WaitForFileContents(out.Name(), []byte("one\ntwo\n"), t) }
func TestRunner_dedup(t *testing.T) { t.Parallel() // Create a template in := test.CreateTempfile([]byte(` {{ range service "consul" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in, t) out1 := test.CreateTempfile(nil, t) defer test.DeleteTempfile(out1, t) out2 := test.CreateTempfile(nil, t) defer test.DeleteTempfile(out2, t) // Start consul consul := testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) { c.Stdout = ioutil.Discard c.Stderr = ioutil.Discard }) defer consul.Stop() // Setup the runner config config := DefaultConfig() config.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{Source: in.Name(), Destination: out1.Name(), Wait: &watch.Wait{}}, }, }) config.Deduplicate.Enabled = true config.Consul = consul.HTTPAddr config.set("consul") config.set("deduplicate") config.set("deduplicate.enabled") config2 := DefaultConfig() config2.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{Source: in.Name(), Destination: out2.Name(), Wait: &watch.Wait{}}, }, }) config2.Deduplicate.Enabled = true config2.Consul = consul.HTTPAddr config2.set("consul") config2.set("deduplicate") config2.set("deduplicate.enabled") // Create the runners r1, err := NewRunner(config, false, false) if err != nil { t.Fatalf("err: %v", err) } go r1.Start() defer r1.Stop() r2, err := NewRunner(config2, false, false) if err != nil { t.Fatalf("err: %v", err) } go r2.Start() defer r2.Stop() // Wait until the output file exists testutil.WaitForResult(func() (bool, error) { _, err := os.Stat(out1.Name()) if err != nil { return false, nil } return true, nil }, func(err error) { t.Fatalf("err: %v", err) }) // Wait until the output file exists testutil.WaitForResult(func() (bool, error) { _, err := os.Stat(out2.Name()) if err != nil { return false, nil } return true, nil }, func(err error) { t.Fatalf("err: %v", err) }) // Should only be a single total watcher total := r1.watcher.Size() + r2.watcher.Size() if total > 1 { t.Fatalf("too many watchers: %d", total) } }