210 lines
5.0 KiB
Go
210 lines
5.0 KiB
Go
package harness
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/hjbdev/patterm/internal/projectkey"
|
|
"github.com/hjbdev/patterm/internal/trust"
|
|
)
|
|
|
|
type Options struct {
|
|
PattermBin string
|
|
Scenario *Scenario
|
|
}
|
|
|
|
type testEnv struct {
|
|
Root string `json:"root"`
|
|
ConfigHome string `json:"xdg_config_home"`
|
|
DataHome string `json:"xdg_data_home"`
|
|
RuntimeDir string `json:"xdg_runtime_dir"`
|
|
BinDir string `json:"bin_dir"`
|
|
ProjectDir string `json:"project_dir"`
|
|
PattermBin string `json:"patterm_bin"`
|
|
Cols uint16 `json:"cols"`
|
|
Rows uint16 `json:"rows"`
|
|
}
|
|
|
|
func prepareEnv(opts Options) (*testEnv, []string, error) {
|
|
sc := opts.Scenario
|
|
root, err := os.MkdirTemp("", "patterm-harness-*")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
env := &testEnv{
|
|
Root: root,
|
|
ConfigHome: filepath.Join(root, "config"),
|
|
DataHome: filepath.Join(root, "data"),
|
|
RuntimeDir: filepath.Join(root, "runtime"),
|
|
BinDir: filepath.Join(root, "bin"),
|
|
Cols: sc.Cols,
|
|
Rows: sc.Rows,
|
|
PattermBin: opts.PattermBin,
|
|
}
|
|
if env.ProjectDir = sc.ProjectDir; env.ProjectDir == "" {
|
|
env.ProjectDir = filepath.Join(root, "project")
|
|
}
|
|
for _, dir := range []string{env.ConfigHome, env.DataHome, env.RuntimeDir, env.BinDir, env.ProjectDir} {
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
if env.PattermBin == "" {
|
|
env.PattermBin, err = defaultPattermBin()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
if err := writeScripts(env.BinDir, sc.Scripts); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := writePresets(env.ConfigHome, sc.Presets); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := seedTrust(env, sc.Trust); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
childEnv := append(os.Environ(),
|
|
"XDG_CONFIG_HOME="+env.ConfigHome,
|
|
"XDG_DATA_HOME="+env.DataHome,
|
|
"XDG_RUNTIME_DIR="+env.RuntimeDir,
|
|
"PATTERM_HARNESS=1",
|
|
"PATH="+env.BinDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
)
|
|
for k, v := range sc.Env {
|
|
childEnv = append(childEnv, k+"="+v)
|
|
}
|
|
return env, childEnv, nil
|
|
}
|
|
|
|
func writeScripts(bin string, scripts []ScenarioScript) error {
|
|
for _, script := range scripts {
|
|
if script.Name == "" {
|
|
return fmt.Errorf("script missing name")
|
|
}
|
|
if strings.Contains(script.Name, "/") {
|
|
return fmt.Errorf("script name %q must not contain /", script.Name)
|
|
}
|
|
path := filepath.Join(bin, script.Name)
|
|
if err := os.WriteFile(path, []byte(script.Body), 0o700); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writePresets(configHome string, presets ScenarioPresets) error {
|
|
base := filepath.Join(configHome, "patterm", "presets")
|
|
if err := os.MkdirAll(filepath.Join(base, "agents"), 0o700); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(base, "processes"), 0o700); err != nil {
|
|
return err
|
|
}
|
|
for _, p := range presets.Agents {
|
|
if err := writePreset(filepath.Join(base, "agents", p.Name+".json"), p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, p := range presets.Processes {
|
|
if err := writePreset(filepath.Join(base, "processes", p.Name+".json"), p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writePreset(path string, p ScenarioPreset) error {
|
|
if p.Name == "" {
|
|
return fmt.Errorf("preset missing name")
|
|
}
|
|
b, err := json.MarshalIndent(p, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b = append(b, '\n')
|
|
return os.WriteFile(path, b, 0o600)
|
|
}
|
|
|
|
func seedTrust(env *testEnv, presets []string) error {
|
|
if len(presets) == 0 {
|
|
return nil
|
|
}
|
|
key, err := projectkey.Key(env.ProjectDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
restore := setenv(map[string]string{"XDG_DATA_HOME": env.DataHome})
|
|
defer restore()
|
|
store, err := trust.Open(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, p := range presets {
|
|
if err := store.Grant(p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setenv(vals map[string]string) func() {
|
|
type old struct {
|
|
v string
|
|
ok bool
|
|
}
|
|
prev := map[string]old{}
|
|
for k, v := range vals {
|
|
ov, ok := os.LookupEnv(k)
|
|
prev[k] = old{v: ov, ok: ok}
|
|
_ = os.Setenv(k, v)
|
|
}
|
|
return func() {
|
|
for k, ov := range prev {
|
|
if ov.ok {
|
|
_ = os.Setenv(k, ov.v)
|
|
} else {
|
|
_ = os.Unsetenv(k)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func defaultPattermBin() (string, error) {
|
|
if p := os.Getenv("PATTERM_BIN"); p != "" {
|
|
return p, nil
|
|
}
|
|
return buildPattermBinary()
|
|
}
|
|
|
|
func buildPattermBinary() (string, error) {
|
|
root, err := repoRoot()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
out := filepath.Join(os.TempDir(), "patterm-harness-bin", "patterm")
|
|
if err := os.MkdirAll(filepath.Dir(out), 0o700); err != nil {
|
|
return "", err
|
|
}
|
|
cmd := exec.Command("go", "build", "-o", out, "./cmd/patterm")
|
|
cmd.Dir = root
|
|
cmd.Env = os.Environ()
|
|
if b, err := cmd.CombinedOutput(); err != nil {
|
|
return "", fmt.Errorf("build patterm: %w\n%s", err, string(b))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func repoRoot() (string, error) {
|
|
_, file, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
return "", fmt.Errorf("runtime.Caller failed")
|
|
}
|
|
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")), nil
|
|
}
|