tstest/integration/vms: codegen for top level tests (#2441)

This moves the distribution definitions into a maintainable hujson file
instead of just existing as constants in `distros.go`. Comments are
maintained from the inline definitions.

This uses jennifer[1] for hygenic source tree creation. This allows us
to generate a unique top-level test for each VM run. This should
hopefully help make the output of `go test` easier to read.

This also separates each test out into its own top-level test so that we
can better track the time that each distro takes. I really wish there
was a way to have the `test_codegen.go` file _always_ run as a part of
the compile process instead of having to rely on people remembering to
run `go generate`, but I am limited by my tools.

This will let us remove the `-distro-regex` flag and use `go test -run`
to pick which distros are run.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
This commit is contained in:
Christine Dodrill
2021-07-16 15:25:16 -04:00
committed by GitHub
parent 391207bbcf
commit 798b0da470
11 changed files with 527 additions and 154 deletions

View File

@@ -18,6 +18,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"testing"
"text/template"
"time"
@@ -57,14 +58,14 @@ func TestDownloadImages(t *testing.T) {
bins := integration.BuildTestBinaries(t)
for _, d := range distros {
for _, d := range Distros {
distro := d
t.Run(distro.name, func(t *testing.T) {
if !distroRex.Unwrap().MatchString(distro.name) {
t.Skipf("distro name %q doesn't match regex: %s", distro.name, distroRex)
t.Run(distro.Name, func(t *testing.T) {
if !distroRex.Unwrap().MatchString(distro.Name) {
t.Skipf("distro name %q doesn't match regex: %s", distro.Name, distroRex)
}
if strings.HasPrefix(distro.name, "nixos") {
if strings.HasPrefix(distro.Name, "nixos") {
t.Skip("NixOS is built on the fly, no need to download it")
}
@@ -98,7 +99,7 @@ func mkLayeredQcow(t *testing.T, tdir string, d Distro, qcowBase string) {
run(t, tdir, "qemu-img", "create",
"-f", "qcow2",
"-o", "backing_file="+qcowBase,
filepath.Join(tdir, d.name+".qcow2"),
filepath.Join(tdir, d.Name+".qcow2"),
)
}
@@ -112,7 +113,7 @@ var (
func mkSeed(t *testing.T, d Distro, sshKey, hostURL, tdir string, port int) {
t.Helper()
dir := filepath.Join(tdir, d.name, "seed")
dir := filepath.Join(tdir, d.Name, "seed")
os.MkdirAll(dir, 0700)
// make meta-data
@@ -127,7 +128,7 @@ func mkSeed(t *testing.T, d Distro, sshKey, hostURL, tdir string, port int) {
Hostname string
}{
ID: "31337",
Hostname: d.name,
Hostname: d.Name,
})
if err != nil {
t.Fatal(err)
@@ -156,7 +157,7 @@ func mkSeed(t *testing.T, d Distro, sshKey, hostURL, tdir string, port int) {
}{
SSHKey: strings.TrimSpace(sshKey),
HostURL: hostURL,
Hostname: d.name,
Hostname: d.Name,
Port: port,
InstallPre: d.InstallPre(),
Password: securePassword,
@@ -220,10 +221,11 @@ func getProbablyFreePortNumber() (int, error) {
return portNum, nil
}
// TestVMIntegrationEndToEnd creates a virtual machine with qemu, installs
// tailscale on it and then ensures that it connects to the network
// successfully.
func TestVMIntegrationEndToEnd(t *testing.T) {
func setupTests(t *testing.T) {
ramsem.once.Do(func() {
ramsem.sem = semaphore.NewWeighted(int64(*vmRamLimit))
})
if !*runVMTests {
t.Skip("not running integration tests (need --run-vm-tests)")
}
@@ -239,56 +241,53 @@ func TestVMIntegrationEndToEnd(t *testing.T) {
t.Logf("hint: nix-shell -p go -p qemu -p cdrkit --run 'go test --v --timeout=60m --run-vm-tests'")
t.Fatalf("missing dependency: %v", err)
}
}
ramsem := semaphore.NewWeighted(int64(*vmRamLimit))
rex := distroRex.Unwrap()
var ramsem struct {
once sync.Once
sem *semaphore.Weighted
}
t.Run("do", func(t *testing.T) {
for n, distro := range distros {
n, distro := n, distro
if rex.MatchString(distro.name) {
t.Logf("%s matches %s", distro.name, rex)
} else {
continue
func testOneDistribution(t *testing.T, n int, distro Distro) {
setupTests(t)
if distroRex.Unwrap().MatchString(distro.Name) {
t.Logf("%s matches %s", distro.Name, distroRex.Unwrap())
} else {
t.Skip("regex not matched")
}
ctx, done := context.WithCancel(context.Background())
t.Cleanup(done)
h := newHarness(t)
dir := t.TempDir()
err := ramsem.sem.Acquire(ctx, int64(distro.MemoryMegs))
if err != nil {
t.Fatalf("can't acquire ram semaphore: %v", err)
}
t.Cleanup(func() { ramsem.sem.Release(int64(distro.MemoryMegs)) })
h.mkVM(t, n, distro, h.pubKey, h.loginServerURL, dir)
var ipm ipMapping
t.Run("wait-for-start", func(t *testing.T) {
waiter := time.NewTicker(time.Second)
defer waiter.Stop()
var ok bool
for {
<-waiter.C
h.ipMu.Lock()
if ipm, ok = h.ipMap[distro.Name]; ok {
h.ipMu.Unlock()
break
}
t.Run(distro.name, func(t *testing.T) {
ctx, done := context.WithCancel(context.Background())
t.Cleanup(done)
t.Parallel()
h := newHarness(t)
dir := t.TempDir()
err := ramsem.Acquire(ctx, int64(distro.mem))
if err != nil {
t.Fatalf("can't acquire ram semaphore: %v", err)
}
defer ramsem.Release(int64(distro.mem))
h.mkVM(t, n, distro, h.pubKey, h.loginServerURL, dir)
var ipm ipMapping
t.Run("wait-for-start", func(t *testing.T) {
waiter := time.NewTicker(time.Second)
defer waiter.Stop()
var ok bool
for {
<-waiter.C
h.ipMu.Lock()
if ipm, ok = h.ipMap[distro.name]; ok {
h.ipMu.Unlock()
break
}
h.ipMu.Unlock()
}
})
h.testDistro(t, distro, ipm)
})
h.ipMu.Unlock()
}
})
h.testDistro(t, distro, ipm)
}
func (h *Harness) testDistro(t *testing.T, d Distro, ipm ipMapping) {
@@ -339,7 +338,7 @@ func (h *Harness) testDistro(t *testing.T, d Distro, ipm ipMapping) {
&expect.BExp{R: `(\#)`},
}
switch d.initSystem {
switch d.InitSystem {
case "openrc":
// NOTE(Xe): this is a sin, however openrc doesn't really have the concept
// of service readiness. If this sleep is removed then tailscale will not be