blob: c2bcc99d5bfd680bdc6be441c227b42b0fbf37da [file] [log] [blame]
// Package swarming downloads and runs the swarming_bot.zip code.
package swarming
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/sklog/glog_and_cloud"
"go.skia.org/infra/go/testutils/unittest"
"go.skia.org/infra/go/util"
)
const metadataURL = "https://example.org"
func setBotSwarmingIDEnvVar(t *testing.T, value string) util.CleanupFunc {
originalValue := os.Getenv(SwarmingBotIDEnvVar)
err := os.Setenv(SwarmingBotIDEnvVar, value)
require.NoError(t, err)
cleanup := func() {
err := os.Setenv(SwarmingBotIDEnvVar, originalValue)
require.NoError(t, err)
}
return cleanup
}
func TestNew_CorrectSwarmingURLForRPIBot(t *testing.T) {
unittest.SmallTest(t)
cleanup := setBotSwarmingIDEnvVar(t, "skia-rpi-test")
defer cleanup()
const pythonPath = "/usr/bin/python2.7"
const swarmingBotPath = "/b/s/swarming_bot.zip"
b, err := New(pythonPath, swarmingBotPath, metadataURL)
require.NoError(t, err)
assert.Equal(t, metadataURL, b.metadataURL)
assert.Equal(t, pythonPath, b.pythonExeFilename)
assert.Equal(t, swarmingBotPath, b.swarmingBotZipFilename)
assert.Contains(t, b.swarmingURL, defaultSwarmingServer)
}
func TestNew_CorrectSwarmingURLForInternalBot(t *testing.T) {
unittest.SmallTest(t)
cleanup := setBotSwarmingIDEnvVar(t, "skia-i-rpi-test")
defer cleanup()
const pythonPath = "/usr/bin/python2.7"
const swarmingBotPath = "/b/s/swarming_bot.zip"
b, err := New(pythonPath, swarmingBotPath, metadataURL)
require.NoError(t, err)
assert.Equal(t, metadataURL, b.metadataURL)
assert.Equal(t, pythonPath, b.pythonExeFilename)
assert.Equal(t, swarmingBotPath, b.swarmingBotZipFilename)
assert.Contains(t, b.swarmingURL, internalSwarmingServer)
}
func TestNew_CorrectSwarmingURLForDebugBot(t *testing.T) {
unittest.SmallTest(t)
cleanup := setBotSwarmingIDEnvVar(t, "skia-d-rpi-test")
defer cleanup()
const pythonPath = "/usr/bin/python2.7"
const swarmingBotPath = "/b/s/swarming_bot.zip"
b, err := New(pythonPath, swarmingBotPath, metadataURL)
require.NoError(t, err)
assert.Equal(t, metadataURL, b.metadataURL)
assert.Equal(t, pythonPath, b.pythonExeFilename)
assert.Equal(t, swarmingBotPath, b.swarmingBotZipFilename)
assert.Contains(t, b.swarmingURL, debugSwarmingServer)
}
func TestNew_ErrIfNoSwarmingBotIDEnvVar(t *testing.T) {
unittest.SmallTest(t)
cleanup := setBotSwarmingIDEnvVar(t, "")
defer cleanup()
const pythonPath = "/usr/bin/python2.7"
const swarmingBotPath = "/b/s/swarming_bot.zip"
_, err := New(pythonPath, swarmingBotPath, metadataURL)
require.Error(t, err)
}
type cleanupFunc func()
const swarmingBotFakeContents = `Pretend this is Python code.`
// newBotForTest returns a new *Bot for testing, the full path to the swarming
// bot code, and a cleanup function to call when tests are complete.
//
// Works by starting an httptest.Server and redirecting the Bot URLs to that
// server.
func newBotForTest(t *testing.T, metadataHander, botCodeHandler http.HandlerFunc) (*Bot, string, cleanupFunc) {
// Get a temp dir.
dir, err := ioutil.TempDir("", "swarming")
require.NoError(t, err)
// Create a temp file to stand in for the python executable.
pythonPath := filepath.Join(dir, "python2.7")
f, err := os.Create(pythonPath)
_, err = f.WriteString("A stand-in for Python.")
require.NoError(t, err)
require.NoError(t, f.Close())
// Pick a spot in that dir where the swarming bot code should go. With a
// couple intervening directories to make sure they get created.
swarmingBotPath := filepath.Join(dir, "b", "s", "swarming_bot.py")
// Now launch a local HTTP server that will stand in place for both the
// metadata server and the swarming server.
r := mux.NewRouter()
// This endpoint will pretend to be the metadata server.
r.HandleFunc("/metadata", metadataHander)
// This endpoint will pretend to be the swarming server.
r.HandleFunc("/bot_code", botCodeHandler)
envCleanup := setBotSwarmingIDEnvVar(t, "skia-rpi-test")
httpTestServer := httptest.NewServer(r)
cleanup := func() {
httpTestServer.Close()
envCleanup()
}
bot, err := New(pythonPath, swarmingBotPath, metadataURL)
require.NoError(t, err)
// Swap out the URLs for ones that point at our local HTTP server.
bot.metadataURL = httpTestServer.URL + "/metadata"
bot.swarmingURL = httpTestServer.URL + "/bot_code"
return bot, swarmingBotPath, cleanup
}
// newBotForTestWithSuccessHandlers is just like newBotForTest, but we also
// automatically set the handlers for the happy path.
func newBotForTestWithSuccessHandlers(t *testing.T) (*Bot, cleanupFunc) {
metadataHandler := func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(`{"access_token":"123"}`))
assert.NoError(t, err)
}
botCodeHandler := func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(swarmingBotFakeContents))
assert.NoError(t, err)
}
bot, _, cleanup := newBotForTest(t, metadataHandler, botCodeHandler)
return bot, cleanup
}
func TestBootstrap_Success(t *testing.T) {
unittest.SmallTest(t)
metadataHandler := func(w http.ResponseWriter, r *http.Request) {
// Confirm that the flavor header was sent.
assert.Equal(t, "Google", r.Header.Get("Metadata-Flavor"))
_, err := w.Write([]byte(`{"access_token":"123"}`))
assert.NoError(t, err)
}
botCodeHandler := func(w http.ResponseWriter, r *http.Request) {
// Confirm that the authorization header was sent correctly.
assert.Equal(t, "Bearer 123", r.Header.Get("Authorization"))
_, err := w.Write([]byte(swarmingBotFakeContents))
assert.NoError(t, err)
}
bot, swarmingBotPath, cleanup := newBotForTest(t, metadataHandler, botCodeHandler)
defer cleanup()
require.NoError(t, bot.bootstrap(context.Background()))
// Confirm that we downloaded the swarming bot contents correctly.
b, err := ioutil.ReadFile(swarmingBotPath)
require.NoError(t, err)
assert.Equal(t, swarmingBotFakeContents, string(b))
}
func TestBootstrap_ErrOnMetadataRequestFail(t *testing.T) {
unittest.SmallTest(t)
metadataHandler := func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
botCodeHandler := func(w http.ResponseWriter, r *http.Request) {
assert.Fail(t, "Should never get here.")
}
bot, _, cleanup := newBotForTest(t, metadataHandler, botCodeHandler)
defer cleanup()
err := bot.bootstrap(context.Background())
assert.Contains(t, err.Error(), "Metadata bad status code")
}
func TestBootstrap_ErrOnMetadataResponseNotJSON(t *testing.T) {
unittest.SmallTest(t)
metadataHandler := func(w http.ResponseWriter, r *http.Request) {
// Confirm that the flavor header was sent.
assert.Equal(t, "Google", r.Header.Get("Metadata-Flavor"))
_, err := w.Write([]byte(`This is not valid JSON.`))
assert.NoError(t, err)
}
botCodeHandler := func(w http.ResponseWriter, r *http.Request) {
assert.Fail(t, "Should never get here.")
}
bot, _, cleanup := newBotForTest(t, metadataHandler, botCodeHandler)
defer cleanup()
err := bot.bootstrap(context.Background())
assert.Contains(t, err.Error(), "Failed to decode metadata")
}
func TestBootstrap_ErrOnSwarmingRequestFail(t *testing.T) {
unittest.SmallTest(t)
metadataHandler := func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(`{"access_token":"123"}`))
assert.NoError(t, err)
}
botCodeHandler := func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
bot, _, cleanup := newBotForTest(t, metadataHandler, botCodeHandler)
defer cleanup()
err := bot.bootstrap(context.Background())
assert.Contains(t, err.Error(), "Swarming server bad status code")
}
const swarmingbotFakeStderrOutput = "This is a line of logging output."
func TestRunSwarmingCommand_ReturnsNilOnExitCodeZero(t *testing.T) {
unittest.SmallTest(t)
bot, cleanup := newBotForTestWithSuccessHandlers(t)
defer cleanup()
// Swap out execCommandContext with an executable that we know exists and
// returns a zero exit code. See
// https://npf.io/2015/06/testing-exec-command/ for details on how this
// works.
execCommandContext = fakeExecCommandContext_ExitCodeZero
defer func() {
execCommandContext = exec.CommandContext
}()
// We also want to test that capturing the stderr output of swarming is
// captured and emitted as sklog lines. To captures sklog we need to capture
// this programs stderr output.
// First create a temp file.
f, err := ioutil.TempFile("", "swarming")
require.NoError(t, err)
// Now swap out os.Stderr with our file.
tmp := os.Stderr
os.Stderr = f
// Swap everything back in place when we leave.
defer func() {
os.Stderr = tmp
util.Close(f)
err := os.Remove(f.Name())
assert.NoError(t, err)
}()
// Now tell sklog to emit to stderr.
glog_and_cloud.SetLogger(glog_and_cloud.NewStdErrCloudLogger(glog_and_cloud.SLogStderr))
ctx := context.Background()
// Run bootstrap so everything is in place for calling runSwarmingCommand.
err = bot.bootstrap(ctx)
require.NoError(t, err)
err = bot.runSwarmingCommand(ctx)
require.NoError(t, err)
// Check the output of sklog.
_, err = f.Seek(0, 0)
require.NoError(t, err)
b, err := ioutil.ReadAll(f)
require.NoError(t, err)
assert.Contains(t, string(b), swarmingbotFakeStderrOutput) // See TestFakeSwarmingExecutable_ExitCodeZero
}
// An exec.CommandContext fake that actually executes another test in this file
// TestFakeSwarmingExecutable_ExitCodeZero instead of the requested exe.
func fakeExecCommandContext_ExitCodeZero(ctx context.Context, command string, args ...string) *exec.Cmd {
extendedArgs := []string{"-test.run=TestFakeSwarmingExecutable_ExitCodeZero", "--", command}
extendedArgs = append(extendedArgs, args...)
cmd := exec.CommandContext(ctx, os.Args[0], extendedArgs...)
cmd.Env = []string{"EMULATE_SWARMING_BOT_EXECUTABLE=1"}
return cmd
}
// TestFakeSwarmingExecutable_ExitCodeZero is used by fakeExecCommandContext_ExitCodeZero.
func TestFakeSwarmingExecutable_ExitCodeZero(t *testing.T) {
unittest.SmallTest(t)
if os.Getenv("EMULATE_SWARMING_BOT_EXECUTABLE") != "1" {
return
}
// Confirm the args are getting passed along.
if os.Args[len(os.Args)-1] != "start_bot" {
sklog.Fatal("Missing start_bot in os.Args.")
}
// Printf on stderr, which should appear in the callers logs.
fmt.Fprintf(os.Stderr, swarmingbotFakeStderrOutput)
os.Exit(0)
}
const nonZeroExitCode = 17
func TestRunSwarmingCommand_ReturnsErrorOnNonZeroExitCode(t *testing.T) {
unittest.SmallTest(t)
bot, cleanup := newBotForTestWithSuccessHandlers(t)
defer cleanup()
// Swap out execCommandContext with an executable that we know exists and
// returns a non-zero exit code. See
// https://npf.io/2015/06/testing-exec-command/ for details on how this
// works.
execCommandContext = fakeExecCommandContext_ExitCodeNonZero
defer func() {
execCommandContext = exec.CommandContext
}()
ctx := context.Background()
err := bot.bootstrap(ctx)
require.NoError(t, err)
err = bot.runSwarmingCommand(ctx)
assert.Contains(t, err.Error(), fmt.Sprintf("exit status %d", nonZeroExitCode))
}
// An exec.CommandContext fake that actually executes another test in this file
// TestFakeSwarmingExecutable_ExitCodeNonZero instead of the requested exe.
func fakeExecCommandContext_ExitCodeNonZero(ctx context.Context, command string, args ...string) *exec.Cmd {
extendedArgs := []string{"-test.run=TestFakeSwarmingExecutable_ExitCodeNonZero", "--", command}
extendedArgs = append(extendedArgs, args...)
cmd := exec.CommandContext(ctx, os.Args[0], extendedArgs...)
cmd.Env = []string{"EMULATE_SWARMING_BOT_EXECUTABLE=1"}
return cmd
}
// TestFakeSwarmingExecutable_ExitCodeNonZero is used by fakeExecCommandContext_ExitCodeNonZero.
func TestFakeSwarmingExecutable_ExitCodeNonZero(t *testing.T) {
unittest.SmallTest(t)
if os.Getenv("EMULATE_SWARMING_BOT_EXECUTABLE") != "1" {
return
}
os.Exit(nonZeroExitCode)
}
func TestLaunch_IsCancellable(t *testing.T) {
unittest.SmallTest(t)
// Create a new Bot.
bot, cleanup := newBotForTestWithSuccessHandlers(t)
defer cleanup()
// Call bootstrap now so when we do call Launch() it won't call bootstrap
// and we'll fall directly into the for {} loop.
err := bot.bootstrap(context.Background())
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
cancel()
err = bot.Launch(ctx)
assert.Contains(t, err.Error(), "Context was cancelled")
}