From 415069be302b2cd9636c65b5965645f3238a7bc1 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Sat, 4 Nov 2023 01:35:38 -0700 Subject: [PATCH] *: Add integration tests for examples --- 0-examples/integration_test.go | 140 +++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 0-examples/integration_test.go diff --git a/0-examples/integration_test.go b/0-examples/integration_test.go new file mode 100644 index 0000000..38c9323 --- /dev/null +++ b/0-examples/integration_test.go @@ -0,0 +1,140 @@ +package examples_test + +import ( + "bytes" + "errors" + "io" + "os" + "os/exec" + "testing" + "time" + + "github.com/diamondburned/arikawa/v3/internal/testenv" +) + +func TestExamples(t *testing.T) { + // Assert that the tests only run when the environment variables are set. + testenv.Must(t) + + // Assert that the Go compiler is available. + _, err := exec.LookPath("go") + if err != nil { + t.Skip("skipping test; go compiler not found") + } + + examplePackages, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + + // Run all examples for 10 seconds each. + // + // TODO(diamondburned): find a way to detect that the bot is online. Maybe + // force all examples to print the current username? + const exampleRunDuration = 10 * time.Second + + buildDir, err := os.MkdirTemp("", "arikawa-examples") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + if err := os.RemoveAll(buildDir); err != nil { + t.Log("cannot remove artifacts dir:", err) + } + }) + + for _, pkg := range examplePackages { + if !pkg.IsDir() { + continue + } + + // Assert package main. + if _, err := os.Stat(pkg.Name() + "/main.go"); err != nil { + continue + } + + pkg := pkg + t.Run(pkg.Name(), func(t *testing.T) { + t.Parallel() + + binPath := buildDir + "/" + pkg.Name() + + gobuild := exec.Command("go", "build", "-o", binPath, "./"+pkg.Name()) + gobuild.Stderr = &lineLogger{dst: func(line string) { t.Log("go build:", line) }} + if err := gobuild.Run(); err != nil { + t.Fatal("cannot go build:", err) + } + + timer := time.NewTimer(exampleRunDuration) + t.Cleanup(func() { timer.Stop() }) + + bin := exec.Command(binPath) + bin.Stderr = &lineLogger{dst: func(line string) { t.Log(pkg.Name()+":", line) }} + if err := bin.Start(); err != nil { + t.Fatal("cannot start binary:", err) + } + + cmdDone := make(chan struct{}) + go func() { + defer close(cmdDone) + + err := bin.Wait() + if err == nil { + return // all good + } + + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) || !exitErr.Exited() { + return + } + + t.Error("binary exited with status", exitErr.ExitCode()) + }() + + select { + case <-cmdDone: + return + case <-timer.C: + } + + // Works well. Just exit. + if err := bin.Process.Signal(os.Interrupt); err != nil { + t.Log("cannot interrupt binary:", err) + bin.Process.Kill() + } + + exitTimer := time.NewTimer(5 * time.Second) + t.Cleanup(func() { exitTimer.Stop() }) + + select { + case <-cmdDone: + return + case <-exitTimer.C: + t.Error("example did not exit after 5 seconds") + bin.Process.Kill() + } + }) + } +} + +type lineLogger struct { + dst func(string) + buf bytes.Buffer +} + +func (l *lineLogger) Write(p []byte) (n int, err error) { + n, _ = l.buf.Write(p) + for { + line, err := l.buf.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + return n, err + } + line = line[:len(line)-1] // remove newline + l.dst(line) + } + return n, nil +}