Factor out Executable() and FlagPath() into testutils.

Also, have FlagPath() assert the absence of the flag file first. Improve
some naming.

Change-Id: Id6a45c74abab86361762ddd1e045dd6576fec925
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/610662
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Commit-Queue: Erik Rose <erikrose@google.com>
diff --git a/go/testutils/testutils.go b/go/testutils/testutils.go
index eaa927c..1e237cd 100644
--- a/go/testutils/testutils.go
+++ b/go/testutils/testutils.go
@@ -206,3 +206,24 @@
 		util.RemoveAll(fakeHome)
 	})
 }
+
+// Executable returns the filesystem path to the binary running the current test, panicking on
+// failure.
+func Executable(t sktest.TestingT) string {
+	executable, err := os.Executable()
+	require.NoError(t, err)
+	return executable
+}
+
+// FlagPath returns the path to a temp file for use as a synchronization flag between the test
+// runner and a mocked-out subprocess launched by tested code; see go/flag-files. It lives next to
+// the test executable and has the specified name, which should contain the name of the test for
+// uniqueness across concurrent tests. It also asserts the absence of said file as a safety measure
+// against leftover files from earlier runs—admittedly unlikely given "go test"'s and Bazel's
+// proclivity for running everything in temp dirs.
+func FlagPath(t sktest.TestingT, fileName string) string {
+	path := filepath.Join(filepath.Dir(Executable(t)), fileName)
+	_, err := os.Stat(path)
+	require.True(t, errors.Is(err, os.ErrNotExist), fmt.Sprintf("Flag file %s existed before it was expected.", fileName))
+	return path
+}
diff --git a/machine/go/test_machine_monitor/foundrybotcustodian/BUILD.bazel b/machine/go/test_machine_monitor/foundrybotcustodian/BUILD.bazel
index 01d5642..b0c0d56 100644
--- a/machine/go/test_machine_monitor/foundrybotcustodian/BUILD.bazel
+++ b/machine/go/test_machine_monitor/foundrybotcustodian/BUILD.bazel
@@ -22,6 +22,7 @@
     deps = [
         "//go/executil",
         "//go/recentschannel",
+        "//go/testutils",
         "@com_github_stretchr_testify//require",
     ],
 )
diff --git a/machine/go/test_machine_monitor/foundrybotcustodian/foundrybotcustodian_test.go b/machine/go/test_machine_monitor/foundrybotcustodian/foundrybotcustodian_test.go
index 3f4e7b9..877f97b 100644
--- a/machine/go/test_machine_monitor/foundrybotcustodian/foundrybotcustodian_test.go
+++ b/machine/go/test_machine_monitor/foundrybotcustodian/foundrybotcustodian_test.go
@@ -5,13 +5,13 @@
 	"errors"
 	"os"
 	"os/signal"
-	"path/filepath"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/require"
 	"go.skia.org/infra/go/executil"
 	"go.skia.org/infra/go/recentschannel"
+	"go.skia.org/infra/go/testutils"
 )
 
 // launchTimeout is how long we're willing to wait for a process to spin up.
@@ -39,12 +39,6 @@
 	time.Sleep(2 * launchTimeout)
 }
 
-func testBinary(t *testing.T) string {
-	executable, err := os.Executable()
-	require.NoError(t, err)
-	return executable
-}
-
 func TestStart_RelaunchesIfProcessExits(t *testing.T) {
 	// This also tests the initial launch.
 	ctx, cancel := context.WithCancel(executil.FakeTestsContext("Test_FakeExe_FoundryBot_ExitsWithZero",
@@ -52,7 +46,7 @@
 	defer cancel()
 	wantFoundryBotUpCh := recentschannel.New[bool](1)
 	wantFoundryBotUpCh.Send(true)
-	require.NoError(t, Start(ctx, testBinary(t), "ignored", wantFoundryBotUpCh))
+	require.NoError(t, Start(ctx, testutils.Executable(t), "ignored", wantFoundryBotUpCh))
 	require.Eventually(t, func() bool {
 		return executil.FakeCommandsReturned(ctx) >= 2
 	}, launchTimeout, launchTimeout/10, "Foundry Bot never got relaunched after exiting.")
@@ -63,10 +57,10 @@
 	require.Contains(t, err.Error(), "Foundry Bot not found")
 }
 
-func gracefulStopTempFile(t *testing.T) string {
-	executable, err := os.Executable()
-	require.NoError(t, err)
-	return filepath.Join(filepath.Dir(executable), "TestStart_GracefullyStopsProcessIfHeartbeatSaysFalse.temp")
+// flagFileForProcessStartAndInterrupt returns the path to the file through which we synchronize the
+// fake Foundry Bot process with the test harness.
+func flagFileForProcessStartAndInterrupt(t *testing.T) string {
+	return testutils.FlagPath(t, "foundryBotStartAndInterrupt.temp")
 }
 
 // Test_FakeExe_FoundryBot_RunsUntilInterruptAndMakesFlagFile pretends to be a Foundry Bot which
@@ -80,8 +74,8 @@
 	require.Contains(t, executil.OriginalArgs(), "session")
 
 	// Make flag file.
-	tempPath := gracefulStopTempFile(t)
-	file, err := os.Create(tempPath)
+	flag := flagFileForProcessStartAndInterrupt(t)
+	file, err := os.Create(flag)
 	require.NoError(t, err)
 	require.NoError(t, file.Close())
 
@@ -91,7 +85,7 @@
 	timeout := time.NewTicker(launchTimeout)
 	select {
 	case <-interrupt:
-		require.NoError(t, os.Remove(tempPath))
+		require.NoError(t, os.Remove(flag))
 	case <-timeout.C:
 		// Let the file leak. If under Bazel, it's in a temp dir anyway.
 	}
@@ -103,17 +97,16 @@
 	wantFoundryBotUpCh := recentschannel.New[bool](1)
 	wantFoundryBotUpCh.Send(true)
 	ctx := executil.FakeTestsContext("Test_FakeExe_FoundryBot_RunsUntilInterruptAndMakesFlagFile")
-	require.NoError(t, Start(ctx, testBinary(t), "ignored", wantFoundryBotUpCh))
-	tempPath := gracefulStopTempFile(t)
+	flag := flagFileForProcessStartAndInterrupt(t)
+	require.NoError(t, Start(ctx, testutils.Executable(t), "ignored", wantFoundryBotUpCh))
 
-	// Wait until TestStart_GracefullyStopsProcessIfHeartbeatSaysFalse.temp exists, showing the
-	// process is up.
+	// Wait until foundryBotStartAndInterrupt.temp exists, showing the process is up.
 	//
 	// Using the FS (relative to the test executable) as a place to rendezvous and also a
 	// synchronization mechanism lets us avoid shoehorning extra channels, mutexes, and struct-level
 	// vars into the implementation just to give visibility to tests.
 	require.Eventually(t, func() bool {
-		_, err := os.Stat(tempPath)
+		_, err := os.Stat(flag)
 		return err == nil
 	}, launchTimeout, launchTimeout/10, "Foundry Bot process never came up.")
 
@@ -122,7 +115,7 @@
 
 	// Wait until temp file disappears, indicating the process has received the requisite SIGINT.
 	require.Eventually(t, func() bool {
-		_, err := os.Stat(tempPath)
+		_, err := os.Stat(flag)
 		return errors.Is(err, os.ErrNotExist)
 	}, launchTimeout, launchTimeout/10, "Foundry Bot process never caught SIGINT.")
 }