From fca6bd0945762165085ab09f910b11f95c287903 Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Sat, 1 Aug 2026 00:59:18 -0700 Subject: [PATCH 1/2] add unit tests --- Makefile | 6 ++- engine/character_data.odin | 14 +++++- engine/character_data_test.odin | 83 +++++++++++++++++++++++++++++++++ engine/shader_backend.odin | 51 ++++++++++---------- engine/shader_backend_test.odin | 43 +++++++++++++++++ engine/sprite_test.odin | 21 +++++++++ 6 files changed, 191 insertions(+), 27 deletions(-) create mode 100644 engine/character_data_test.odin create mode 100644 engine/shader_backend_test.odin create mode 100644 engine/sprite_test.odin diff --git a/Makefile b/Makefile index 70fa75b..ef409f7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: shaders-vulkan shaders-d3d12 shaders-metal shaders-all toad check help +.PHONY: shaders-vulkan shaders-d3d12 shaders-metal shaders-all toad check test help help: @echo "Targets:" @@ -6,6 +6,7 @@ help: @echo " shaders-d3d12 Compile DXIL into shaders/d3d12/ (needs shadercross)" @echo " shaders-metal Compile MSL into shaders/metal/ (needs shadercross)" @echo " shaders-all Build all shader backends" + @echo " test Run engine pure unit tests" @echo " check Typecheck examples/toad" @echo " toad Run the toad example" @@ -20,6 +21,9 @@ shaders-metal: shaders-all: shaders-vulkan shaders-d3d12 shaders-metal +test: + odin test engine + check: odin check examples/toad -collection:pkg=. diff --git a/engine/character_data.odin b/engine/character_data.odin index b378bd9..6fe670e 100644 --- a/engine/character_data.odin +++ b/engine/character_data.odin @@ -36,6 +36,15 @@ Character_Data :: struct { height: int, } +parse_char_def :: proc(file_data: []u8) -> (Char_Def, bool) { + def: Char_Def + if err := json.unmarshal(file_data, &def); err != nil { + fmt.eprintfln("json unmarshal failed: %v", err) + return {}, false + } + return def, true +} + load_character_data :: proc(app: ^App, character_json_path: string) -> (Character_Data, bool) { file_data, read_err := os.read_entire_file(character_json_path, context.allocator) if read_err != nil { @@ -46,8 +55,9 @@ load_character_data :: proc(app: ^App, character_json_path: string) -> (Characte defer delete(file_data) out: Character_Data - if err := json.unmarshal(file_data, &out.def); err != nil { - fmt.eprintfln("json unmarshal failed: %v", err) + ok: bool + out.def, ok = parse_char_def(file_data) + if !ok { return {}, false } diff --git a/engine/character_data_test.odin b/engine/character_data_test.odin new file mode 100644 index 0000000..7e91e89 --- /dev/null +++ b/engine/character_data_test.odin @@ -0,0 +1,83 @@ +package engine + +import "core:testing" + +@(test) +parse_char_def_happy_path :: proc(t: ^testing.T) { + src := transmute([]u8)string( + `{"version":1,"id":"toad","atlas":"toad.atlas.png","pixels_per_unit":160.0,"pivot":[0.5,1.0],"clips":{"idle":{"loop":true,"fps":8.0,"frames":[{"rect":[1,2,3,4],"source_size":[3,4],"trim_offset":[0,0]}]}}}`, + ) + + def, ok := parse_char_def(src) + defer destroy_char_def(&def) + + testing.expect(t, ok) + testing.expect_value(t, def.id, "toad") + testing.expect_value(t, def.atlas, "toad.atlas.png") + testing.expect_value(t, def.version, 1) + testing.expect_value(t, def.pixels_per_unit, f32(160.0)) + testing.expect_value(t, def.pivot[0], f32(0.5)) + testing.expect_value(t, def.pivot[1], f32(1.0)) + + idle, found := def.clips["idle"] + testing.expect(t, found) + testing.expect(t, idle.loop) + testing.expect_value(t, idle.fps, f32(8.0)) + testing.expect_value(t, len(idle.frames), 1) + testing.expect_value(t, idle.frames[0].rect, [4]int{1, 2, 3, 4}) +} + +@(test) +parse_char_def_bad_json :: proc(t: ^testing.T) { + _, ok := parse_char_def(transmute([]u8)string("{ not json")) + testing.expect(t, !ok) +} + +@(test) +character_frame_rect_hit_and_miss :: proc(t: ^testing.T) { + data: Character_Data + data.def.clips = make(map[string]Clip_Def) + defer delete(data.def.clips) + + frames := make([]Frame_Def, 2) + frames[0] = {rect = {10, 20, 30, 40}} + frames[1] = {rect = {50, 60, 70, 80}} + defer delete(frames) + + data.def.clips["idle"] = Clip_Def { + loop = true, + fps = 10, + frames = frames, + } + + rect, ok := character_frame_rect(&data, "idle", 1) + testing.expect(t, ok) + testing.expect_value(t, rect, [4]int{50, 60, 70, 80}) + + _, ok = character_frame_rect(&data, "missing", 0) + testing.expect(t, !ok) + + _, ok = character_frame_rect(&data, "idle", -1) + testing.expect(t, !ok) + + _, ok = character_frame_rect(&data, "idle", 2) + testing.expect(t, !ok) +} + +destroy_char_def :: proc(def: ^Char_Def) { + if def == nil do return + + keys := make([dynamic]string, context.temp_allocator) + for key, clip in def.clips { + delete(clip.frames) + append(&keys, key) + } + for key in keys { + delete_key(&def.clips, key) + delete(key) + } + delete(def.clips) + delete(def.id) + delete(def.atlas) + def^ = {} +} diff --git a/engine/shader_backend.odin b/engine/shader_backend.odin index e023140..20b2c25 100644 --- a/engine/shader_backend.odin +++ b/engine/shader_backend.odin @@ -4,43 +4,46 @@ import "core:fmt" import "core:os" import sdl "vendor:sdl3" -choose_shader_runtime :: proc(device: ^sdl.GPUDevice) -> (Shader_Runtime, bool) { - supported := sdl.GetGPUShaderFormats(device) - +choose_shader_runtime_from_formats :: proc( + supported: sdl.GPUShaderFormat, +) -> ( + Shader_Runtime, + bool, +) { if .MSL in supported { return { - backend = .Metal_MSL, - format = {.MSL}, - shader_dir = "shaders/metal", - entrypoint = "main", - }, - true - + backend = .Metal_MSL, + format = {.MSL}, + shader_dir = "shaders/metal", + entrypoint = "main", + }, true } if .DXIL in supported { return { - backend = .DSD12_DXIL, - format = {.DXIL}, - shader_dir = "shaders/d3d12", - entrypoint = "main", - }, - true - + backend = .DSD12_DXIL, + format = {.DXIL}, + shader_dir = "shaders/d3d12", + entrypoint = "main", + }, true } if .SPIRV in supported { return { - backend = .Vulkan_SPIRV, - format = {.SPIRV}, - shader_dir = "shaders/vulkan", - entrypoint = "main", - }, - true + backend = .Vulkan_SPIRV, + format = {.SPIRV}, + shader_dir = "shaders/vulkan", + entrypoint = "main", + }, true } - fmt.eprintfln("No supported shader format from device {got %v}", supported) + fmt.eprintfln("No supported shader format from device (got %v)", supported) return {}, false } +choose_shader_runtime :: proc(device: ^sdl.GPUDevice) -> (Shader_Runtime, bool) { + supported := sdl.GetGPUShaderFormats(device) + return choose_shader_runtime_from_formats(supported) +} + load_shader_blob :: proc(path: string) -> ([]u8, bool) { data, err := os.read_entire_file(path, context.allocator) if err != nil { diff --git a/engine/shader_backend_test.odin b/engine/shader_backend_test.odin new file mode 100644 index 0000000..69cb1fe --- /dev/null +++ b/engine/shader_backend_test.odin @@ -0,0 +1,43 @@ +package engine + +import "core:testing" +import sdl "vendor:sdl3" + +@(test) +shader_filenames_per_backend :: proc(t: ^testing.T) { + vert, frag := shader_filenames(.Vulkan_SPIRV) + testing.expect_value(t, vert, "sprite.vert.spv") + testing.expect_value(t, frag, "sprite.frag.spv") + + vert, frag = shader_filenames(.DSD12_DXIL) + testing.expect_value(t, vert, "sprite.vert.dxil") + testing.expect_value(t, frag, "sprite.frag.dxil") + + vert, frag = shader_filenames(.Metal_MSL) + testing.expect_value(t, vert, "sprite.vert.msl") + testing.expect_value(t, frag, "sprite.frag.msl") +} + +@(test) +choose_shader_runtime_prefers_msl :: proc(t: ^testing.T) { + rt, ok := choose_shader_runtime_from_formats({.MSL, .SPIRV, .DXIL}) + testing.expect(t, ok) + testing.expect_value(t, rt.backend, Shader_Backend.Metal_MSL) + testing.expect_value(t, rt.shader_dir, "shaders/metal") + testing.expect_value(t, rt.format, sdl.GPUShaderFormat{.MSL}) +} + +@(test) +choose_shader_runtime_spirv_only :: proc(t: ^testing.T) { + rt, ok := choose_shader_runtime_from_formats({.SPIRV}) + testing.expect(t, ok) + testing.expect_value(t, rt.backend, Shader_Backend.Vulkan_SPIRV) + testing.expect_value(t, rt.shader_dir, "shaders/vulkan") + testing.expect_value(t, rt.format, sdl.GPUShaderFormat{.SPIRV}) +} + +@(test) +choose_shader_runtime_empty_fails :: proc(t: ^testing.T) { + _, ok := choose_shader_runtime_from_formats({}) + testing.expect(t, !ok) +} diff --git a/engine/sprite_test.odin b/engine/sprite_test.odin new file mode 100644 index 0000000..a6c6fa9 --- /dev/null +++ b/engine/sprite_test.odin @@ -0,0 +1,21 @@ +package engine + +import "core:testing" + +@(test) +to_clip_corners :: proc(t: ^testing.T) { + // top-left pixel (0,0) -> clip (-1, 1) with y-flip + p := to_clip(0, 0, 800, 600) + testing.expect_value(t, p[0], f32(-1)) + testing.expect_value(t, p[1], f32(1)) + + // bottom-right pixel (sw, sh) -> clip (1, -1) + p = to_clip(800, 600, 800, 600) + testing.expect_value(t, p[0], f32(1)) + testing.expect_value(t, p[1], f32(-1)) + + // center + p = to_clip(400, 300, 800, 600) + testing.expect_value(t, p[0], f32(0)) + testing.expect_value(t, p[1], f32(0)) +} From 71d3ce40417d200827a3f4563af1b48c2a7761c7 Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Sat, 1 Aug 2026 01:03:16 -0700 Subject: [PATCH 2/2] small edit to makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ef409f7..93ac37c 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ help: @echo " shaders-d3d12 Compile DXIL into shaders/d3d12/ (needs shadercross)" @echo " shaders-metal Compile MSL into shaders/metal/ (needs shadercross)" @echo " shaders-all Build all shader backends" - @echo " test Run engine pure unit tests" + @echo " test Run engine unit tests" @echo " check Typecheck examples/toad" @echo " toad Run the toad example"