v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,844 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDecode_MapPointerValues tests map[string]*Struct decoding
|
||||
// This is the exact pattern used by RootConfig.States
|
||||
func TestDecode_MapPointerValues(t *testing.T) {
|
||||
data := map[string]any{
|
||||
"items": map[string]any{
|
||||
"first": map[string]any{
|
||||
"name": "alpha",
|
||||
},
|
||||
"second": map[string]any{
|
||||
"name": "beta",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Name string `toml:"name"`
|
||||
}
|
||||
type Config struct {
|
||||
Items map[string]*Item `toml:"items"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := Decode(data, &cfg); err != nil {
|
||||
t.Fatalf("Decode failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Items == nil {
|
||||
t.Fatal("Items map is nil")
|
||||
}
|
||||
if len(cfg.Items) != 2 {
|
||||
t.Fatalf("Expected 2 items, got %d", len(cfg.Items))
|
||||
}
|
||||
if cfg.Items["first"] == nil || cfg.Items["first"].Name != "alpha" {
|
||||
t.Errorf("first item mismatch: %+v", cfg.Items["first"])
|
||||
}
|
||||
if cfg.Items["second"] == nil || cfg.Items["second"].Name != "beta" {
|
||||
t.Errorf("second item mismatch: %+v", cfg.Items["second"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmarshal_DottedTableToMapPointer tests [parent.child] -> map[string]*Struct
|
||||
func TestUnmarshal_DottedTableToMapPointer(t *testing.T) {
|
||||
input := []byte(`
|
||||
[states.Gameplay]
|
||||
parent = "Root"
|
||||
|
||||
[states.TrySpawn]
|
||||
parent = "Gameplay"
|
||||
`)
|
||||
|
||||
type StateConfig struct {
|
||||
Parent string `toml:"parent"`
|
||||
}
|
||||
type Config struct {
|
||||
States map[string]*StateConfig `toml:"states"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := Unmarshal(input, &cfg); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.States == nil {
|
||||
t.Fatal("States map is nil")
|
||||
}
|
||||
if len(cfg.States) != 2 {
|
||||
t.Fatalf("Expected 2 states, got %d", len(cfg.States))
|
||||
}
|
||||
if cfg.States["Gameplay"] == nil {
|
||||
t.Fatal("Gameplay state is nil")
|
||||
}
|
||||
if cfg.States["Gameplay"].Parent != "Root" {
|
||||
t.Errorf("Gameplay.Parent mismatch: %q", cfg.States["Gameplay"].Parent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmarshal_InlineTableArray tests arrays of inline tables
|
||||
func TestUnmarshal_InlineTableArray(t *testing.T) {
|
||||
input := []byte(`
|
||||
[state]
|
||||
transitions = [
|
||||
{ trigger = "EventA", target = "StateB" },
|
||||
{ trigger = "EventB", target = "StateC", guard = "CheckX" }
|
||||
]
|
||||
`)
|
||||
|
||||
type Transition struct {
|
||||
Trigger string `toml:"trigger"`
|
||||
Target string `toml:"target"`
|
||||
Guard string `toml:"guard,omitempty"`
|
||||
}
|
||||
type State struct {
|
||||
Transitions []Transition `toml:"transitions"`
|
||||
}
|
||||
type Config struct {
|
||||
State State `toml:"state"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := Unmarshal(input, &cfg); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.State.Transitions) != 2 {
|
||||
t.Fatalf("Expected 2 transitions, got %d", len(cfg.State.Transitions))
|
||||
}
|
||||
if cfg.State.Transitions[0].Trigger != "EventA" {
|
||||
t.Errorf("Transition[0].Trigger mismatch: %q", cfg.State.Transitions[0].Trigger)
|
||||
}
|
||||
if cfg.State.Transitions[1].Guard != "CheckX" {
|
||||
t.Errorf("Transition[1].Guard mismatch: %q", cfg.State.Transitions[1].Guard)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshal_MultilineInlineTable(t *testing.T) {
|
||||
input := []byte(`
|
||||
[state]
|
||||
config = {
|
||||
name = "test",
|
||||
nested = { a = 1, b = 2 },
|
||||
array = [
|
||||
{ x = 10 },
|
||||
{ x = 20 }
|
||||
]
|
||||
}
|
||||
`)
|
||||
|
||||
type Inner struct {
|
||||
X int `toml:"x"`
|
||||
}
|
||||
type Config struct {
|
||||
Name string `toml:"name"`
|
||||
Nested map[string]int `toml:"nested"`
|
||||
Array []Inner `toml:"array"`
|
||||
}
|
||||
type State struct {
|
||||
Config Config `toml:"config"`
|
||||
}
|
||||
type Root struct {
|
||||
State State `toml:"state"`
|
||||
}
|
||||
|
||||
var cfg Root
|
||||
if err := Unmarshal(input, &cfg); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.State.Config.Name != "test" {
|
||||
t.Errorf("Name = %q", cfg.State.Config.Name)
|
||||
}
|
||||
if cfg.State.Config.Nested["a"] != 1 {
|
||||
t.Errorf("Nested.a = %d", cfg.State.Config.Nested["a"])
|
||||
}
|
||||
if len(cfg.State.Config.Array) != 2 || cfg.State.Config.Array[1].X != 20 {
|
||||
t.Errorf("Array = %+v", cfg.State.Config.Array)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshal_DeeplyNestedMultiline(t *testing.T) {
|
||||
input := []byte(`
|
||||
transition = { trigger = "Tick", target = "Active", guard = "Or", guard_args = { guards = [
|
||||
{ name = "Compare", args = { key = "val", op = "gt", value = 0 } },
|
||||
{ name = "Check", args = { flag = true } }
|
||||
]} }
|
||||
`)
|
||||
|
||||
type Args struct {
|
||||
Key string `toml:"key"`
|
||||
Op string `toml:"op"`
|
||||
Value int `toml:"value"`
|
||||
Flag bool `toml:"flag"`
|
||||
}
|
||||
type Guard struct {
|
||||
Name string `toml:"name"`
|
||||
Args Args `toml:"args"`
|
||||
}
|
||||
type GuardArgs struct {
|
||||
Guards []Guard `toml:"guards"`
|
||||
}
|
||||
type Transition struct {
|
||||
Trigger string `toml:"trigger"`
|
||||
Target string `toml:"target"`
|
||||
Guard string `toml:"guard"`
|
||||
GuardArgs GuardArgs `toml:"guard_args"`
|
||||
}
|
||||
type Root struct {
|
||||
Transition Transition `toml:"transition"`
|
||||
}
|
||||
|
||||
var cfg Root
|
||||
if err := Unmarshal(input, &cfg); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Transition.Guard != "Or" {
|
||||
t.Errorf("Guard = %q", cfg.Transition.Guard)
|
||||
}
|
||||
if len(cfg.Transition.GuardArgs.Guards) != 2 {
|
||||
t.Fatalf("Guards count = %d", len(cfg.Transition.GuardArgs.Guards))
|
||||
}
|
||||
if cfg.Transition.GuardArgs.Guards[0].Args.Op != "gt" {
|
||||
t.Errorf("Guards[0].Args.Op = %q", cfg.Transition.GuardArgs.Guards[0].Args.Op)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmarshal_FSMConfigExact tests the exact FSM config structure
|
||||
func TestUnmarshal_FSMConfigExact(t *testing.T) {
|
||||
input := []byte(`
|
||||
initial = "TrySpawnGold"
|
||||
|
||||
[states.Gameplay]
|
||||
parent = "Root"
|
||||
|
||||
[states.TrySpawnGold]
|
||||
parent = "Gameplay"
|
||||
on_enter = [
|
||||
{ action = "EmitEvent", event = "EventGoldSpawnRequest" }
|
||||
]
|
||||
transitions = [
|
||||
{ trigger = "EventGoldSpawned", target = "GoldActive" },
|
||||
{ trigger = "EventGoldSpawnFailed", target = "GoldRetryWait" }
|
||||
]
|
||||
|
||||
[states.GoldRetryWait]
|
||||
parent = "Gameplay"
|
||||
transitions = [
|
||||
{ trigger = "Tick", target = "TrySpawnGold", guard = "StateTimeExceeds", guard_args = { ms = 2000 } }
|
||||
]
|
||||
|
||||
[states.GoldActive]
|
||||
parent = "Gameplay"
|
||||
transitions = [
|
||||
{ trigger = "EventGoldCollected", target = "TrySpawnGold" }
|
||||
]
|
||||
`)
|
||||
|
||||
type ActionConfig struct {
|
||||
Action string `toml:"action"`
|
||||
Event string `toml:"event,omitempty"`
|
||||
}
|
||||
type TransitionConfig struct {
|
||||
Trigger string `toml:"trigger"`
|
||||
Target string `toml:"target"`
|
||||
Guard string `toml:"guard,omitempty"`
|
||||
GuardArgs map[string]any `toml:"guard_args,omitempty"`
|
||||
}
|
||||
type StateConfig struct {
|
||||
Parent string `toml:"parent,omitempty"`
|
||||
OnEnter []ActionConfig `toml:"on_enter,omitempty"`
|
||||
Transitions []TransitionConfig `toml:"transitions,omitempty"`
|
||||
}
|
||||
type RootConfig struct {
|
||||
InitialState string `toml:"initial"`
|
||||
States map[string]*StateConfig `toml:"states"`
|
||||
}
|
||||
|
||||
var cfg RootConfig
|
||||
if err := Unmarshal(input, &cfg); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
// Check initial
|
||||
if cfg.InitialState != "TrySpawnGold" {
|
||||
t.Errorf("InitialState mismatch: %q", cfg.InitialState)
|
||||
}
|
||||
|
||||
// Check states map
|
||||
if cfg.States == nil {
|
||||
t.Fatal("States map is nil")
|
||||
}
|
||||
if len(cfg.States) != 4 {
|
||||
t.Errorf("Expected 4 states, got %d", len(cfg.States))
|
||||
for k := range cfg.States {
|
||||
t.Logf(" Found state: %q", k)
|
||||
}
|
||||
}
|
||||
|
||||
// Check TrySpawnGold
|
||||
tsg := cfg.States["TrySpawnGold"]
|
||||
if tsg == nil {
|
||||
t.Fatal("TrySpawnGold state is nil")
|
||||
}
|
||||
if tsg.Parent != "Gameplay" {
|
||||
t.Errorf("TrySpawnGold.Parent mismatch: %q", tsg.Parent)
|
||||
}
|
||||
if len(tsg.OnEnter) != 1 {
|
||||
t.Errorf("TrySpawnGold.OnEnter count mismatch: %d", len(tsg.OnEnter))
|
||||
}
|
||||
if len(tsg.Transitions) != 2 {
|
||||
t.Errorf("TrySpawnGold.Transitions count mismatch: %d", len(tsg.Transitions))
|
||||
}
|
||||
|
||||
// Check GoldRetryWait guard_args
|
||||
grw := cfg.States["GoldRetryWait"]
|
||||
if grw == nil {
|
||||
t.Fatal("GoldRetryWait state is nil")
|
||||
}
|
||||
if len(grw.Transitions) != 1 {
|
||||
t.Fatalf("GoldRetryWait.Transitions count mismatch: %d", len(grw.Transitions))
|
||||
}
|
||||
if grw.Transitions[0].GuardArgs == nil {
|
||||
t.Error("GuardArgs is nil")
|
||||
} else if ms, ok := grw.Transitions[0].GuardArgs["ms"]; !ok {
|
||||
t.Error("GuardArgs missing 'ms' key")
|
||||
} else if msInt, ok := ms.(int); !ok || msInt != 2000 {
|
||||
t.Errorf("GuardArgs.ms mismatch: %T %v", ms, ms)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParser_DottedTableStructure verifies parser output for dotted tables
|
||||
func TestParser_DottedTableStructure(t *testing.T) {
|
||||
input := []byte(`
|
||||
[states.Alpha]
|
||||
name = "first"
|
||||
|
||||
[states.Beta]
|
||||
name = "second"
|
||||
`)
|
||||
|
||||
p := NewParser(input)
|
||||
result, err := p.Parse()
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
// Check raw parser output structure
|
||||
states, ok := result["states"]
|
||||
if !ok {
|
||||
t.Fatal("'states' key missing from parser output")
|
||||
}
|
||||
|
||||
statesMap, ok := states.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("'states' is not map[string]any, got %T", states)
|
||||
}
|
||||
|
||||
if len(statesMap) != 2 {
|
||||
t.Errorf("Expected 2 states in parser output, got %d", len(statesMap))
|
||||
}
|
||||
|
||||
alpha, ok := statesMap["Alpha"]
|
||||
if !ok {
|
||||
t.Error("'Alpha' key missing")
|
||||
}
|
||||
alphaMap, ok := alpha.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("'Alpha' is not map[string]any, got %T", alpha)
|
||||
}
|
||||
if alphaMap["name"] != "first" {
|
||||
t.Errorf("Alpha.name mismatch: %v", alphaMap["name"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecode_MapNilInitialization verifies map initialization during decode
|
||||
func TestDecode_MapNilInitialization(t *testing.T) {
|
||||
data := map[string]any{
|
||||
"items": map[string]any{
|
||||
"a": map[string]any{"val": 1},
|
||||
},
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Val int `toml:"val"`
|
||||
}
|
||||
type Config struct {
|
||||
Items map[string]*Item `toml:"items"` // nil initially
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
// cfg.Items is nil here
|
||||
|
||||
if err := Decode(data, &cfg); err != nil {
|
||||
t.Fatalf("Decode failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Items == nil {
|
||||
t.Fatal("Decode did not initialize nil map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshal_ExtremeComplexity(t *testing.T) {
|
||||
input := []byte(`
|
||||
# Root level mixed types
|
||||
version = "2.0.0-beta"
|
||||
debug = true
|
||||
tick_rate = 144
|
||||
delta_time = 0.00694
|
||||
|
||||
# Deep dotted header (5 levels)
|
||||
[engine.renderer.pipeline.stage.config]
|
||||
name = "deferred"
|
||||
priority = 1
|
||||
enabled = true
|
||||
scale_factor = 1.5e-2
|
||||
tags = ["lighting", "shadows", "post-fx"]
|
||||
|
||||
# Nested inline table inside dotted section
|
||||
[engine.renderer.pipeline.stage.config.viewport]
|
||||
width = 1920
|
||||
height = 1080
|
||||
settings = { vsync = true, hdr = false, gamma = 2.2 }
|
||||
|
||||
# Hyphenated keys at multiple levels
|
||||
[engine.audio-system.spatial-audio]
|
||||
enabled = true
|
||||
max-sources = 64
|
||||
falloff-curve = "exponential"
|
||||
rolloff-factor = 1.0e+0
|
||||
|
||||
# Map with pointer values using dotted headers
|
||||
[game.entities.player]
|
||||
health = 100
|
||||
position.x = 0.0
|
||||
position.y = -9.81e-1
|
||||
position.z = 0.0
|
||||
tags = ["controllable", "damageable"]
|
||||
inventory = { slots = 20, weight_limit = 150.5 }
|
||||
|
||||
[game.entities.enemy-boss]
|
||||
health = 5000
|
||||
position.x = 100.0
|
||||
position.y = 0.0
|
||||
position.z = -50.0
|
||||
tags = ["hostile", "boss", "damageable"]
|
||||
ai = { aggression = 0.9, patrol_radius = 25 }
|
||||
|
||||
[game.entities."فارسی-test"]
|
||||
health = 1
|
||||
position.x = 1.0
|
||||
position.y = 1.0
|
||||
position.z = 1.0
|
||||
tags = []
|
||||
|
||||
# Nested map of maps
|
||||
[game.levels.level-01.zones.spawn-area]
|
||||
bounds.min.x = -10
|
||||
bounds.min.y = 0
|
||||
bounds.min.z = -10
|
||||
bounds.max.x = 10
|
||||
bounds.max.y = 5
|
||||
bounds.max.z = 10
|
||||
enemy_count = 0
|
||||
is_safe = true
|
||||
|
||||
[game.levels.level-01.zones.combat-zone]
|
||||
bounds.min.x = 50
|
||||
bounds.min.y = 0
|
||||
bounds.min.z = 50
|
||||
bounds.max.x = 150
|
||||
bounds.max.y = 20
|
||||
bounds.max.z = 150
|
||||
enemy_count = 25
|
||||
is_safe = false
|
||||
|
||||
# Array of tables with nested complexity
|
||||
[[game.waves]]
|
||||
id = 1
|
||||
delay_ms = 0
|
||||
spawns = [
|
||||
{ entity = "enemy-grunt", count = 5, position = { x = 10.0, y = 0.0, z = 10.0 } },
|
||||
{ entity = "enemy-scout", count = 3, position = { x = -10.0, y = 0.0, z = 10.0 } }
|
||||
]
|
||||
|
||||
[[game.waves]]
|
||||
id = 2
|
||||
delay_ms = 30000
|
||||
spawns = [
|
||||
{ entity = "enemy-boss", count = 1, position = { x = 0.0, y = 0.0, z = 50.0 } }
|
||||
]
|
||||
|
||||
# Deeply nested with mixed inline and standard tables
|
||||
[physics.collision.layers.player-projectiles]
|
||||
mask = 0b1010
|
||||
priority = 10
|
||||
callbacks.on_enter = "HandleProjectileHit"
|
||||
callbacks.on_exit = "CleanupProjectile"
|
||||
|
||||
[physics.collision.layers.environment]
|
||||
mask = 0b1111
|
||||
priority = 1
|
||||
callbacks.on_enter = "HandleCollision"
|
||||
callbacks.on_exit = ""
|
||||
|
||||
# Scientific notation stress test
|
||||
[constants]
|
||||
planck = 6.62607015e-34
|
||||
c = 2.998e+8
|
||||
epsilon_0 = 8.854e-12
|
||||
very_small = 1e-100
|
||||
very_large = 1e+100
|
||||
negative_exp = -5.5e-10
|
||||
|
||||
# Empty and edge cases mixed in
|
||||
[edge.cases]
|
||||
empty_string = ""
|
||||
empty_array = []
|
||||
empty_inline = {}
|
||||
zero_int = 0
|
||||
zero_float = 0.0
|
||||
negative_int = -42
|
||||
negative_float = -273.15
|
||||
unicode_value = "日本語テスト 🎮 Ελληνικά"
|
||||
hex_val = 0xDEAD
|
||||
octal_val = 0o755
|
||||
binary_val = 0b1010
|
||||
`)
|
||||
|
||||
type Vec3 struct {
|
||||
X float64 `toml:"x"`
|
||||
Y float64 `toml:"y"`
|
||||
Z float64 `toml:"z"`
|
||||
}
|
||||
|
||||
type Bounds struct {
|
||||
Min Vec3 `toml:"min"`
|
||||
Max Vec3 `toml:"max"`
|
||||
}
|
||||
|
||||
type ViewportSettings struct {
|
||||
Vsync bool `toml:"vsync"`
|
||||
HDR bool `toml:"hdr"`
|
||||
Gamma float64 `toml:"gamma"`
|
||||
}
|
||||
|
||||
type Viewport struct {
|
||||
Width int `toml:"width"`
|
||||
Height int `toml:"height"`
|
||||
Settings ViewportSettings `toml:"settings"`
|
||||
}
|
||||
|
||||
type StageConfig struct {
|
||||
Name string `toml:"name"`
|
||||
Priority int `toml:"priority"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
ScaleFactor float64 `toml:"scale_factor"`
|
||||
Tags []string `toml:"tags"`
|
||||
Viewport Viewport `toml:"viewport"`
|
||||
}
|
||||
|
||||
type Stage struct {
|
||||
Config StageConfig `toml:"config"`
|
||||
}
|
||||
|
||||
type Pipeline struct {
|
||||
Stage Stage `toml:"stage"`
|
||||
}
|
||||
|
||||
type Renderer struct {
|
||||
Pipeline Pipeline `toml:"pipeline"`
|
||||
}
|
||||
|
||||
type SpatialAudio struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MaxSources int `toml:"max-sources"`
|
||||
FalloffCurve string `toml:"falloff-curve"`
|
||||
RolloffFactor float64 `toml:"rolloff-factor"`
|
||||
}
|
||||
|
||||
type AudioSystem struct {
|
||||
SpatialAudio SpatialAudio `toml:"spatial-audio"`
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
Renderer Renderer `toml:"renderer"`
|
||||
AudioSystem AudioSystem `toml:"audio-system"`
|
||||
}
|
||||
|
||||
type EntityConfig struct {
|
||||
Health int `toml:"health"`
|
||||
Position Vec3 `toml:"position"`
|
||||
Tags []string `toml:"tags"`
|
||||
Inventory map[string]any `toml:"inventory,omitempty"`
|
||||
AI map[string]any `toml:"ai,omitempty"`
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
Bounds Bounds `toml:"bounds"`
|
||||
EnemyCount int `toml:"enemy_count"`
|
||||
IsSafe bool `toml:"is_safe"`
|
||||
}
|
||||
|
||||
type Level struct {
|
||||
Zones map[string]*Zone `toml:"zones"`
|
||||
}
|
||||
|
||||
type SpawnPoint struct {
|
||||
Entity string `toml:"entity"`
|
||||
Count int `toml:"count"`
|
||||
Position map[string]any `toml:"position"`
|
||||
}
|
||||
|
||||
type Wave struct {
|
||||
ID int `toml:"id"`
|
||||
DelayMs int `toml:"delay_ms"`
|
||||
Spawns []SpawnPoint `toml:"spawns"`
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
Entities map[string]*EntityConfig `toml:"entities"`
|
||||
Levels map[string]*Level `toml:"levels"`
|
||||
Waves []*Wave `toml:"waves"`
|
||||
}
|
||||
|
||||
type Callbacks struct {
|
||||
OnEnter string `toml:"on_enter"`
|
||||
OnExit string `toml:"on_exit"`
|
||||
}
|
||||
|
||||
type CollisionLayer struct {
|
||||
Mask int `toml:"mask"`
|
||||
Priority int `toml:"priority"`
|
||||
Callbacks Callbacks `toml:"callbacks"`
|
||||
}
|
||||
|
||||
type Collision struct {
|
||||
Layers map[string]*CollisionLayer `toml:"layers"`
|
||||
}
|
||||
|
||||
type Physics struct {
|
||||
Collision Collision `toml:"collision"`
|
||||
}
|
||||
|
||||
type Constants struct {
|
||||
Planck float64 `toml:"planck"`
|
||||
C float64 `toml:"c"`
|
||||
Epsilon0 float64 `toml:"epsilon_0"`
|
||||
VerySmall float64 `toml:"very_small"`
|
||||
VeryLarge float64 `toml:"very_large"`
|
||||
NegativeExp float64 `toml:"negative_exp"`
|
||||
}
|
||||
|
||||
type EdgeCases struct {
|
||||
EmptyString string `toml:"empty_string"`
|
||||
EmptyArray []any `toml:"empty_array"`
|
||||
EmptyInline map[string]any `toml:"empty_inline"`
|
||||
ZeroInt int `toml:"zero_int"`
|
||||
ZeroFloat float64 `toml:"zero_float"`
|
||||
NegativeInt int `toml:"negative_int"`
|
||||
NegativeFloat float64 `toml:"negative_float"`
|
||||
UnicodeValue string `toml:"unicode_value"`
|
||||
HexVal int `toml:"hex_val"`
|
||||
OctalVal int `toml:"octal_val"`
|
||||
BinaryVal int `toml:"binary_val"`
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Cases EdgeCases `toml:"cases"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Version string `toml:"version"`
|
||||
Debug bool `toml:"debug"`
|
||||
TickRate int `toml:"tick_rate"`
|
||||
DeltaTime float64 `toml:"delta_time"`
|
||||
Engine Engine `toml:"engine"`
|
||||
Game Game `toml:"game"`
|
||||
Physics Physics `toml:"physics"`
|
||||
Constants Constants `toml:"constants"`
|
||||
Edge Edge `toml:"edge"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := Unmarshal(input, &cfg); err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
// Root level
|
||||
if cfg.Version != "2.0.0-beta" {
|
||||
t.Errorf("Version = %q", cfg.Version)
|
||||
}
|
||||
if !cfg.Debug {
|
||||
t.Error("Debug should be true")
|
||||
}
|
||||
if cfg.TickRate != 144 {
|
||||
t.Errorf("TickRate = %d", cfg.TickRate)
|
||||
}
|
||||
|
||||
// 5-level deep dotted header
|
||||
sc := cfg.Engine.Renderer.Pipeline.Stage.Config
|
||||
if sc.Name != "deferred" {
|
||||
t.Errorf("Stage.Config.Name = %q", sc.Name)
|
||||
}
|
||||
if sc.ScaleFactor != 1.5e-2 {
|
||||
t.Errorf("ScaleFactor = %e", sc.ScaleFactor)
|
||||
}
|
||||
if len(sc.Tags) != 3 || sc.Tags[1] != "shadows" {
|
||||
t.Errorf("Stage tags = %v", sc.Tags)
|
||||
}
|
||||
if sc.Viewport.Width != 1920 {
|
||||
t.Errorf("Viewport.Width = %d", sc.Viewport.Width)
|
||||
}
|
||||
if sc.Viewport.Settings.Gamma != 2.2 {
|
||||
t.Errorf("Viewport.Settings.Gamma = %f", sc.Viewport.Settings.Gamma)
|
||||
}
|
||||
|
||||
// Hyphenated keys
|
||||
sa := cfg.Engine.AudioSystem.SpatialAudio
|
||||
if sa.MaxSources != 64 {
|
||||
t.Errorf("MaxSources = %d", sa.MaxSources)
|
||||
}
|
||||
if sa.FalloffCurve != "exponential" {
|
||||
t.Errorf("FalloffCurve = %q", sa.FalloffCurve)
|
||||
}
|
||||
|
||||
// Map pointer values with dotted keys inside
|
||||
player := cfg.Game.Entities["player"]
|
||||
if player == nil {
|
||||
t.Fatal("player entity nil")
|
||||
}
|
||||
if player.Health != 100 {
|
||||
t.Errorf("player.Health = %d", player.Health)
|
||||
}
|
||||
if player.Position.Y != -9.81e-1 {
|
||||
t.Errorf("player.Positions.Y = %e", player.Position.Y)
|
||||
}
|
||||
if len(player.Tags) != 2 {
|
||||
t.Errorf("player.Tags = %v", player.Tags)
|
||||
}
|
||||
|
||||
boss := cfg.Game.Entities["enemy-boss"]
|
||||
if boss == nil {
|
||||
t.Fatal("enemy-boss entity nil")
|
||||
}
|
||||
if boss.Health != 5000 {
|
||||
t.Errorf("boss.Health = %d", boss.Health)
|
||||
}
|
||||
|
||||
// Unicode key (edge case)
|
||||
unicode := cfg.Game.Entities["فارسی-test"]
|
||||
if unicode == nil {
|
||||
t.Fatal("unicode entity nil")
|
||||
}
|
||||
if unicode.Health != 1 {
|
||||
t.Errorf("unicode.Health = %d", unicode.Health)
|
||||
}
|
||||
|
||||
// Deeply nested map of maps
|
||||
lvl := cfg.Game.Levels["level-01"]
|
||||
if lvl == nil {
|
||||
t.Fatal("level-01 nil")
|
||||
}
|
||||
spawn := lvl.Zones["spawn-area"]
|
||||
if spawn == nil {
|
||||
t.Fatal("spawn-area nil")
|
||||
}
|
||||
if spawn.Bounds.Min.X != -10 {
|
||||
t.Errorf("spawn.Bounds.Min.X = %f", spawn.Bounds.Min.X)
|
||||
}
|
||||
if spawn.Bounds.Max.Y != 5 {
|
||||
t.Errorf("spawn.Bounds.Max.Y = %f", spawn.Bounds.Max.Y)
|
||||
}
|
||||
if !spawn.IsSafe {
|
||||
t.Error("spawn.IsSafe should be true")
|
||||
}
|
||||
|
||||
combat := lvl.Zones["combat-zone"]
|
||||
if combat == nil {
|
||||
t.Fatal("combat-zone nil")
|
||||
}
|
||||
if combat.EnemyCount != 25 {
|
||||
t.Errorf("combat.EnemyCount = %d", combat.EnemyCount)
|
||||
}
|
||||
|
||||
// Array of tables with pointer slice
|
||||
if len(cfg.Game.Waves) != 2 {
|
||||
t.Fatalf("Waves count = %d", len(cfg.Game.Waves))
|
||||
}
|
||||
w1 := cfg.Game.Waves[0]
|
||||
if w1.ID != 1 || w1.DelayMs != 0 {
|
||||
t.Errorf("Wave[0] = %+v", w1)
|
||||
}
|
||||
if len(w1.Spawns) != 2 {
|
||||
t.Errorf("Wave[0].Spawns count = %d", len(w1.Spawns))
|
||||
}
|
||||
if w1.Spawns[0].Entity != "enemy-grunt" || w1.Spawns[0].Count != 5 {
|
||||
t.Errorf("Wave[0].Spawns[0] = %+v", w1.Spawns[0])
|
||||
}
|
||||
|
||||
w2 := cfg.Game.Waves[1]
|
||||
if w2.DelayMs != 30000 {
|
||||
t.Errorf("Wave[1].DelayMs = %d", w2.DelayMs)
|
||||
}
|
||||
|
||||
// Collision layers map
|
||||
projLayer := cfg.Physics.Collision.Layers["player-projectiles"]
|
||||
if projLayer == nil {
|
||||
t.Fatal("player-projectiles layer nil")
|
||||
}
|
||||
if projLayer.Mask != 0b1010 {
|
||||
t.Errorf("projLayer.Mask = %d", projLayer.Mask)
|
||||
}
|
||||
if projLayer.Callbacks.OnEnter != "HandleProjectileHit" {
|
||||
t.Errorf("projLayer.Callbacks.OnEnter = %q", projLayer.Callbacks.OnEnter)
|
||||
}
|
||||
|
||||
// Scientific notation
|
||||
if cfg.Constants.Planck != 6.62607015e-34 {
|
||||
t.Errorf("Planck = %e", cfg.Constants.Planck)
|
||||
}
|
||||
if cfg.Constants.C != 2.998e+8 {
|
||||
t.Errorf("C = %e", cfg.Constants.C)
|
||||
}
|
||||
if cfg.Constants.VerySmall != 1e-100 {
|
||||
t.Errorf("VerySmall = %e", cfg.Constants.VerySmall)
|
||||
}
|
||||
if cfg.Constants.NegativeExp != -5.5e-10 {
|
||||
t.Errorf("NegativeExp = %e", cfg.Constants.NegativeExp)
|
||||
}
|
||||
|
||||
// Edge cases
|
||||
if cfg.Edge.Cases.EmptyString != "" {
|
||||
t.Errorf("EmptyString = %q", cfg.Edge.Cases.EmptyString)
|
||||
}
|
||||
if len(cfg.Edge.Cases.EmptyArray) != 0 {
|
||||
t.Errorf("EmptyArray = %v", cfg.Edge.Cases.EmptyArray)
|
||||
}
|
||||
if len(cfg.Edge.Cases.EmptyInline) != 0 {
|
||||
t.Errorf("EmptyInline = %v", cfg.Edge.Cases.EmptyInline)
|
||||
}
|
||||
if cfg.Edge.Cases.NegativeInt != -42 {
|
||||
t.Errorf("NegativeInt = %d", cfg.Edge.Cases.NegativeInt)
|
||||
}
|
||||
if cfg.Edge.Cases.NegativeFloat != -273.15 {
|
||||
t.Errorf("NegativeFloat = %f", cfg.Edge.Cases.NegativeFloat)
|
||||
}
|
||||
if cfg.Edge.Cases.UnicodeValue != "日本語テスト 🎮 Ελληνικά" {
|
||||
t.Errorf("UnicodeValue = %q", cfg.Edge.Cases.UnicodeValue)
|
||||
}
|
||||
if cfg.Edge.Cases.HexVal != 0xDEAD {
|
||||
t.Errorf("HexVal = %d, want %d", cfg.Edge.Cases.HexVal, 0xDEAD)
|
||||
}
|
||||
if cfg.Edge.Cases.OctalVal != 0o755 {
|
||||
t.Errorf("OctalVal = %d, want %d", cfg.Edge.Cases.OctalVal, 0o755)
|
||||
}
|
||||
if cfg.Edge.Cases.BinaryVal != 0b1010 {
|
||||
t.Errorf("BinaryVal = %d, want %d", cfg.Edge.Cases.BinaryVal, 0b1010)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user