blob: 154f0ac0c10ba58bfcf4d7e61f32efeef2d2fc3f [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package subprocess
import (
"bytes"
"context"
"errors"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"go.fuchsia.dev/fuchsia/tools/lib/clock"
)
func TestRun(t *testing.T) {
t.Run("Run", func(t *testing.T) {
t.Run("should execute a command", func(t *testing.T) {
r := Runner{
Env: []string{"FOO=bar"}, // Cover env var handling.
}
message := "Hello, World!"
command := []string{"echo", message}
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
if err := r.Run(context.Background(), command, stdout, stderr); err != nil {
t.Fatal(err)
}
stdoutS := strings.TrimSpace(stdout.String())
if stdoutS != message {
t.Fatalf("Expected output %q, but got %q", message, stdoutS)
}
stderrS := strings.TrimSpace(stderr.String())
if stderrS != "" {
t.Fatalf("Expected empty stderr, but got %q", stderrS)
}
})
t.Run("should error if the context completes before the command", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
r := Runner{}
command := []string{"sleep", "5"}
err := r.Run(ctx, command, nil, nil)
if err == nil {
t.Fatal("Expected sleep command to terminate early but it completed")
} else if !errors.Is(err, ctx.Err()) {
t.Fatalf("Expected Run() to return a context error after cancelation but got: %s", err)
}
})
t.Run("should return an error if the command fails", func(t *testing.T) {
r := Runner{}
command := []string{"not_a_command_12345"}
err := r.Run(context.Background(), command, nil, nil)
if err == nil {
t.Fatalf("Expected invalid command to fail but it succeeded: %s", err)
} else if !errors.Is(err, exec.ErrNotFound) {
t.Fatalf("Expected Run() to return exec.ErrNotFound but got: %s", err)
}
})
t.Run("should wait for command to finish after sending SIGTERM", func(t *testing.T) {
script := filepath.Join(t.TempDir(), "script")
// The script below will print `start` to signify that it's ready to handle
// SIGTERMs and SIGINTs and then run cleanup() when it receives the signal.
// By checking that `start` is printed before canceling the context and checking
// that `finished` is printed after, we can assert that the cleanup() function
// was run before the script exited.
if err := ioutil.WriteFile(script, []byte(
`#!/bin/bash
cleanup() {
echo "finished"; exit 1
}
trap cleanup TERM INT
echo "start"
while true;do :; done`,
), os.ModePerm); err != nil {
t.Fatalf("Failed to write script: %s", err)
}
r := Runner{}
command := []string{script}
stdoutReader, stdout := io.Pipe()
defer stdoutReader.Close()
defer stdout.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
buf := make([]byte, 20)
// Wait for script to print `start` before calling cancel() to know
// that it's ready to handle the SIGTERM.
if _, err := stdoutReader.Read(buf); err != nil || !bytes.Contains(buf, []byte("start")) {
t.Errorf("Failed to read `start` from stdout: %s, got: %s", err, string(buf))
}
cancel()
// After sending the SIGTERM, check that the script ran cleanup() and
// printed `finished`.
buf = make([]byte, 20)
if _, err := stdoutReader.Read(buf); err != nil || !bytes.Contains(buf, []byte("finished")) {
t.Errorf("Failed to read `finished` from stdout: %s, got: %s", err, string(buf))
}
}()
if err := r.Run(ctx, command, stdout, stdout); err == nil {
t.Errorf("Expected script to terminate early but it completed successfully")
} else {
if !errors.Is(err, context.Canceled) {
t.Errorf("Expected Run() to return context.Canceled but got: %s", err)
}
}
})
t.Run("should kill command if it doesn't terminate after sending SIGTERM", func(t *testing.T) {
if runtime.GOOS == "darwin" {
// Setting the pgid doesn't work on Mac OS, so this test
// will hang because it can't kill the sleep process.
// TODO(fxbug.dev/86162): Enable if we can find a way to kill
// the child processes.
t.Skip("Skipping on Mac because setting pgid doesn't work")
}
script := filepath.Join(t.TempDir(), "script")
// The script below will print `start` to signify that it's ready to handle
// SIGTERMs and SIGINTs and then run cleanup() when it receives the signal.
// However, since it starts a `sleep` process, it actually waits for the process
// to finish before entering cleanup(). This will test that the process group
// gets killed if it can't clean up and exit in time. In this test, `finished`
// should not be in the output because the process would have been killed before
// it could run cleanup().
if err := ioutil.WriteFile(script, []byte(
`#!/bin/bash
cleanup() {
echo "finished"; exit 1
}
trap cleanup TERM INT
echo "start"
sleep 10000`,
), os.ModePerm); err != nil {
t.Fatalf("Failed to write script: %s", err)
}
r := Runner{}
command := []string{script}
stdoutReader, stdout := io.Pipe()
defer stdoutReader.Close()
defer stdout.Close()
fakeClock := clock.NewFakeClock()
ctx := clock.NewContext(context.Background(), fakeClock)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
buf := make([]byte, 20)
if _, err := stdoutReader.Read(buf); err != nil || !bytes.Contains(buf, []byte("start")) {
t.Errorf("Failed to read `start` from stdout: %s, got: %s", err, string(buf))
}
cancel()
// Wait for After() to be called before advancing the clock.
<-fakeClock.AfterCalledChan()
fakeClock.Advance(cleanupGracePeriod + time.Second)
// The script should be killed before it reaches cleanup(), so `finished`
// should NOT have been printed to stdout. No need to check the err from
// Read() because we don't expect the script to print anything more to
// stdout, so it should block until the deferred stdout.Close() gets
// executed.
buf = make([]byte, 20)
stdoutReader.Read(buf)
if bytes.Contains(buf, []byte("finished")) {
t.Errorf("Expected script to be killed without doing cleanup")
}
}()
if err := r.RunWithStdin(ctx, command, stdout, stdout, nil); err == nil {
t.Errorf("Expected script to terminate early but it completed successfully")
} else {
if !errors.Is(err, context.Canceled) {
t.Errorf("Expected Run() to return context.Canceled but got: %s", err)
}
}
})
})
}