Repository: tetratelabs/getenvoy
Pull request #272 (2021-06-09 02:30:03)

Ports capture script to go for windows
This ports the shell script to a real binary. This will allow more realistic tests and also things to work naturally on windows

See #105

codefromthecrypt: I'm noticing we do a lot of unnecessary work that smells like we distrust go's `exec` package (ex writing back the cwd etc). I will remove that
codefromthecrypt: ok simpler now
codefromthecrypt: found more duplicated code.. pruning now..

Patch:
diff --git a/internal/cmd/run.go b/internal/cmd/run.go
index 263feb6..c9ba2aa 100644
--- a/internal/cmd/run.go
+++ b/internal/cmd/run.go
@@ -36,20 +36,22 @@ func NewRunCmd(o *globals.GlobalOpts) *cli.Command {
 	var envoyVersion string
 	cmd := &cli.Command{
 		Name:            "run",
-		Usage:           "Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C",
+		Usage:           "Run Envoy with the given [arguments...] until interrupted",
 		ArgsUsage:       "[arguments...]",
 		SkipFlagParsing: true,
-		Description: `To run Envoy, execute ` + "`getenvoy run -c your_envoy_config.yaml`" + `. This
-downloads and installs the latest version of Envoy for you.
+		Description: `To run Envoy, execute ` + "`getenvoy run -c your_envoy_config.yaml`" + `.
 
-Envoy runs in the current directory and interprets the '[arguments...]'.
 The first version in the below is run, controllable by the "use" command:
 ` + fmt.Sprintf("```\n%s\n```", envoy.VersionUsageList()) + `
+The version to use is downloaded and installed, if necessary.
 
-While Envoy is running, the run directory (` + "`$GETENVOY_HOME/runs/$epochtime`" + `)
-includes minimally "stdout.log" and "stderr.log". On Ctrl-C, shutdown hooks
-write troubleshooting files, including admin endpoints, network and process
-state. Upon exit, this archives as ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`",
+Envoy interprets the '[arguments...]' and runs in the current working
+directory (aka $CWD) until getenvoy is interrupted (ex Ctrl+C, Ctrl+Break).
+
+Console output is captured as "stdout.log" and "stderr.log" in the run
+directory (` + "`$GETENVOY_HOME/runs/$epochtime`" + `). When interrupted,
+shutdown hooks write files, including admin endpoints, network and process
+state. Upon exit, these zip into ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`",
 		Before: func(c *cli.Context) error {
 			if err := os.MkdirAll(o.HomeDir, 0750); err != nil {
 				return NewValidationError(err.Error())
diff --git a/internal/cmd/use.go b/internal/cmd/use.go
index 354abbf..039ebed 100644
--- a/internal/cmd/use.go
+++ b/internal/cmd/use.go
@@ -28,7 +28,7 @@ import (
 func NewUseCmd(o *globals.GlobalOpts) *cli.Command {
 	return &cli.Command{
 		Name:      "use",
-		Usage:     `Sets the current [version] used by the "run" command, installing as necessary`,
+		Usage:     `Sets the current [version] used by the "run" command`,
 		ArgsUsage: "[version]",
 		Description: fmt.Sprintf(`The '[version]' is from the "versions -a" command.
 The Envoy [version] installs on-demand into $GETENVOY_HOME/versions/[version]
diff --git a/internal/envoy/runtime.go b/internal/envoy/runtime.go
index f1d2bf0..f1e422a 100644
--- a/internal/envoy/runtime.go
+++ b/internal/envoy/runtime.go
@@ -50,7 +50,7 @@ type Runtime struct {
 
 	adminAddress, adminAddressPath string
 
-	// FakeInterrupt is exposed for unit tests to pretend "getenvoy run" received an interrupt or a ctrl-c.
+	// FakeInterrupt is exposed for unit tests to pretend "getenvoy run" received a Ctrl+C or Ctrl+Break.
 	// End-to-end tests should kill the getenvoy process to achieve the same.
 	FakeInterrupt context.CancelFunc
 
diff --git a/site/usage.md b/site/usage.md
index 56d9d7a..5d4375b 100644
--- a/site/usage.md
+++ b/site/usage.md
@@ -15,9 +15,9 @@ your JSON first: https://getenvoy.io/envoy-versions-schema.json
 | Name | Usage |
 | ---- | ----- |
 | help | Shows how to use a [command] |
-| run | Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C |
+| run | Run Envoy with the given [arguments...] until interrupted |
 | versions | List Envoy versions |
-| use | Sets the current [version] used by the "run" command, installing as necessary |
+| use | Sets the current [version] used by the "run" command |
 | --version, -v | Print the version of GetEnvoy |
 
 # Environment Variables


Test patch:
diff --git a/internal/cmd/run_test.go b/internal/cmd/run_test.go
index ea7ad71..2e33d72 100644
--- a/internal/cmd/run_test.go
+++ b/internal/cmd/run_test.go
@@ -16,6 +16,7 @@ package cmd_test
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -24,53 +25,42 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+	"github.com/urfave/cli/v2"
 
+	rootcmd "github.com/tetratelabs/getenvoy/internal/cmd"
 	"github.com/tetratelabs/getenvoy/internal/globals"
+	"github.com/tetratelabs/getenvoy/internal/test"
 	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
+// Runner allows us to not introduce dependency cycles on envoy.Runtime
+type runner struct {
+	c *cli.App
+}
+
+func (r *runner) Run(ctx context.Context, args []string) error {
+	return r.c.RunContext(ctx, args)
+}
+
+// TestGetEnvoyRun executes envoy then cancels the context. This results in no stdout
 func TestGetEnvoyRun(t *testing.T) {
-	tests := []struct {
-		name              string
-		args              []string
-		expectedEnvoyArgs string
-	}{
-		{
-			name: "no envoy args",
-			args: []string{"getenvoy", "run"},
-		},
-		{
-			name:              "envoy args",
-			args:              []string{"getenvoy", "run", "-c", "envoy.yaml"},
-			expectedEnvoyArgs: `-c envoy.yaml `,
-		},
-	}
+	o, cleanup := setupTest(t)
+	defer cleanup()
 
-	for _, tc := range tests {
-		tc := tc // pin! see https://github.com/kyoh86/scopelint for why
-
-		t.Run(tc.name, func(t *testing.T) {
-			o, cleanup := setupTest(t)
-			defer cleanup()
-
-			c, stdout, stderr := newApp(o)
-			o.Out = io.Discard // don't verify logging
-			err := c.Run(tc.args)
-
-			// Verify the command invoked, passing the correct default commandline
-			require.NoError(t, err)
-
-			// We expect getenvoy to print the context it will run, and Envoy to execute the same, except adding the
-			// --admin-address-path flag
-			expectedEnvoyArgs := fmt.Sprint(tc.expectedEnvoyArgs, "--admin-address-path ", filepath.Join(o.RunDir, "admin-address.txt"))
-			expectedStdout := fmt.Sprintf(`starting: %[1]s %[2]s
-envoy bin: %[1]s
-envoy args: %[2]s`, o.EnvoyPath, expectedEnvoyArgs)
-			require.Equal(t, expectedStdout+"\n", stdout.String())
-			require.Equal(t, "envoy stderr\n", stderr.String())
-		})
-	}
+	stdout := new(bytes.Buffer)
+	stderr := new(bytes.Buffer)
+	c := rootcmd.NewApp(o)
+
+	args := []string{"getenvoy", "run", "-c", "envoy.yaml"}
+	// tee the error stream so we can look for the "starting main dispatch loop" line without consuming it.
+	errCopy := new(bytes.Buffer)
+	c.ErrWriter = io.MultiWriter(stderr, errCopy)
+	err := test.RequireRun(t, nil, &runner{c}, errCopy, args...)
+
+	require.NoError(t, err)
+	require.Empty(t, stdout)
+	require.Equal(t, "initializing epoch 0\nstarting main dispatch loop\n", stderr.String())
 }
 
 func TestGetEnvoyRun_TeesConsoleToLogs(t *testing.T) {
@@ -80,9 +70,7 @@ func TestGetEnvoyRun_TeesConsoleToLogs(t *testing.T) {
 	c, stdout, stderr := newApp(o)
 	o.Out = io.Discard         // stdout/stderr only includes what envoy writes, not our status messages
 	o.DontArchiveRunDir = true // we need to read-back the log files
-	err := c.Run([]string{"getenvoy", "run"})
-
-	require.NoError(t, err)
+	runWithoutConfig(t, c)
 
 	have, err := ioutil.ReadFile(filepath.Join(o.RunDir, "stdout.log"))
 	require.NoError(t, err)
@@ -104,7 +92,7 @@ func TestGetEnvoyRun_ReadsHomeVersionFile(t *testing.T) {
 	require.NoError(t, os.WriteFile(filepath.Join(o.HomeDir, "version"), []byte(version.LastKnownEnvoy), 0600))
 
 	c, _, _ := newApp(o)
-	require.NoError(t, c.Run([]string{"getenvoy", "run"}))
+	runWithoutConfig(t, c)
 
 	// No implicit lookup
 	require.NotContains(t, o.Out.(*bytes.Buffer).String(), "looking up latest version\n")
@@ -121,7 +109,7 @@ func TestGetEnvoyRun_CreatesHomeVersionFile(t *testing.T) {
 	require.NoError(t, os.RemoveAll(o.HomeDir))
 
 	c, _, _ := newApp(o)
-	require.NoError(t, c.Run([]string{"getenvoy", "run"}))
+	runWithoutConfig(t, c)
 
 	// We logged the implicit lookup
 	require.Contains(t, o.Out.(*bytes.Buffer).String(), "looking up latest version\n")
@@ -129,6 +117,11 @@ func TestGetEnvoyRun_CreatesHomeVersionFile(t *testing.T) {
 	require.Equal(t, version.LastKnownEnvoy, o.EnvoyVersion)
 }
 
+// runWithoutConfig intentionally has envoy quit. This allows tests to not have to interrupt envoy to proceed.
+func runWithoutConfig(t *testing.T, c *cli.App) {
+	require.EqualError(t, c.Run([]string{"getenvoy", "run"}), "envoy exited with status: 1")
+}
+
 func TestGetEnvoyRun_ValidatesHomeVersion(t *testing.T) {
 	o, cleanup := setupTest(t)
 	o.Out = new(bytes.Buffer)
diff --git a/internal/cmd/testdata/getenvoy_help.txt b/internal/cmd/testdata/getenvoy_help.txt
index 337c7b4..5a603a9 100644
--- a/internal/cmd/testdata/getenvoy_help.txt
+++ b/internal/cmd/testdata/getenvoy_help.txt
@@ -18,9 +18,9 @@ VERSION:
 
 COMMANDS:
    help      Shows how to use a [command]
-   run       Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C
+   run       Run Envoy with the given [arguments...] until interrupted
    versions  List Envoy versions
-   use       Sets the current [version] used by the "run" command, installing as necessary
+   use       Sets the current [version] used by the "run" command
 
 GLOBAL OPTIONS:
    --home-dir value            GetEnvoy home directory (location of installed versions and run archives) (default: ${HOME}/.getenvoy) [$GETENVOY_HOME]
diff --git a/internal/cmd/testdata/getenvoy_run_help.txt b/internal/cmd/testdata/getenvoy_run_help.txt
index 2b5746d..b2b9242 100644
--- a/internal/cmd/testdata/getenvoy_run_help.txt
+++ b/internal/cmd/testdata/getenvoy_run_help.txt
@@ -1,20 +1,22 @@
 NAME:
-   getenvoy run - Run Envoy with the given [arguments...], running shutdown hooks on Ctrl-C
+   getenvoy run - Run Envoy with the given [arguments...] until interrupted
 
 USAGE:
    getenvoy run [arguments...]
 
 DESCRIPTION:
-   To run Envoy, execute `getenvoy run -c your_envoy_config.yaml`. This
-   downloads and installs the latest version of Envoy for you.
+   To run Envoy, execute `getenvoy run -c your_envoy_config.yaml`.
    
-   Envoy runs in the current directory and interprets the '[arguments...]'.
    The first version in the below is run, controllable by the "use" command:
    ```
    $ENVOY_VERSION, $PWD/.envoy-version, $GETENVOY_HOME/version
    ```
+   The version to use is downloaded and installed, if necessary.
    
-   While Envoy is running, the run directory (`$GETENVOY_HOME/runs/$epochtime`)
-   includes minimally "stdout.log" and "stderr.log". On Ctrl-C, shutdown hooks
-   write troubleshooting files, including admin endpoints, network and process
-   state. Upon exit, this archives as `$GETENVOY_HOME/runs/$epochtime.tar.gz`
+   Envoy interprets the '[arguments...]' and runs in the current working
+   directory (aka $CWD) until getenvoy is interrupted (ex Ctrl+C, Ctrl+Break).
+   
+   Console output is captured as "stdout.log" and "stderr.log" in the run
+   directory (`$GETENVOY_HOME/runs/$epochtime`). When interrupted,
+   shutdown hooks write files, including admin endpoints, network and process
+   state. Upon exit, these zip into `$GETENVOY_HOME/runs/$epochtime.tar.gz`
diff --git a/internal/cmd/testdata/getenvoy_use_help.txt b/internal/cmd/testdata/getenvoy_use_help.txt
index e12b3e3..cfab6d1 100644
--- a/internal/cmd/testdata/getenvoy_use_help.txt
+++ b/internal/cmd/testdata/getenvoy_use_help.txt
@@ -1,5 +1,5 @@
 NAME:
-   getenvoy use - Sets the current [version] used by the "run" command, installing as necessary
+   getenvoy use - Sets the current [version] used by the "run" command
 
 USAGE:
    getenvoy use [version]
diff --git a/internal/envoy/envoy_test.go b/internal/envoy/envoy_test.go
deleted file mode 100644
index d552df4..0000000
--- a/internal/envoy/envoy_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2021 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package envoy
-
-import (
-	"testing"
-)
-
-// RequireEnvoyPid returns the pid of the child process
-func RequireEnvoyPid(t *testing.T, r *Runtime) int {
-	if r.cmd == nil || r.cmd.Process == nil {
-		t.Fatal("envoy process not yet started")
-	}
-	return r.cmd.Process.Pid
-}
diff --git a/internal/envoy/run_test.go b/internal/envoy/run_test.go
index c40abec..aa7e8ea 100644
--- a/internal/envoy/run_test.go
+++ b/internal/envoy/run_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package envoy_test
+package envoy
 
 import (
 	"bytes"
@@ -26,7 +26,6 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/tetratelabs/getenvoy/internal/envoy"
 	"github.com/tetratelabs/getenvoy/internal/globals"
 	"github.com/tetratelabs/getenvoy/internal/test"
 	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
@@ -40,9 +39,8 @@ func TestRuntime_Run(t *testing.T) {
 	runDir := filepath.Join(runsDir, "1619574747231823000") // fake a realistic value
 	adminFlag := fmt.Sprintf("--admin-address-path %s/admin-address.txt", runDir)
 
-	// "quiet" as we aren't testing the environment envoy runs in
-	fakeEnvoy := filepath.Join(tempDir, "quiet")
-	morerequire.RequireCaptureScript(t, fakeEnvoy)
+	fakeEnvoy := filepath.Join(tempDir, "envoy")
+	test.RequireFakeEnvoy(t, fakeEnvoy)
 
 	tests := []struct {
 		name                           string
@@ -53,10 +51,11 @@ func TestRuntime_Run(t *testing.T) {
 		wantShutdownHook               bool
 	}{
 		{
-			name: "GetEnvoy Ctrl-C",
+			name: "GetEnvoy Ctrl+C",
+			args: []string{"-c", "envoy.yaml"},
 			// Don't warn the user when they exited the process
-			expectedStdout:   fmt.Sprintln("starting:", fakeEnvoy, adminFlag),
-			expectedStderr:   "started\ncaught SIGINT\n",
+			expectedStdout:   fmt.Sprintln("starting:", fakeEnvoy, "-c", "envoy.yaml", adminFlag) + "GET /ready HTTP/1.1\n",
+			expectedStderr:   "initializing epoch 0\nstarting main dispatch loop\ncaught SIGINT\nexiting\n",
 			wantShutdownHook: true,
 		},
 		// We don't test envoy dying from an external signal as it isn't reported back to the getenvoy process and
@@ -64,10 +63,10 @@ func TestRuntime_Run(t *testing.T) {
 		{
 			name:           "Envoy exited with error",
 			shutdown:       func() { time.Sleep(time.Millisecond * 100) },
-			args:           []string{"quiet_exit=3"},
-			expectedStdout: fmt.Sprintln("starting:", fakeEnvoy, "quiet_exit=3", adminFlag),
-			expectedStderr: "started\n",
-			expectedErr:    "envoy exited with status: 3",
+			args:           []string{}, // no config file!
+			expectedStdout: fmt.Sprintln("starting:", fakeEnvoy, adminFlag),
+			expectedStderr: "initializing epoch 0\nexiting\nAt least one of --config-path or --config-yaml or Options::configProto() should be non-empty\n",
+			expectedErr:    "envoy exited with status: 1",
 		},
 	}
 
@@ -80,12 +79,12 @@ func TestRuntime_Run(t *testing.T) {
 			stdout := new(bytes.Buffer)
 			stderr := new(bytes.Buffer)
 
-			r := envoy.NewRuntime(o)
+			r := NewRuntime(o)
 			r.Out = stdout
 			r.Err = stderr
 			var haveShutdownHook bool
 			r.RegisterShutdownHook(func(_ context.Context) error {
-				pid := envoy.RequireEnvoyPid(t, r)
+				pid := requireEnvoyPid(t, r)
 				_, err := os.FindProcess(pid)
 				require.NoError(t, err, "shutdownHook called after process shutdown")
 				haveShutdownHook = true
@@ -97,7 +96,7 @@ func TestRuntime_Run(t *testing.T) {
 				shutdown = interrupt(r)
 			}
 
-			// tee the error stream so we can look for the "started" line without consuming it.
+			// tee the error stream so we can look for the "starting main dispatch loop" line without consuming it.
 			errCopy := new(bytes.Buffer)
 			r.Err = io.MultiWriter(r.Err, errCopy)
 			err := test.RequireRun(t, shutdown, r, errCopy, tc.args...)
@@ -108,6 +107,12 @@ func TestRuntime_Run(t *testing.T) {
 				require.EqualError(t, err, tc.expectedErr)
 			}
 
+			// Ensure envoy was run with the expected environment
+			require.Empty(t, r.cmd.Dir) // envoy runs in the same directory as getenvoy
+			expectedArgs := append([]string{fakeEnvoy}, tc.args...)
+			expectedArgs = append(expectedArgs, "--admin-address-path", filepath.Join(runDir, "admin-address.txt"))
+			require.Equal(t, expectedArgs, r.cmd.Args)
+
 			// Assert appropriate hooks are called
 			require.Equal(t, tc.wantShutdownHook, haveShutdownHook)
 
@@ -128,7 +133,7 @@ func TestRuntime_Run(t *testing.T) {
 	}
 }
 
-func interrupt(r *envoy.Runtime) func() {
+func interrupt(r *Runtime) func() {
 	return func() {
 		fakeInterrupt := r.FakeInterrupt
 		if fakeInterrupt != nil {
@@ -136,3 +141,10 @@ func interrupt(r *envoy.Runtime) func() {
 		}
 	}
 }
+
+func requireEnvoyPid(t *testing.T, r *Runtime) int {
+	if r.cmd == nil || r.cmd.Process == nil {
+		t.Fatal("envoy process not yet started")
+	}
+	return r.cmd.Process.Pid
+}
diff --git a/internal/envoy/shutdown/admin_test.go b/internal/envoy/shutdown/admin_test.go
index e2cca2f..1e85d18 100644
--- a/internal/envoy/shutdown/admin_test.go
+++ b/internal/envoy/shutdown/admin_test.go
@@ -58,7 +58,7 @@ func TestEnableEnvoyAdminDataCollection(t *testing.T) {
 // runWithShutdownHook is like RequireRun, except invokes the hook on shutdown
 func runWithShutdownHook(t *testing.T, runDir string, hook func(r *envoy.Runtime) error, args ...string) error {
 	fakeEnvoy := filepath.Join(runDir, "envoy")
-	morerequire.RequireCaptureScript(t, fakeEnvoy)
+	test.RequireFakeEnvoy(t, fakeEnvoy)
 
 	o := &globals.RunOpts{EnvoyPath: fakeEnvoy, RunDir: runDir, DontArchiveRunDir: true}
 
diff --git a/internal/test/envoy.go b/internal/test/envoy.go
index 8e9ba65..2d16265 100644
--- a/internal/test/envoy.go
+++ b/internal/test/envoy.go
@@ -17,22 +17,33 @@ package test
 import (
 	"bufio"
 	"context"
+	_ "embed" // Embedding the fakeEnvoySrc is easier than file I/O and ensures it doesn't skew coverage
 	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
 	"strings"
+	"sync"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 )
 
 // Runner allows us to not introduce dependency cycles on envoy.Runtime
 type Runner interface {
-	Run(ctx context.Context, args []string) (err error)
+	Run(ctx context.Context, args []string) error
 }
 
 // RequireRun executes Run on the given Runtime and calls shutdown after it started.
 func RequireRun(t *testing.T, shutdown func(), r Runner, stderr io.Reader, args ...string) (err error) {
 	ctx, cancel := context.WithCancel(context.Background())
+	if shutdown == nil {
+		shutdown = cancel
+	}
 	go func() {
 		err = r.Run(ctx, args)
 		cancel()
@@ -41,7 +52,7 @@ func RequireRun(t *testing.T, shutdown func(), r Runner, stderr io.Reader, args
 	reader := bufio.NewReader(stderr)
 	require.Eventually(t, func() bool {
 		b, err := reader.Peek(512)
-		return err != nil && strings.Contains(string(b), "started\n")
+		return err != nil && strings.Contains(string(b), "initializing epoch 0\n")
 	}, 2*time.Second, 100*time.Millisecond, "never started process")
 
 	shutdown()
@@ -53,3 +64,53 @@ func RequireRun(t *testing.T, shutdown func(), r Runner, stderr io.Reader, args
 	}
 	return //nolint
 }
+
+var (
+	// fakeEnvoySrc is a test source file used to simulate Envoy console output and signal processing.
+	//go:embed testdata/fake_envoy.go
+	fakeEnvoySrc []byte
+	// fakeEnvoyBin is the compiled code of fakeEnvoySrc which will be runtime.GOOS dependent.
+	fakeEnvoyBin   []byte
+	builtFakeEnvoy sync.Once
+)
+
+// RequireFakeEnvoy writes fakeEnvoyBin to the given path
+func RequireFakeEnvoy(t *testing.T, path string) {
+	builtFakeEnvoy.Do(func() {
+		fakeEnvoyBin = requireBuildFakeEnvoy(t)
+	})
+	require.NoError(t, os.WriteFile(path, fakeEnvoyBin, 0700)) //nolint:gosec
+}
+
+// requireBuildFakeEnvoy builds a fake envoy binary and returns its contents.
+func requireBuildFakeEnvoy(t *testing.T) []byte {
+	goBin := requireGoBin(t)
+	tempDir, deleteTempDir := morerequire.RequireNewTempDir(t)
+	defer deleteTempDir()
+
+	name := "envoy"
+	src := name + ".go"
+	require.NoError(t, os.WriteFile(filepath.Join(tempDir, src), fakeEnvoySrc, 0600))
+	cmd := exec.Command(goBin, "build", "-o", name, src) //nolint:gosec
+	cmd.Dir = tempDir
+	out, err := cmd.CombinedOutput()
+	require.NoError(t, err, "couldn't compile %s: %s", src, string(out))
+	bytes, err := os.ReadFile(filepath.Join(tempDir, name))
+	require.NoError(t, err)
+	return bytes
+}
+
+func requireGoBin(t *testing.T) string {
+	binName := "go"
+	if runtime.GOOS == "windows" {
+		binName += ".exe"
+	}
+	goBin := filepath.Join(runtime.GOROOT(), "bin", binName)
+	if _, err := os.Stat(goBin); err == nil {
+		return goBin
+	}
+	// Now, search the path
+	goBin, err := exec.LookPath(binName)
+	require.NoError(t, err, "couldn't find %s in the PATH", goBin)
+	return goBin
+}
diff --git a/internal/test/morerequire/morerequire.go b/internal/test/morerequire/morerequire.go
index 8bc5487..70d411a 100644
--- a/internal/test/morerequire/morerequire.go
+++ b/internal/test/morerequire/morerequire.go
@@ -17,8 +17,6 @@
 package morerequire
 
 import (
-	// Embedding the capture script is easier than file I/O each time it is used.
-	_ "embed"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -46,18 +44,6 @@ func RequireSetMtime(t *testing.T, dir, date string) {
 	require.NoError(t, os.Chtimes(dir, td, td))
 }
 
-var (
-	// captureScript is a test script used for capturing arguments and signals. If it receives an argument "exit=N",
-	// where N is a code number, the script exits with that status. Otherwise it sleeps until a signal interrupts it.
-	//go:embed testdata/capture.sh
-	captureScript []byte
-)
-
-// RequireCaptureScript writes captureScript to the given path
-func RequireCaptureScript(t *testing.T, path string) {
-	require.NoError(t, os.WriteFile(path, captureScript, 0700)) //nolint:gosec
-}
-
 // RequireSetenv will os.Setenv the given key and value. The function returned reverts to the original.
 func RequireSetenv(t *testing.T, key, value string) func() {
 	previous := os.Getenv(key)
diff --git a/internal/test/morerequire/testdata/capture.sh b/internal/test/morerequire/testdata/capture.sh
deleted file mode 100755
index 4ffbbad..0000000
--- a/internal/test/morerequire/testdata/capture.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/bin/sh
-
-# Copyright 2021 Tetrate
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Below are how Envoy 1.17 handle signals
-# * `kill -2 pid` or Ctrl-C
-trap 'echo >&2 "caught SIGINT"; exit 0' INT
-# * `kill pid`
-trap 'echo >&2 "caught ENVOY_SIGTERM"; exit 0' TERM
-# * `kill -9 pid` cannot be trapped, but Envoy echos "Killed: 9" and exits 137
-
-# Echo a line so we know the traps applied
-echo >&2 "started"
-
-# This script allows us to test what was invoked by exec.Command, and any signal handling.
-# This is intentionally written in minimal syntax, which works in ash (Alpine) as well as bash.
-set -ue
-
-# Echo invocation context to stdout and fake stderr to ensure it is not combined into stdout.
-# We prefix output to help distinguish between this script and what's calling it.
-bin=$(basename "$0")
-if [ "$bin" != "quiet" ]; then
-  echo "$bin wd: $PWD"
-  echo "$bin bin: $0"
-  echo "$bin args: $*"
-fi
-
-# If any arg is ${bin}_exit=N, exit with that code. Ex. envoy_exit=3
-for arg in "$@"; do
-  case $arg in ${bin}_exit=*) exit $(echo "$arg" | cut -d= -f2) ;; esac
-done
-
-# sleep and echo the interrupting signal.
-while true; do sleep 1; done
diff --git a/internal/test/server.go b/internal/test/server.go
index 38f459d..e93a8b2 100644
--- a/internal/test/server.go
+++ b/internal/test/server.go
@@ -106,15 +106,7 @@ func requireFakeEnvoyTarGz(t *testing.T, v string) []byte {
 	// construct the platform directory based on the input version
 	installDir := filepath.Join(tempDir, v)
 	require.NoError(t, os.MkdirAll(filepath.Join(installDir, "bin"), 0700)) //nolint:gosec
-	// go:embed doesn't allow us to retain execute bit, so it is simpler to inline this.
-	fakeEnvoy := []byte(`#!/bin/sh
-set -ue
-# Echo invocation context to stdout and fake stderr to ensure it is not combined into stdout.
-echo envoy bin: $0
-echo envoy args: $@
-echo >&2 envoy stderr
-`)
-	require.NoError(t, os.WriteFile(filepath.Join(installDir, "bin", "envoy"), fakeEnvoy, 0700)) //nolint:gosec
+	RequireFakeEnvoy(t, filepath.Join(installDir, "bin", "envoy"))
 
 	// tar.gz the platform dir
 	tempGz := filepath.Join(tempDir, "envoy.tar.gz")
diff --git a/internal/test/testdata/fake_envoy.go b/internal/test/testdata/fake_envoy.go
new file mode 100644
index 0000000..d83559a
--- /dev/null
+++ b/internal/test/testdata/fake_envoy.go
@@ -0,0 +1,67 @@
+package main
+
+import (
+	"os"
+	"os/signal"
+	"runtime"
+	"syscall"
+)
+
+const errorConfig = "At least one of --config-path or --config-yaml or Options::configProto() should be non-empty"
+
+// lf ensures line feeds are realistic
+var lf = func() string {
+	if runtime.GOOS == "windows" {
+		return "\r\n"
+	}
+	return "\n"
+}()
+
+// main was originally ported from a shell script. Compiling allows a more realistic test.
+func main() {
+	// Trap signals so we can respond like Envoy does
+	c := make(chan os.Signal, 1)
+	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+
+	// Echo the same first line Envoy would. This also lets code scraping output know signals are trapped
+	os.Stderr.Write([]byte("initializing epoch 0" + lf)) //nolint
+
+	// Validate a haveConfig is passed just like Envoy
+	haveConfig := false
+	for _, arg := range os.Args[1:] {
+		switch arg {
+		case "-c", "--haveConfig-path", "--haveConfig-yaml":
+			haveConfig = true
+		}
+	}
+	if !haveConfig {
+		exit(1, "exiting", errorConfig) // oddly, it is this order
+	}
+
+	// Echo the same line Envoy would on successful startup
+	os.Stderr.Write([]byte("starting main dispatch loop" + lf)) //nolint
+
+	// Envoy console messages all write to stderr. Simulate access_log_path: '/dev/stdout'
+	os.Stdout.Write([]byte("GET /ready HTTP/1.1" + lf)) //nolint
+
+	// Block until we receive a signal
+	msg := "unexpected"
+	select {
+	case s := <-c: // Below are how Envoy 1.17 handle signals
+		switch s {
+		case os.Interrupt: // Ex. "kill -2 $pid", Ctrl+C or Ctrl+Break
+			msg = "caught SIGINT"
+		case syscall.SIGTERM: // Ex. "kill $pid"
+			msg = "caught ENVOY_SIGTERM"
+		}
+	}
+
+	exit(0, msg, "exiting")
+}
+
+func exit(ec int, messages ...string) {
+	for _, m := range messages {
+		os.Stderr.Write([]byte(m + lf)) //nolint
+	}
+	os.Exit(ec)
+}