Files
sow-tools/internal/assets/compile_test.go
T
archvillainette 8e7cead5c0
build-binaries / build-binaries (push) Successful in 2m14s
Keep model compilation headless (#49)
## Summary

- always wrap NWN model compilation with `xvfb-run`, even when the caller has `DISPLAY`
- fail closed with an actionable error when `xvfb-run` is unavailable instead of opening the client UI
- update the compiler contract and regression coverage

## Verification

- focused red/green regression tests
- `nix develop -c make check`
- real NWN compile with `DISPLAY=:0` under transient Xvfb; binary MDL output verified

Generated with Claude CodeReviewed-on: #49

Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
2026-07-23 07:27:37 +00:00

172 lines
5.3 KiB
Go

package assets
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCompileDrivesEngineAndReplacesInPlace(t *testing.T) {
home := t.TempDir()
userData := filepath.Join(home, ".local", "share", "Neverwinter Nights")
dev := filepath.Join(userData, "development")
mc := filepath.Join(userData, "modelcompiler")
for _, d := range []string{dev, mc} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
// Fake nwmain binary for discovery via --nwn.
binDir := t.TempDir()
nwmain := filepath.Join(binDir, "nwmain-linux")
if err := os.WriteFile(nwmain, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
xvfbDir := t.TempDir()
xvfb := filepath.Join(xvfbDir, "xvfb-run")
if err := os.WriteFile(xvfb, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", xvfbDir+string(os.PathListSeparator)+os.Getenv("PATH"))
getenv := func(k string) string {
switch k {
case "HOME":
return home
case "DISPLAY":
return ":0" // a desktop display must not make compilation interactive
}
return ""
}
// Stub the engine: writes a binary compiled model into modelcompiler/.
orig := runner
defer func() { runner = orig }()
runner = func(dir string, env []string, name string, args ...string) ([]byte, error) {
if name != xvfb || len(args) != 5 || args[0] != "-a" ||
args[1] != "--server-args=-screen 0 1024x768x24" || args[2] != nwmain ||
args[3] != "compilemodel" {
t.Fatalf("engine command = %q %q, want xvfb-run wrapping nwmain", name, args)
}
stem := args[len(args)-1]
compiled := filepath.Join(mc, stem+".mdl")
return nil, os.WriteFile(compiled, []byte("\x00\x00compiled"), 0o644)
}
// Source tree with one ASCII model whose names already match its stem.
srcDir := t.TempDir()
src := filepath.Join(srcDir, "foo.mdl")
body := "newmodel foo\nbeginmodelgeom foo\n node dummy foo\n parent null\n endnode\nendmodelgeom foo\ndonemodel foo\n"
if err := os.WriteFile(src, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := runCompile([]string{"--nwn", nwmain, srcDir}, &stdout, &stderr, getenv)
if code != exitOK {
t.Fatalf("compile exit = %d\n%s", code, stderr.String())
}
// The source is now binary (the compiled artifact moved back over it).
data, err := os.ReadFile(src)
if err != nil {
t.Fatalf("compiled model missing: %v", err)
}
if string(data) != "\x00\x00compiled" {
t.Fatalf("source not replaced by compiled binary: %q", data)
}
}
func TestCompileAbortsOnNameMismatch(t *testing.T) {
home := t.TempDir()
if err := os.MkdirAll(filepath.Join(home, ".local", "share", "Neverwinter Nights", "development"), 0o755); err != nil {
t.Fatal(err)
}
binDir := t.TempDir()
nwmain := filepath.Join(binDir, "nwmain-linux")
if err := os.WriteFile(nwmain, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
xvfbDir := t.TempDir()
if err := os.WriteFile(filepath.Join(xvfbDir, "xvfb-run"), []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", xvfbDir+string(os.PathListSeparator)+os.Getenv("PATH"))
getenv := func(k string) string {
if k == "HOME" {
return home
}
if k == "DISPLAY" {
return ":0"
}
return ""
}
orig := runner
defer func() { runner = orig }()
engineCalled := false
runner = func(string, []string, string, ...string) ([]byte, error) {
engineCalled = true
return nil, nil
}
srcDir := t.TempDir()
// Internal name "wrong" != stem "foo": must abort before touching the engine.
if err := os.WriteFile(filepath.Join(srcDir, "foo.mdl"),
[]byte("newmodel wrong\ndonemodel wrong\n"), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
if code := runCompile([]string{"--nwn", nwmain, srcDir}, &stdout, &stderr, getenv); code != exitFail {
t.Fatalf("mismatch exit = %d, want %d", code, exitFail)
}
if engineCalled {
t.Fatal("engine should not run for a name-mismatched model")
}
}
func TestCompileFailsClosedWithoutXvfb(t *testing.T) {
home := t.TempDir()
userData := filepath.Join(home, ".local", "share", "Neverwinter Nights")
for _, d := range []string{filepath.Join(userData, "development"), filepath.Join(userData, "modelcompiler")} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
nwmain := filepath.Join(t.TempDir(), "nwmain-linux")
if err := os.WriteFile(nwmain, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", t.TempDir())
getenv := func(k string) string {
if k == "HOME" {
return home
}
return ""
}
orig := runner
defer func() { runner = orig }()
engineCalled := false
runner = func(string, []string, string, ...string) ([]byte, error) {
engineCalled = true
return nil, nil
}
srcDir := t.TempDir()
if err := os.WriteFile(filepath.Join(srcDir, "foo.mdl"),
[]byte("newmodel foo\nbeginmodelgeom foo\n node dummy foo\n parent null\n endnode\nendmodelgeom foo\ndonemodel foo\n"), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
if code := runCompile([]string{"--nwn", nwmain, srcDir}, &stdout, &stderr, getenv); code != exitTool {
t.Fatalf("compile exit = %d, want %d\n%s", code, exitTool, stderr.String())
}
if engineCalled {
t.Fatal("engine must not run without xvfb-run")
}
if !strings.Contains(stderr.String(), "xvfb-run") {
t.Fatalf("missing actionable xvfb-run error: %s", stderr.String())
}
}