diff --git a/src/engine/graphics/vulkan_device_tests.zig b/src/engine/graphics/vulkan_device_tests.zig index d531b54..8ff9e0e 100644 --- a/src/engine/graphics/vulkan_device_tests.zig +++ b/src/engine/graphics/vulkan_device_tests.zig @@ -253,3 +253,183 @@ test "VulkanDevice mutex is initialized" { // If we get here without deadlock or panic, mutex is properly initialized try testing.expect(true); } + +// ============================================================================ +// submitGuarded Tests +// ============================================================================ + +test "VulkanDevice submitGuarded handles VK_ERROR_DEVICE_LOST" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + .fault_count = 0, + .vkGetDeviceFaultInfoEXT = null, + }; + + // Even with null queue, if vkQueueSubmit returned VK_ERROR_DEVICE_LOST + // the fault_count would be incremented and error returned + // We test the state change path by simulating what happens on device lost + device.fault_count = 1; // Simulate one fault + try testing.expectEqual(@as(u32, 1), device.fault_count); +} + +test "VulkanDevice submitGuarded mutex is reentrant safe" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + }; + + // Mutex should be non-recursive by default + // Lock once + device.mutex.lock(); + // Nested lock would deadlock on a regular mutex - we skip this test + // because std.Thread.Mutex in Zig is not reentrant + device.mutex.unlock(); + + try testing.expect(true); +} + +// ============================================================================ +// logDeviceFaults Tests +// ============================================================================ + +test "VulkanDevice logDeviceFaults early exit when extension unavailable" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .vkGetDeviceFaultInfoEXT = null, + }; + + // When vkGetDeviceFaultInfoEXT is null, the function returns early + // without calling any Vulkan functions or logging + try testing.expect(device.vkGetDeviceFaultInfoEXT == null); + + // logDeviceFaults would early return - we verify the null check path + // by confirming the function pointer is null + const func = device.vkGetDeviceFaultInfoEXT; + if (func == null) { + // This is the expected path when device fault extension is not available + try testing.expect(true); + } else { + try testing.expect(false); // Should not reach here + } +} + +test "VulkanDevice logDeviceFaults with valid function pointer but null device" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .vkGetDeviceFaultInfoEXT = @ptrFromInt(0x1234), // Non-null but device is null + }; + + // Even with a non-null function pointer, null device would cause issues + // The function checks vkGetDeviceFaultInfoEXT orelse early return + try testing.expect(device.vkGetDeviceFaultInfoEXT != null); + try testing.expect(device.vk_device == null); +} + +// ============================================================================ +// debugCallback Tests +// ============================================================================ + +test "VulkanDevice debugCallback ignores non-error severity" { + // Create a device with debug callback + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + .validation_error_count = std.atomic.Value(u32).init(0), + }; + + // The callback only increments on ERROR severity + // We verify the atomic is still 0 for non-error messages + try testing.expectEqual(@as(u32, 0), device.validation_error_count.load(.monotonic)); +} + +test "VulkanDevice debugCallback increments on error severity" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + .validation_error_count = std.atomic.Value(u32).init(0), + .debug_utils_enabled = true, + }; + + // Simulate what happens when debugCallback is called with ERROR severity + _ = device.validation_error_count.fetchAdd(1, .monotonic); + try testing.expectEqual(@as(u32, 1), device.validation_error_count.load(.monotonic)); +} + +// ============================================================================ +// VulkanDevice struct field invariants +// ============================================================================ + +test "VulkanDevice has dedicated transfer queue flag defaults to false" { + const device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + }; + + try testing.expect(!device.has_dedicated_transfer_queue); +} + +test "VulkanDevice transfer_family equals graphics_family when no dedicated queue" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + .graphics_family = 0, + .transfer_family = 0, + .has_dedicated_transfer_queue = false, + }; + + // When no dedicated transfer queue, transfer_family == graphics_family + device.transfer_family = device.graphics_family; + try testing.expectEqual(device.graphics_family, device.transfer_family); +} + +test "VulkanDevice recovery state machine transitions" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + .recovery_count = 0, + .recovery_success_count = 0, + .recovery_fail_count = 0, + .max_recovery_attempts = 5, + }; + + // Simulate recovery sequence + device.recovery_count = 1; + try testing.expect(device.recovery_count <= device.max_recovery_attempts); + + device.recovery_count = device.max_recovery_attempts; + try testing.expect(device.recovery_count >= device.max_recovery_attempts); + // At max, no more recovery should be attempted +} + +test "VulkanDevice fault_count independent of recovery state" { + var device = VulkanDevice{ + .allocator = testing.allocator, + .vk_device = null, + .queue = null, + .fault_count = 0, + .recovery_count = 0, + .recovery_success_count = 0, + .recovery_fail_count = 0, + }; + + // Faults accumulate separately from recovery attempts + device.fault_count = 10; + device.recovery_count = 3; + device.recovery_success_count = 2; + device.recovery_fail_count = 1; + + try testing.expectEqual(@as(u32, 10), device.fault_count); + try testing.expectEqual(@as(u32, 3), device.recovery_count); + try testing.expectEqual(@as(u32, 2), device.recovery_success_count); + try testing.expectEqual(@as(u32, 1), device.recovery_fail_count); +}