mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
90 lines
3.2 KiB
Go
90 lines
3.2 KiB
Go
package ioscompanion
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion/runnerassets"
|
|
)
|
|
|
|
// runnerPortPlaceholder is the marker the prepare script plants in the
|
|
// packaged test configuration's environment. The spawner substitutes the
|
|
// session's port before launching.
|
|
const runnerPortPlaceholder = "__COMPANION_PORT__"
|
|
|
|
// realSpawnRunner extracts the embedded runner bundle, writes a test
|
|
// configuration bound to the session's port, and starts the hosting test
|
|
// session against the configured simulator. Cancel sends SIGTERM so the
|
|
// session tears down its in-simulator children cleanly.
|
|
func (d *Driver) realSpawnRunner(ctx context.Context, address string) (*exec.Cmd, error) {
|
|
extractDirectory := filepath.Join(os.TempDir(), "sanderling-runner")
|
|
testRunPath, err := runnerassets.Extract(extractDirectory)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extract runner: %w", err)
|
|
}
|
|
_, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
boundTestRunPath, err := bindTestRunPort(testRunPath, port)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logPath := filepath.Join(extractDirectory, "runner-session-"+port+".log")
|
|
logFile, err := os.Create(logPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create runner log: %w", err)
|
|
}
|
|
|
|
command := exec.CommandContext(ctx, "xcrun", "xcodebuild", "test-without-building",
|
|
"-xctestrun", boundTestRunPath,
|
|
"-destination", "platform=iOS Simulator,id="+d.udid)
|
|
// The session log stays out of the run output: the build tool is noisy
|
|
// and would pollute the run's error scan. The path is reported once so a
|
|
// failed startup is diagnosable.
|
|
command.Stdout = logFile
|
|
command.Stderr = logFile
|
|
command.Env = []string{
|
|
"HOME=" + os.Getenv("HOME"),
|
|
"PATH=/usr/bin:/bin",
|
|
"TMPDIR=" + os.TempDir(),
|
|
}
|
|
command.Cancel = func() error { return command.Process.Signal(syscall.SIGTERM) }
|
|
command.WaitDelay = shutdownGrace
|
|
startErr := command.Start()
|
|
// The child holds its own descriptor after Start, so the parent's copy
|
|
// closes either way.
|
|
logFile.Close()
|
|
if startErr != nil {
|
|
return nil, fmt.Errorf("start runner session: %w", startErr)
|
|
}
|
|
fmt.Fprintf(d.output, "runner session pid=%d listening on %s (log: %s)\n", command.Process.Pid, address, logPath)
|
|
return command, nil
|
|
}
|
|
|
|
// bindTestRunPort writes a copy of the extracted test configuration with the
|
|
// port placeholder substituted, next to the original so its relative paths
|
|
// keep resolving. Returns the bound copy's path.
|
|
func bindTestRunPort(testRunPath, port string) (string, error) {
|
|
configuration, err := os.ReadFile(testRunPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read test configuration: %w", err)
|
|
}
|
|
if !bytes.Contains(configuration, []byte(runnerPortPlaceholder)) {
|
|
return "", fmt.Errorf("test configuration %s carries no %s placeholder", testRunPath, runnerPortPlaceholder)
|
|
}
|
|
bound := bytes.ReplaceAll(configuration, []byte(runnerPortPlaceholder), []byte(port))
|
|
boundPath := filepath.Join(filepath.Dir(testRunPath), "runner-"+port+".xctestrun")
|
|
if err := os.WriteFile(boundPath, bound, 0o644); err != nil {
|
|
return "", fmt.Errorf("write bound test configuration: %w", err)
|
|
}
|
|
return boundPath, nil
|
|
}
|