Michael Stapelberg 5a07d6696d split integration tests into multiple packages
This makes them complete more quickly (because they are run in parallel) and
invalidates only the cache for the integration test I’m working on, not for all
of them.
2018-06-24 11:46:49 +02:00

146 lines
3.4 KiB
Go

// Package dnsmasq manages the process lifecycle of the dnsmasq(8) DHCP server.
package dnsmasq
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
"testing"
"time"
)
// Process is a handle for a dnsmasq(8) process.
type Process struct {
killed bool // whether Kill was called
done chan struct{} // closed when Done() is called
wait chan struct{} // closed when Wait() returns
dnsmasq *exec.Cmd
mu sync.Mutex
actions []string
}
var dhcpActionRe = regexp.MustCompile(` (DHCP[^(]+\(.*)$`)
// Run starts a dnsmasq(8) process and returns a handle to it.
func Run(t *testing.T, iface, ns string) *Process {
ready, err := ioutil.TempFile("", "router7")
if err != nil {
t.Fatal(err)
}
ready.Close() // dnsmasq will re-create the file anyway
defer os.Remove(ready.Name()) // dnsmasq does not clean up its pid file
// iface, err := net.InterfaceByName("veth0a")
// if err != nil {
// t.Fatal(err)
// }
p := &Process{
wait: make(chan struct{}),
}
p.dnsmasq = exec.Command("ip", "netns", "exec", ns, "dnsmasq",
"--keep-in-foreground", // cannot use --no-daemon because we need --pid-file
"--log-facility=-", // log to stderr
"--pid-file="+ready.Name(),
"--bind-interfaces",
"--interface="+iface,
"--dhcp-range=192.168.23.2,192.168.23.10",
"--dhcp-range=::1,::10,constructor:"+iface,
"--dhcp-authoritative", // eliminate timeouts
"--no-ping", // disable ICMP confirmation of unused addresses to eliminate tedious timeout
"--leasefile-ro", // do not create a lease database
)
p.dnsmasq.Stdout = os.Stdout
stderr := make(chan string)
r, w := io.Pipe()
scanner := bufio.NewScanner(r)
go func() {
for scanner.Scan() {
stderr <- scanner.Text()
}
close(stderr)
}()
p.dnsmasq.Stderr = w
//mac := iface.HardwareAddr.String()
go func() {
for line := range stderr {
fmt.Printf("dnsmasq log line: %s\n", line)
if !strings.HasPrefix(line, "dnsmasq-dhcp") {
continue
}
// if !strings.Contains(line, mac) {
// continue
// }
matches := dhcpActionRe.FindStringSubmatch(line)
if matches == nil {
continue
}
p.mu.Lock()
p.actions = append(p.actions, matches[1])
p.mu.Unlock()
}
}()
if err := p.dnsmasq.Start(); err != nil {
t.Fatal(err)
}
p.done = make(chan struct{})
go func() {
err := p.dnsmasq.Wait()
close(p.wait)
select {
case <-p.done:
return // test done, any errors are from our Kill()
default:
t.Fatalf("dnsmasq exited prematurely: %v", err)
}
}()
// TODO(later): use inotify instead of polling
// Wait for dnsmasq to write its process id, at which point it is already
// listening for requests.
for {
b, err := ioutil.ReadFile(ready.Name())
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(b)) == strconv.Itoa(p.dnsmasq.Process.Pid) {
break
}
time.Sleep(10 * time.Millisecond)
}
return p
}
// Kill shuts down the dnsmasq(8) process and returns once waitpid returns.
func (p *Process) Kill() {
if p.killed {
return
}
p.killed = true
close(p.done)
p.dnsmasq.Process.Kill()
<-p.wait
}
// Actions returns a string slice of dnsmasq(8) actions (as per its stderr log)
// received up until now. Use Kill before calling Actions to force a log flush.
func (p *Process) Actions() []string {
p.mu.Lock()
defer p.mu.Unlock()
result := make([]string, len(p.actions))
copy(result, p.actions)
return result
}