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()) } }