added test for all implementations
This commit is contained in:
97
internal/bsp/build_test.go
Normal file
97
internal/bsp/build_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package bsp
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"bspviz/internal/geom"
|
||||
)
|
||||
|
||||
func TestStopUsesDefaults(t *testing.T) {
|
||||
segs := make([]geom.Seg, 13)
|
||||
if stop(segs, 0, Params{}) {
|
||||
t.Fatalf("stop should continue when using default LeafMax with 13 segs")
|
||||
}
|
||||
if !stop(segs, 33, Params{}) {
|
||||
t.Fatalf("stop should trigger when depth exceeds default MaxDepth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalCostSplitsAndBalance(t *testing.T) {
|
||||
line := []geom.Seg{
|
||||
{A: geom.V(-1, 1), B: geom.V(1, 1)},
|
||||
{A: geom.V(-1, -1), B: geom.V(1, -1)},
|
||||
{A: geom.V(-1, -1), B: geom.V(1, 1)},
|
||||
}
|
||||
p := Params{Alpha: 10, Beta: 2, Eps: geom.EPS}
|
||||
splits, balance, cost := evalCost(line, geom.V(0, 0), geom.V(1, 0), p)
|
||||
if splits != 1 {
|
||||
t.Fatalf("splits=%d want 1", splits)
|
||||
}
|
||||
if balance != 0 {
|
||||
t.Fatalf("balance=%d want 0", balance)
|
||||
}
|
||||
if cost != 10 {
|
||||
t.Fatalf("cost=%v want 10", cost)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitAllProducesFrontAndBack(t *testing.T) {
|
||||
segs := []geom.Seg{{A: geom.V(-1, -1), B: geom.V(1, 1)}}
|
||||
left, right, splits := splitAll(segs, geom.V(0, 0), geom.V(1, 0), Params{Eps: geom.EPS})
|
||||
if splits != 1 {
|
||||
t.Fatalf("splits=%d want 1", splits)
|
||||
}
|
||||
if len(left) != 1 || len(right) != 1 {
|
||||
t.Fatalf("expected split into two segments, left=%d right=%d", len(left), len(right))
|
||||
}
|
||||
share := func(a, b geom.Vec) bool {
|
||||
return math.Abs(a.X-b.X) < geom.EPS && math.Abs(a.Y-b.Y) < geom.EPS
|
||||
}
|
||||
if !share(left[0].A, right[0].B) && !share(left[0].B, right[0].A) {
|
||||
t.Fatalf("split point mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRespectsLeafAndDepth(t *testing.T) {
|
||||
segs := []geom.Seg{
|
||||
{A: geom.V(-2, -2), B: geom.V(2, -2)},
|
||||
{A: geom.V(2, -2), B: geom.V(2, 2)},
|
||||
{A: geom.V(2, 2), B: geom.V(-2, 2)},
|
||||
{A: geom.V(-2, 2), B: geom.V(-2, -2)},
|
||||
{A: geom.V(-2, -2), B: geom.V(2, 2)},
|
||||
{A: geom.V(-2, 2), B: geom.V(2, -2)},
|
||||
}
|
||||
p := Params{LeafMax: 2, MaxDepth: 3, Seed: 1, Cands: 4, Alpha: 5, Beta: 1, Eps: geom.EPS}
|
||||
root := Build(segs, p)
|
||||
if root == nil {
|
||||
t.Fatalf("Build returned nil")
|
||||
}
|
||||
|
||||
stats := Measure(root)
|
||||
if stats.MaxDepth > p.MaxDepth {
|
||||
t.Fatalf("MaxDepth=%d exceeds limit %d", stats.MaxDepth, p.MaxDepth)
|
||||
}
|
||||
if stats.Leaves == 0 {
|
||||
t.Fatalf("expected some leaves in tree")
|
||||
}
|
||||
verifyLeaves(t, root, 0, p)
|
||||
}
|
||||
|
||||
func verifyLeaves(t *testing.T, n *Node, depth int, p Params) {
|
||||
t.Helper()
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
if n.Leaf != nil {
|
||||
if depth < p.MaxDepth && len(n.Leaf.Segs) > p.LeafMax {
|
||||
t.Fatalf("leaf at depth %d has %d segs (limit %d)", depth, len(n.Leaf.Segs), p.LeafMax)
|
||||
}
|
||||
return
|
||||
}
|
||||
if depth >= p.MaxDepth {
|
||||
t.Fatalf("internal node at depth %d exceeds MaxDepth %d", depth, p.MaxDepth)
|
||||
}
|
||||
verifyLeaves(t, n.Left, depth+1, p)
|
||||
verifyLeaves(t, n.Right, depth+1, p)
|
||||
}
|
||||
83
internal/geom/geom_test.go
Normal file
83
internal/geom/geom_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package geom
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSide(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
P, O, D Vec
|
||||
want float64
|
||||
}{
|
||||
{"point left of line", V(1, 1), V(0, 0), V(1, 0), 1},
|
||||
{"point right of line", V(1, -1), V(0, 0), V(1, 0), -1},
|
||||
{"point on line", V(2, 0), V(0, 0), V(1, 0), 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := Side(tc.P, tc.O, tc.D); got < tc.want-EPS || got > tc.want+EPS {
|
||||
t.Fatalf("%s: Side()=%v want %v", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSegLineIntersect(t *testing.T) {
|
||||
O := V(0, 0)
|
||||
D := V(1, 0)
|
||||
tests := []struct {
|
||||
name string
|
||||
A, B Vec
|
||||
wantOK bool
|
||||
wantT float64
|
||||
}{
|
||||
{"hits interior", V(0, -1), V(0, 1), true, 0.5},
|
||||
{"parallel", V(0, 1), V(1, 1), false, 0},
|
||||
{"touches endpoint", V(0, 0), V(0, 1), false, 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
ok, tVal := SegLineIntersect(tc.A, tc.B, O, D)
|
||||
if ok != tc.wantOK {
|
||||
t.Fatalf("%s: ok=%v want %v", tc.name, ok, tc.wantOK)
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if diff := tVal - tc.wantT; diff > 1e-6 || diff < -1e-6 {
|
||||
t.Fatalf("%s: t=%v want %v", tc.name, tVal, tc.wantT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSeg(t *testing.T) {
|
||||
O := V(0, 0)
|
||||
D := V(1, 0)
|
||||
seg := Seg{A: V(0, -1), B: V(0, 1)}
|
||||
front, back := SplitSeg(seg, O, D)
|
||||
if len(front) != 1 || len(back) != 1 {
|
||||
t.Fatalf("expected split into two pieces got front=%d back=%d", len(front), len(back))
|
||||
}
|
||||
share := func(a, b Vec) bool {
|
||||
return math.Abs(a.X-b.X) < EPS && math.Abs(a.Y-b.Y) < EPS
|
||||
}
|
||||
if !share(front[0].A, back[0].B) && !share(front[0].B, back[0].A) {
|
||||
t.Fatalf("split point mismatch: front=%v back=%v", front[0], back[0])
|
||||
}
|
||||
|
||||
sameSide := Seg{A: V(0, 1), B: V(1, 1)}
|
||||
front, back = SplitSeg(sameSide, O, D)
|
||||
if len(back) != 0 || len(front) != 1 {
|
||||
t.Fatalf("expected no split for same side got front=%d back=%d", len(front), len(back))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBounds(t *testing.T) {
|
||||
pts := []Vec{V(-1, 2), V(3, -4), V(0, 0)}
|
||||
box := Bounds(pts)
|
||||
if box.Min != V(-1, -4) {
|
||||
t.Fatalf("Min=%v want (-1,-4)", box.Min)
|
||||
}
|
||||
if box.Max != V(3, 2) {
|
||||
t.Fatalf("Max=%v want (3,2)", box.Max)
|
||||
}
|
||||
}
|
||||
76
internal/mapfmt/map_test.go
Normal file
76
internal/mapfmt/map_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package mapfmt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseVertices(t *testing.T) {
|
||||
data := []byte{
|
||||
0, 0, 0, 0, // (0,0)
|
||||
255, 255, 10, 0, // (-1,10)
|
||||
}
|
||||
verts, err := ParseVertices(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseVertices error: %v", err)
|
||||
}
|
||||
if len(verts) != 2 {
|
||||
t.Fatalf("got %d vertices", len(verts))
|
||||
}
|
||||
if verts[1].X != -1 || verts[1].Y != 10 {
|
||||
t.Fatalf("unexpected vertex %+v", verts[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVerticesBadSize(t *testing.T) {
|
||||
if _, err := ParseVertices([]byte{0}); err == nil {
|
||||
t.Fatalf("expected error on odd sized vertex data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLinedefs(t *testing.T) {
|
||||
data := make([]byte, 14)
|
||||
data[0] = 1 // V1
|
||||
data[2] = 2 // V2
|
||||
lines, err := ParseLinedefs(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLinedefs error: %v", err)
|
||||
}
|
||||
if len(lines) != 1 {
|
||||
t.Fatalf("expected one linedef")
|
||||
}
|
||||
if lines[0].V1 != 1 || lines[0].V2 != 2 {
|
||||
t.Fatalf("unexpected linedef %+v", lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLinedefsBadSize(t *testing.T) {
|
||||
if _, err := ParseLinedefs(make([]byte, 13)); err == nil {
|
||||
t.Fatalf("expected error on odd sized linedef data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMap(t *testing.T) {
|
||||
raw := map[string][]byte{
|
||||
"VERTEXES": {0, 0, 0, 0},
|
||||
"LINEDEFS": make([]byte, 14),
|
||||
}
|
||||
m, err := LoadMap(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMap error: %v", err)
|
||||
}
|
||||
if len(m.Vertices) != 1 || len(m.Linedefs) != 1 {
|
||||
t.Fatalf("unexpected map data %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinedefsToSegs(t *testing.T) {
|
||||
verts := []Vertex{{X: 0, Y: 0}, {X: 10, Y: 0}, {X: 10, Y: 0}}
|
||||
lines := []Linedef{{V1: 0, V2: 1}, {V1: 1, V2: 2}} // second is degenerate
|
||||
segs := LinedefsToSegs(verts, lines)
|
||||
if len(segs) != 1 {
|
||||
t.Fatalf("expected one segment, got %d", len(segs))
|
||||
}
|
||||
if segs[0].A.X != 0 || segs[0].B.X != 10 {
|
||||
t.Fatalf("unexpected segment: %+v", segs[0])
|
||||
}
|
||||
}
|
||||
73
internal/viz/viz_test.go
Normal file
73
internal/viz/viz_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package viz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"bspviz/internal/bsp"
|
||||
"bspviz/internal/geom"
|
||||
"bspviz/internal/mapfmt"
|
||||
)
|
||||
|
||||
func TestEmitDOT(t *testing.T) {
|
||||
root := &bsp.Node{
|
||||
O: geom.V(0, 0),
|
||||
D: geom.V(1, 0),
|
||||
Left: &bsp.Node{
|
||||
Leaf: &bsp.Leaf{Segs: []geom.Seg{{A: geom.V(0, 0), B: geom.V(1, 1)}}},
|
||||
},
|
||||
Right: &bsp.Node{
|
||||
Leaf: &bsp.Leaf{},
|
||||
},
|
||||
}
|
||||
out := filepath.Join(t.TempDir(), "tree.dot")
|
||||
if err := EmitDOT(root, out); err != nil {
|
||||
t.Fatalf("EmitDOT error: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
expect := "digraph BSP {\n" +
|
||||
" node [fontname=\"Helvetica\"];\n" +
|
||||
" n0 [label=\"Split\\nO=(0,0) D=(1,0)\"];\n" +
|
||||
" n1 [label=\"Leaf\\nSegs=1\", shape=ellipse, style=filled, fillcolor=lightgray];\n" +
|
||||
" n2 [label=\"Leaf\\nSegs=0\", shape=ellipse, style=filled, fillcolor=lightgray];\n" +
|
||||
" n0 -> n1 [label=\"L\"];\n" +
|
||||
" n0 -> n2 [label=\"R\"];\n" +
|
||||
"}\n"
|
||||
if string(got) != expect {
|
||||
t.Fatalf("unexpected DOT output:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderPNGProducesFile(t *testing.T) {
|
||||
m := &mapfmt.MapData{
|
||||
Vertices: []mapfmt.Vertex{{X: 0, Y: 0}, {X: 64, Y: 0}, {X: 64, Y: 64}},
|
||||
Linedefs: []mapfmt.Linedef{
|
||||
{V1: 0, V2: 1},
|
||||
{V1: 1, V2: 2},
|
||||
{V1: 2, V2: 0},
|
||||
},
|
||||
}
|
||||
root := &bsp.Node{
|
||||
O: geom.V(0, 0),
|
||||
D: geom.V(1, 0),
|
||||
Left: &bsp.Node{Leaf: &bsp.Leaf{Segs: []geom.Seg{{A: geom.V(0, 0), B: geom.V(64, 64)}}}},
|
||||
Right: &bsp.Node{Leaf: &bsp.Leaf{}},
|
||||
}
|
||||
out := filepath.Join(t.TempDir(), "render.png")
|
||||
if err := RenderPNG(m, root, out); err != nil {
|
||||
t.Fatalf("RenderPNG error: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
pngMagic := []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
|
||||
if len(data) < len(pngMagic) || !bytes.Equal(data[:len(pngMagic)], pngMagic) {
|
||||
t.Fatalf("output is not a PNG")
|
||||
}
|
||||
}
|
||||
140
internal/wad/wad_test.go
Normal file
140
internal/wad/wad_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package wad
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lumpDef struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func writeTestWAD(t *testing.T, lumps []lumpDef) string {
|
||||
t.Helper()
|
||||
tmp, err := os.CreateTemp(t.TempDir(), "wad-*.wad")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTemp: %v", err)
|
||||
}
|
||||
|
||||
hdr := header{Ident: [4]byte{'P', 'W', 'A', 'D'}, NumLumps: int32(len(lumps))}
|
||||
if err := binary.Write(tmp, binary.LittleEndian, hdr); err != nil {
|
||||
t.Fatalf("write header: %v", err)
|
||||
}
|
||||
|
||||
dir := make([]DirEntry, 0, len(lumps))
|
||||
for _, l := range lumps {
|
||||
pos, err := tmp.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("seek: %v", err)
|
||||
}
|
||||
if len(l.data) > 0 {
|
||||
if _, err := tmp.Write(l.data); err != nil {
|
||||
t.Fatalf("write lump %s: %v", l.name, err)
|
||||
}
|
||||
}
|
||||
var name [8]byte
|
||||
copy(name[:], []byte(l.name))
|
||||
dir = append(dir, DirEntry{
|
||||
FilePos: int32(pos),
|
||||
Size: int32(len(l.data)),
|
||||
Name8: name,
|
||||
})
|
||||
}
|
||||
dirOffset, err := tmp.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("seek dir: %v", err)
|
||||
}
|
||||
if err := binary.Write(tmp, binary.LittleEndian, dir); err != nil {
|
||||
t.Fatalf("write dir: %v", err)
|
||||
}
|
||||
|
||||
hdr.DirOffset = int32(dirOffset)
|
||||
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatalf("seek hdr: %v", err)
|
||||
}
|
||||
if err := binary.Write(tmp, binary.LittleEndian, hdr); err != nil {
|
||||
t.Fatalf("rewrite header: %v", err)
|
||||
}
|
||||
|
||||
if err := tmp.Close(); err != nil {
|
||||
t.Fatalf("close temp wad: %v", err)
|
||||
}
|
||||
|
||||
return tmp.Name()
|
||||
}
|
||||
|
||||
func TestOpenAndReadLump(t *testing.T) {
|
||||
name := writeTestWAD(t, []lumpDef{
|
||||
{name: "MAP01", data: nil},
|
||||
{name: "DATA", data: []byte{1, 2, 3}},
|
||||
})
|
||||
w, err := Open(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = w.Close() })
|
||||
|
||||
if len(w.Dir()) != 2 {
|
||||
t.Fatalf("Dir entries=%d want 2", len(w.Dir()))
|
||||
}
|
||||
|
||||
data, _, err := w.ReadLumpByName("data")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadLumpByName: %v", err)
|
||||
}
|
||||
if !bytes.Equal(data, []byte{1, 2, 3}) {
|
||||
t.Fatalf("unexpected lump data: %v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindMapAndLoadMapLumps(t *testing.T) {
|
||||
verts := make([]byte, 4)
|
||||
lines := make([]byte, 14)
|
||||
name := writeTestWAD(t, []lumpDef{
|
||||
{name: "MAP01", data: nil},
|
||||
{name: "VERTEXES", data: verts},
|
||||
{name: "LINEDEFS", data: lines},
|
||||
{name: "MAP02", data: nil},
|
||||
})
|
||||
w, err := Open(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = w.Close() })
|
||||
|
||||
start, end, err := w.FindMap("map01")
|
||||
if err != nil {
|
||||
t.Fatalf("FindMap: %v", err)
|
||||
}
|
||||
if start != 0 || end != 3 {
|
||||
t.Fatalf("unexpected marker bounds start=%d end=%d", start, end)
|
||||
}
|
||||
|
||||
lumps, err := w.LoadMapLumps("map01", "VERTEXES", "LINEDEFS")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMapLumps: %v", err)
|
||||
}
|
||||
if len(lumps) != 2 {
|
||||
t.Fatalf("expected 2 lumps, got %d", len(lumps))
|
||||
}
|
||||
if _, ok := lumps["VERTEXES"]; !ok {
|
||||
t.Fatalf("missing VERTEXES lump")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMapLumpsMissing(t *testing.T) {
|
||||
name := writeTestWAD(t, []lumpDef{{name: "MAP01", data: nil}})
|
||||
w, err := Open(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = w.Close() })
|
||||
|
||||
if _, err := w.LoadMapLumps("map01", "VERTEXES"); err == nil {
|
||||
t.Fatalf("expected error for missing lump")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user