mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Integrate reviewed real GNU timeout prerequisite for macOS native checks
Change-source: pulse-maintainer
This commit is contained in:
@@ -84,6 +84,26 @@ jobs:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Supply GNU timeout for macOS guidance checks
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
brew install coreutils
|
||||
gnu_timeout="$(brew --prefix coreutils)/bin/gtimeout"
|
||||
test -x "$gnu_timeout"
|
||||
"$gnu_timeout" --version | grep -q 'GNU coreutils'
|
||||
timeout_bin="$(mktemp -d "$RUNNER_TEMP/pulse-guidance-tools.XXXXXX")"
|
||||
ln -s "$gnu_timeout" "$timeout_bin/timeout"
|
||||
printf '%s\n' "$timeout_bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Verify native Unix timeout prerequisite
|
||||
if: matrix.unix
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
timeout --version | grep -q 'GNU coreutils'
|
||||
|
||||
- name: Test native agent runtime
|
||||
run: >-
|
||||
go test
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
//go:build !windows
|
||||
|
||||
package installtests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type nativeGuidanceStep struct {
|
||||
Name, If, Shell, Run string
|
||||
}
|
||||
|
||||
func nativeGuidanceToolSteps(content []byte) (nativeGuidanceStep, nativeGuidanceStep, error) {
|
||||
var workflow struct {
|
||||
Jobs map[string]struct{ Steps []nativeGuidanceStep }
|
||||
}
|
||||
if err := yaml.Unmarshal(content, &workflow); err != nil {
|
||||
return nativeGuidanceStep{}, nativeGuidanceStep{}, err
|
||||
}
|
||||
steps := workflow.Jobs["native-agent"].Steps
|
||||
bootstrapIndex, verifyIndex, installerIndex := -1, -1, -1
|
||||
for i, step := range steps {
|
||||
switch step.Name {
|
||||
case "Supply GNU timeout for macOS guidance checks":
|
||||
bootstrapIndex = i
|
||||
case "Verify native Unix timeout prerequisite":
|
||||
verifyIndex = i
|
||||
case "Test native Unix installer contracts":
|
||||
installerIndex = i
|
||||
}
|
||||
}
|
||||
if bootstrapIndex < 0 || verifyIndex <= bootstrapIndex || installerIndex <= verifyIndex {
|
||||
return nativeGuidanceStep{}, nativeGuidanceStep{}, fmt.Errorf("native guidance needs GNU tool supply and verification before installer tests")
|
||||
}
|
||||
bootstrap, verify := steps[bootstrapIndex], steps[verifyIndex]
|
||||
if bootstrap.If != "runner.os == 'macOS'" || verify.If != "matrix.unix" || steps[installerIndex].If != "matrix.unix" || bootstrap.Shell != "bash" || verify.Shell != "bash" {
|
||||
return bootstrap, verify, fmt.Errorf("GNU tool preparation must cover macOS, verification and installer tests must cover every native Unix job")
|
||||
}
|
||||
return bootstrap, verify, nil
|
||||
}
|
||||
|
||||
func TestNativeGuidanceToolsSupplyAndVerifyRealGNUTimeout(t *testing.T) {
|
||||
content, err := os.ReadFile(repoFile(".github", "workflows", "unified-agent-native.yml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bootstrap, verify, err := nativeGuidanceToolSteps(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
realTimeout, err := exec.LookPath("timeout")
|
||||
if err != nil {
|
||||
t.Fatal("native Unix verification requires the real GNU timeout prerequisite")
|
||||
}
|
||||
for _, scenario := range []string{"real GNU tool", "install failure", "missing tool", "non-GNU tool"} {
|
||||
t.Run(scenario, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
bin, prefix := filepath.Join(root, "bin"), filepath.Join(root, "coreutils with spaces")
|
||||
coreutilsBin, runnerTemp := filepath.Join(prefix, "bin"), filepath.Join(root, "runner temp")
|
||||
for _, dir := range []string{bin, coreutilsBin, runnerTemp} {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// Only Homebrew's installer is simulated. The positive timeout is
|
||||
// the real executable; the copied TERM/KILL recipes remain unchanged.
|
||||
const brew = `#!/bin/sh
|
||||
printf '%s\n' "$*" >> "$BREW_CALLS"
|
||||
case "$*" in
|
||||
'install coreutils') exit "${BREW_EXIT:-0}" ;;
|
||||
'--prefix coreutils') printf '%s\n' "$COREUTILS_PREFIX" ;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(bin, "brew"), []byte(brew), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tool := filepath.Join(coreutilsBin, "gtimeout")
|
||||
switch scenario {
|
||||
case "real GNU tool", "install failure":
|
||||
if err := os.Symlink(realTimeout, tool); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case "non-GNU tool":
|
||||
if err := os.WriteFile(tool, []byte("#!/bin/sh\nprintf 'BSD timeout\\n'\n"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
pathFile, callsFile := filepath.Join(root, "github-path"), filepath.Join(root, "brew-calls")
|
||||
installExit := "0"
|
||||
if scenario == "install failure" {
|
||||
installExit = "23"
|
||||
}
|
||||
env := append(os.Environ(), "PATH="+bin+":"+os.Getenv("PATH"), "GITHUB_PATH="+pathFile,
|
||||
"COREUTILS_PREFIX="+prefix, "BREW_CALLS="+callsFile, "BREW_EXIT="+installExit, "RUNNER_TEMP="+runnerTemp)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "bash", "-c", bootstrap.Run)
|
||||
cmd.Env = env
|
||||
output, runErr := cmd.CombinedOutput()
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("prerequisite observation did not finish: %v", ctx.Err())
|
||||
}
|
||||
if scenario != "real GNU tool" {
|
||||
wantExit := 1
|
||||
if scenario == "install failure" {
|
||||
wantExit = 23
|
||||
}
|
||||
exit, ok := runErr.(*exec.ExitError)
|
||||
if !ok || exit.ExitCode() != wantExit {
|
||||
t.Fatalf("prerequisite exit %v, want %d: %s", runErr, wantExit, output)
|
||||
}
|
||||
if _, err := os.Stat(pathFile); !os.IsNotExist(err) {
|
||||
t.Fatal("failed prerequisite exported an executable search path")
|
||||
}
|
||||
return
|
||||
}
|
||||
if runErr != nil {
|
||||
t.Fatalf("GNU prerequisite failed: %v: %s", runErr, output)
|
||||
}
|
||||
path, err := os.ReadFile(pathFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
timeoutBin := strings.TrimSuffix(string(path), "\n")
|
||||
if filepath.Dir(timeoutBin) != runnerTemp || !strings.HasPrefix(filepath.Base(timeoutBin), "pulse-guidance-tools.") {
|
||||
t.Fatal("bootstrap did not export its newly owned timeout directory")
|
||||
}
|
||||
entries, err := os.ReadDir(timeoutBin)
|
||||
if err != nil || len(entries) != 1 || entries[0].Name() != "timeout" {
|
||||
t.Fatal("bootstrap must not replace native macOS utilities other than timeout")
|
||||
}
|
||||
if target, err := os.Readlink(filepath.Join(timeoutBin, "timeout")); err != nil || target != tool {
|
||||
t.Fatal("timeout must resolve to the exact installed GNU executable")
|
||||
}
|
||||
calls, err := os.ReadFile(callsFile)
|
||||
if err != nil || string(calls) != "install coreutils\n--prefix coreutils\n" {
|
||||
t.Fatal("bootstrap changed its fixed Homebrew dependency")
|
||||
}
|
||||
cmd = exec.CommandContext(ctx, "bash", "-c", verify.Run)
|
||||
cmd.Env = append(env, "PATH="+timeoutBin+":"+os.Getenv("PATH"))
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("next workflow step cannot use supplied GNU timeout: %v: %s", err, output)
|
||||
}
|
||||
cmd = exec.CommandContext(ctx, "bash", "-c", verify.Run)
|
||||
cmd.Env = append(env, "PATH="+bin+":"+os.Getenv("PATH"))
|
||||
if err := os.WriteFile(filepath.Join(bin, "timeout"), []byte("#!/bin/sh\nprintf 'BSD timeout\\n'\n"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if output, err := cmd.CombinedOutput(); err == nil {
|
||||
t.Fatalf("Unix prerequisite probe accepted a non-GNU timeout: %s", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user