osbase: move test packages under osbase/test

The cmd, freeport and socksproxy packages are only used in tests, and
are also intended to be only used in tests. This change moves these
packages under osbase/test.

Change-Id: I8fe679945990c295bdf822abfce56c25233c4588
Reviewed-on: https://review.monogon.dev/c/monogon/+/4564
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/test/cmd/BUILD.bazel b/osbase/test/cmd/BUILD.bazel
new file mode 100644
index 0000000..6d6a3c9
--- /dev/null
+++ b/osbase/test/cmd/BUILD.bazel
@@ -0,0 +1,11 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "cmd",
+    srcs = ["run.go"],
+    importpath = "source.monogon.dev/osbase/test/cmd",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//osbase/logbuffer",
+    ],
+)
diff --git a/osbase/test/cmd/run.go b/osbase/test/cmd/run.go
new file mode 100644
index 0000000..9649c03
--- /dev/null
+++ b/osbase/test/cmd/run.go
@@ -0,0 +1,111 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package cmd contains helpers that abstract away the chore of starting new
+// processes, tracking their lifetime, inspecting their output, etc.
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"strings"
+
+	"source.monogon.dev/osbase/logbuffer"
+)
+
+// RunCommand starts a new process and waits until either its completion, or
+// until the supplied predicate function pf returns true. The function is called
+// for each line produced by the new process.
+//
+// The returned boolean value equals the last value returned by pf.
+//
+// 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, 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()
+
+	// Copy the stdout and stderr output to a single channel of lines so that they
+	// can then be matched against expectedOutput.
+
+	// Since LineBuffer can write its buffered contents on a deferred Close,
+	// after the reader loop is broken, avoid deadlocks by making lineC a
+	// buffered channel.
+	lineC := make(chan string, 2)
+	lineCB := func(l *logbuffer.Line) {
+		// If the context is canceled, no-one is listening on lineC anymore, so we would
+		// block.
+		select {
+		case <-ctx.Done():
+			return
+		case lineC <- l.Data:
+		}
+	}
+	outBuffer := logbuffer.NewLineBuffer(1024, lineCB)
+	defer outBuffer.Close()
+	errBuffer := logbuffer.NewLineBuffer(1024, lineCB)
+	defer errBuffer.Close()
+
+	// Prepare the command context, and start the process.
+	cmd := exec.CommandContext(ctx, path, args...)
+	// Tee std{out,err} into the linebuffers above and the process' std{out,err}, to
+	// allow easier debugging.
+	cmd.Stdout = io.MultiWriter(os.Stdout, outBuffer)
+	cmd.Stderr = io.MultiWriter(os.Stderr, errBuffer)
+	if err := cmd.Start(); err != nil {
+		return false, fmt.Errorf("couldn't start the process: %w", err)
+	}
+
+	// Handle the case in which the process finishes before pf takes the chance to
+	// kill it.
+	complC := make(chan error, 1)
+	go func() {
+		complC <- cmd.Wait()
+	}()
+
+	// Try matching against expectedOutput and return the result.
+	for {
+		select {
+		case <-ctx.Done():
+			return false, ctx.Err()
+		case line := <-lineC:
+			if pf(line) {
+				cmd.Process.Kill()
+				<-complC
+				return true, nil
+			}
+		case err := <-complC:
+			return false, err
+		}
+	}
+}
+
+// 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. If logf isn't nil, it will be called whenever a new
+// line is received.
+func TerminateIfFound(needle string, logf func(string)) func(string) bool {
+	return func(haystack string) bool {
+		if logf != nil {
+			logf(haystack)
+		}
+		return strings.Contains(haystack, needle)
+	}
+}
+
+// WaitUntilCompletion creates a RunCommand predicate that will make it wait
+// for the process to exit on its own. If logf isn't nil, it will be called
+// whenever a new line is received.
+func WaitUntilCompletion(logf func(string)) func(string) bool {
+	return func(line string) bool {
+		if logf != nil {
+			logf(line)
+		}
+		return false
+	}
+}