m/p/cmd: use predicates in RunCommand

This generalizes RunCommand by making it accept any completion
predicate function.

Change-Id: Ic6b911244aaecd16c01000050fca618a8c8e09d7
Reviewed-on: https://review.monogon.dev/c/monogon/+/846
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/test/test.go b/metropolis/cli/metroctl/test/test.go
index 41cd2b5..4f572a1 100644
--- a/metropolis/cli/metroctl/test/test.go
+++ b/metropolis/cli/metroctl/test/test.go
@@ -25,7 +25,8 @@
 	}
 
 	log.Printf("$ metroctl %s", strings.Join(args, " "))
-	found, err := cmd.RunCommand(ctx, path, args, expect)
+	// Terminate metroctl as soon as the expected output is found.
+	found, err := cmd.RunCommand(ctx, path, args, cmd.TerminateIfFound(expect))
 	if err != nil {
 		return fmt.Errorf("while running metroctl: %v", err)
 	}
diff --git a/metropolis/installer/test/main.go b/metropolis/installer/test/main.go
index 2a03a75..6b5bbfd 100644
--- a/metropolis/installer/test/main.go
+++ b/metropolis/installer/test/main.go
@@ -69,7 +69,8 @@
 		"-no-reboot",
 	}
 	qemuArgs := append(defaultArgs, args...)
-	return cmd.RunCommand(ctx, "external/qemu/qemu-x86_64-softmmu", qemuArgs, expectedOutput)
+	pf := cmd.TerminateIfFound(expectedOutput)
+	return cmd.RunCommand(ctx, "external/qemu/qemu-x86_64-softmmu", qemuArgs, pf)
 }
 
 // runQemuWithInstaller runs the Metropolis Installer in a qemu, performing the
diff --git a/metropolis/pkg/cmd/run.go b/metropolis/pkg/cmd/run.go
index d5690a1..7591d56 100644
--- a/metropolis/pkg/cmd/run.go
+++ b/metropolis/pkg/cmd/run.go
@@ -14,12 +14,12 @@
 )
 
 // RunCommand starts a new process and waits until either its completion, or
-// appearance of expectedOutput in any line emitted by it. It returns true, if
-// expectedOutput was found, and false otherwise.
+// until the supplied predicate function returns true. The function is called
+// for each line produced by the new process.
 //
 // The process will be killed both in the event the context is cancelled, and
 // when expectedOutput is found.
-func RunCommand(ctx context.Context, path string, args []string, expectedOutput string) (bool, error) {
+func RunCommand(ctx context.Context, path string, args []string, pf func(string) bool) (bool, error) {
 	// Make a sub-context to ensure the process exits when this function is done.
 	ctx, ctxC := context.WithCancel(ctx)
 	defer ctxC()
@@ -56,7 +56,7 @@
 		case <-ctx.Done():
 			return false, ctx.Err()
 		case line := <-lineC:
-			if strings.Contains(line, expectedOutput) {
+			if pf(line) {
 				cmd.Process.Kill()
 				cmd.Wait()
 				return true, nil
@@ -64,3 +64,13 @@
 		}
 	}
 }
+
+// TerminateIfFound creates RunCommand predicates that instantly terminate
+// program execution in the event the given string is found in any line
+// produced. RunCommand will return true, if the string searched for was found,
+// and false otherwise.
+func TerminateIfFound(needle string) func(string) bool {
+	return func(haystack string) bool {
+		return strings.Contains(haystack, needle)
+	}
+}