304 lines
9.3 KiB
Go
304 lines
9.3 KiB
Go
package agent_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"aego/app/agent"
|
|
"aego/app/run"
|
|
"aego/asset/pipeline"
|
|
"aego/core/log"
|
|
"aego/project"
|
|
"aego/scene/mutate"
|
|
"aego/sdk"
|
|
"aego/tests/fixture"
|
|
)
|
|
|
|
var update = flag.Bool("update", false, "rewrite the golden files")
|
|
|
|
type env struct {
|
|
t *testing.T
|
|
dir string
|
|
p *project.Project
|
|
opt agent.Options
|
|
}
|
|
|
|
func open(t *testing.T) *env {
|
|
t.Helper()
|
|
dir := fixture.Copy(t, "platformer")
|
|
p, err := project.Load(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mods := []sdk.Module{fixture.Platformer{}}
|
|
return &env{t: t, dir: dir, p: p, opt: agent.Options{Mods: mods, Run: run.AgentRunner(mods...)}}
|
|
}
|
|
|
|
func (e *env) run(name string, a agent.Args) (*agent.Output, error) {
|
|
e.t.Helper()
|
|
s, err := agent.Open(e.p, e.opt)
|
|
if err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
return agent.Run(s, name, a)
|
|
}
|
|
|
|
func (e *env) must(name string, a agent.Args) *agent.Output {
|
|
e.t.Helper()
|
|
out, err := e.run(name, a)
|
|
if err != nil {
|
|
e.t.Fatalf("%s %v: %v", name, a, err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (e *env) read(rel string) []byte {
|
|
e.t.Helper()
|
|
b, err := os.ReadFile(filepath.Join(e.dir, filepath.FromSlash(rel)))
|
|
if err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (e *env) write(rel string, b []byte) {
|
|
e.t.Helper()
|
|
abs := filepath.Join(e.dir, filepath.FromSlash(rel))
|
|
os.MkdirAll(filepath.Dir(abs), 0o755)
|
|
if err := os.WriteFile(abs, b, 0o644); err != nil {
|
|
e.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
const level = "scenes/main.aescene"
|
|
|
|
func golden(t *testing.T, name string, v any) {
|
|
t.Helper()
|
|
got, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = append(got, '\n')
|
|
path := filepath.Join("testdata", name+".json")
|
|
if *update {
|
|
if err := os.WriteFile(path, got, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
want, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("%v (run with -update)", err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("%s differs from %s (run with -update):\n%s", name, path, got)
|
|
}
|
|
}
|
|
|
|
func TestReadCommandsGolden(t *testing.T) {
|
|
e := open(t)
|
|
cases := []struct {
|
|
name string
|
|
cmd string
|
|
args agent.Args
|
|
}{
|
|
{"scene_tree", "scene.tree", agent.Args{"file": level}},
|
|
{"scene_get_player", "scene.get", agent.Args{"file": level, "node": "Player"}},
|
|
{"schema_list", "schema.list", agent.Args{}},
|
|
{"schema_show_body2d", "schema.show", agent.Args{"type": "Body2D"}},
|
|
{"refs_missing", "refs.missing", agent.Args{}},
|
|
{"project_validate", "project.validate", agent.Args{}},
|
|
{"asset_list_prefab", "asset.list", agent.Args{"kind": "prefab"}},
|
|
}
|
|
for _, c := range cases {
|
|
golden(t, c.name, e.must(c.cmd, c.args))
|
|
}
|
|
}
|
|
|
|
func TestSetGetAndDryRun(t *testing.T) {
|
|
e := open(t)
|
|
writes := []struct {
|
|
cmd string
|
|
args agent.Args
|
|
}{
|
|
{"scene.set", agent.Args{"file": level, "node": "Player", "path": "Body2D.gravityScale", "value": "2"}},
|
|
{"scene.add-node", agent.Args{"file": level, "name": "Extra"}},
|
|
{"scene.rename", agent.Args{"file": level, "node": "Player", "name": "Hero"}},
|
|
{"scene.add-comp", agent.Args{"file": level, "node": "Camera", "comp": "Tags"}},
|
|
{"data.set", agent.Args{"file": "assets/data/enemy_stats.aedata", "path": "speed", "value": "3"}},
|
|
{"asset.rename", agent.Args{"asset": "prefabs/fast_enemy.aeprefab", "name": "quick_enemy"}},
|
|
}
|
|
stamp := map[string]time.Time{}
|
|
snap := map[string][]byte{}
|
|
filepath.WalkDir(e.dir, func(p string, d fs.DirEntry, err error) error {
|
|
if err == nil && !d.IsDir() && !strings.Contains(p, ".aego") {
|
|
st, _ := d.Info()
|
|
stamp[p], snap[p] = st.ModTime(), e.read(strings.TrimPrefix(filepath.ToSlash(p), filepath.ToSlash(e.dir)+"/"))
|
|
}
|
|
return nil
|
|
})
|
|
for _, w := range writes {
|
|
a := agent.Args{"dry-run": "true"}
|
|
for k, v := range w.args {
|
|
a[k] = v
|
|
}
|
|
out := e.must(w.cmd, a)
|
|
if !out.DryRun {
|
|
t.Fatalf("%s: not reported as a dry run", w.cmd)
|
|
}
|
|
}
|
|
for p, ts := range stamp {
|
|
st, err := os.Stat(p)
|
|
if err != nil || !st.ModTime().Equal(ts) || !bytes.Equal(e.read(strings.TrimPrefix(filepath.ToSlash(p), filepath.ToSlash(e.dir)+"/")), snap[p]) {
|
|
t.Fatalf("a dry run touched %s", p)
|
|
}
|
|
}
|
|
e.must("scene.set", writes[0].args)
|
|
if got := e.must("scene.get", agent.Args{"file": level, "node": "Player", "path": "Body2D.gravityScale"}).Text; got != "2" {
|
|
t.Fatalf("gravityScale %q after set", got)
|
|
}
|
|
e.must("data.set", writes[4].args)
|
|
v := e.must("data.get", agent.Args{"file": "assets/data/enemy_stats.aedata"}).Value.(map[string]any)
|
|
if v["fields"].(map[string]string)["speed"] != "3" {
|
|
t.Fatalf("data %v", v)
|
|
}
|
|
}
|
|
|
|
func TestApplyAndInverse(t *testing.T) {
|
|
e := open(t)
|
|
before := e.read(level)
|
|
tx := mutate.Tx{Label: "t", Ops: []mutate.Op{
|
|
mutate.AddNode("Root", "Ledge").WithID("c00000000000000000000000000000aa"),
|
|
mutate.Set("Player", "Body2D.gravityScale", "3"),
|
|
mutate.Rename("Camera", "Eye"),
|
|
}}
|
|
src, _ := tx.Encode()
|
|
s, err := agent.Open(e.p, e.opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc, err := mutate.Load(before, level, mutate.Env{Types: s.Types, Library: s.Library})
|
|
s.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := doc.Apply(tx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inv, _ := res.Inverse.Encode()
|
|
e.must("scene.apply", agent.Args{"file": level, "tx": string(src)})
|
|
if bytes.Equal(e.read(level), before) {
|
|
t.Fatal("apply changed nothing")
|
|
}
|
|
e.write("inverse.aetext", inv)
|
|
e.must("scene.apply", agent.Args{"file": level, "tx-file": filepath.Join(e.dir, "inverse.aetext")})
|
|
if !bytes.Equal(e.read(level), before) {
|
|
t.Fatalf("the inverse did not restore the scene:\n%s", e.read(level))
|
|
}
|
|
}
|
|
|
|
func TestPrefabFlow(t *testing.T) {
|
|
e := open(t)
|
|
out := e.must("prefab.create", agent.Args{"file": level, "node": "Ground", "out": "prefabs/ground"})
|
|
if len(out.Created) != 1 {
|
|
t.Fatalf("create %+v", out)
|
|
}
|
|
ins := e.must("prefab.instantiate", agent.Args{"file": level, "prefab": "prefabs/ground.aeprefab", "name": "Ground2"})
|
|
second := ins.Created[0]
|
|
e.must("scene.set", agent.Args{"file": level, "node": "Ground", "path": "SpriteRenderer.tint", "value": "#123456ff"})
|
|
e.must("prefab.apply", agent.Args{"file": level, "node": "Ground", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
|
if got := e.must("scene.get", agent.Args{"file": level, "node": second, "path": "SpriteRenderer.tint"}).Text; got != "#123456ff" {
|
|
t.Fatalf("second instance tint %q", got)
|
|
}
|
|
again := e.must("prefab.apply", agent.Args{"file": level, "node": "Ground", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
|
if len(again.Notes) != 1 || again.Notes[0] != "nothing to apply" {
|
|
t.Fatalf("second apply %+v", again)
|
|
}
|
|
}
|
|
|
|
func TestDeleteMissingFix(t *testing.T) {
|
|
e := open(t)
|
|
const stats = "assets/data/enemy_stats.aedata"
|
|
data := e.read(stats)
|
|
if _, err := e.run("asset.delete", agent.Args{"asset": stats}); err == nil || !strings.Contains(err.Error(), "enemy.aeprefab") {
|
|
t.Fatalf("delete of a used asset: %v", err)
|
|
}
|
|
e.must("asset.delete", agent.Args{"asset": stats, "force": "true"})
|
|
miss := e.must("refs.missing", agent.Args{}).Value.(map[string]any)["assets"].([]agent.MissingView)
|
|
if len(miss) != 1 || miss[0].Was != stats {
|
|
t.Fatalf("missing %+v", miss)
|
|
}
|
|
e.write(stats, data)
|
|
e.must("refs.fix", agent.Args{"from": miss[0].Asset, "to": stats})
|
|
if got := e.must("refs.missing", agent.Args{}).Value.(map[string]any)["assets"].([]agent.MissingView); len(got) != 0 {
|
|
t.Fatalf("still missing %+v", got)
|
|
}
|
|
}
|
|
|
|
func runHash(t *testing.T, e *env, file string) agent.RunReport {
|
|
t.Helper()
|
|
out := e.must("scene.run", agent.Args{"file": file, "frames": "120"})
|
|
return out.Value.(agent.RunReport)
|
|
}
|
|
|
|
func TestRenameScriptAndRun(t *testing.T) {
|
|
e := open(t)
|
|
e.must("asset.rename", agent.Args{"asset": "scripts/player/player.aes", "name": "hero"})
|
|
a, b := runHash(t, e, level), runHash(t, e, level)
|
|
if a.Error != "" || a.Frames != 120 || a.Hash != b.Hash {
|
|
t.Fatalf("runs %+v %+v", a, b)
|
|
}
|
|
}
|
|
|
|
func TestRunReportsScriptFault(t *testing.T) {
|
|
e := open(t)
|
|
e.write("scripts/boom/boom.aes", []byte("package boom\n\nimport \"aego/log\"\n\nscript Boom {\n\tfunc Update(dt float) {\n\t\tvar z int = 0\n\t\tlog.Info(string(1 / z))\n\t}\n}\n"))
|
|
src := strings.Replace(string(e.read(level)), `script "mygame.player.PlayerController" enabled=true {`,
|
|
`script "mygame.boom.Boom" enabled=true {}
|
|
script "mygame.player.PlayerController" enabled=true {`, 1)
|
|
e.write(level, []byte(src))
|
|
out, err := e.run("scene.run", agent.Args{"file": level, "frames": "30"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rep := out.Value.(agent.RunReport)
|
|
if rep.Error == "" && !strings.Contains(strings.Join(rep.Log, "\n"), "zero") {
|
|
t.Fatalf("no fault reported: %+v", rep)
|
|
}
|
|
if !out.Errors() && rep.Error != "" {
|
|
t.Fatal("the run error is not an error-level problem")
|
|
}
|
|
}
|
|
|
|
func TestCreateRefusesSharedIndex(t *testing.T) {
|
|
e := open(t)
|
|
e.must("asset.list", agent.Args{})
|
|
s, err := agent.Open(e.p, e.opt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Close()
|
|
other, err := pipeline.Open(e.p, mustImporters(t, e), log.Nop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer other.Close()
|
|
if _, err := e.run("scene.new", agent.Args{"file": "scenes/fresh"}); err == nil || !strings.Contains(err.Error(), "holds the asset index") {
|
|
t.Fatalf("scene new while the index is held: %v", err)
|
|
}
|
|
}
|
|
|
|
func mustImporters(t *testing.T, e *env) *pipeline.ImporterRegistry {
|
|
t.Helper()
|
|
return pipeline.NewImporterRegistry()
|
|
}
|