initial commit

This commit is contained in:
2026-07-23 21:23:01 +00:00
commit c6c309f941
25 changed files with 6825 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
local utils = test_helper.utils
describe('Edge Cases', function()
local core, cat, house, ui, voxel, json
setup(function()
mocks.setupFilesystem()
core = require('src.core')
cat = require('src.cat.init')
house = require('src.house.init')
ui = require('src.ui.init')
voxel = require('src.voxel.init')
json = require('src.json')
end)
it('should handle zero cats', function()
core.state.cats = {}
core.calculateHouseHappiness()
assert.are.equal(0, core.state.house.totalHappiness)
end)
it('should handle zero distance between cats', function()
local c1 = utils.createMockCat({ position = { x = 0, y = 0, z = 0 } })
local c2 = utils.createMockCat({ position = { x = 0, y = 0, z = 0 } })
c1:updateNeeds()
c2:updateNeeds()
end)
it('should handle nil personality', function()
local c = cat.createCat(1, 'Test', nil)
assert.are.equal('social', c.personality.id)
end)
it('should handle invalid JSON', function()
local decoded, err = json.decode('{ invalid json }')
assert.are_equal(nil, decoded)
assert.are_not_equal(nil, err)
end)
it('should handle max capacity', function()
core.state.cats = {}
for i = 1, 8 do
core.spawnCat()
end
assert.are.equal(8, #core.state.cats)
-- Should not exceed max
local initialCount = #core.state.cats
core.spawnCat()
assert.are.equal(initialCount, #core.state.cats)
end)
it('should handle furniture at grid bounds', function()
local canPlace, reason = house.canPlaceFurniture({ x = 2.25, y = 0, z = 2.25 }, { 0.5, 1.5, 0.5 })
assert.is_true(canPlace)
end)
it('should handle low happiness departure', function()
local catObj = utils.createMockCat({
needs = { comfort = 0.05, hunger = 0.5 }
})
table.insert(core.state.cats, catObj)
local initialCount = #core.state.cats
core.checkCatDepartures()
assert.is_true(#core.state.cats < initialCount)
end)
it('should handle furniture upgrades', function()
local furn = utils.createMockFurniture()
furn:upgrade()
assert.are.equal(2, furn.upgradeLevel)
furn:upgrade()
assert.are.equal(3, furn.upgradeLevel)
local result = furn:upgrade()
assert.is_false(result)
end)
it('should handle personality compatibility', function()
local c1 = utils.createMockCat({
personality = { id = 'lone_wolf', compatibility = { ['social'] = -0.5 } }
})
local c2 = utils.createMockCat({
personality = { id = 'social' },
position = { x = 0.5, y = 0, z = 0 }
})
local oldSocial = c1.needs.social
c1:updateNeeds()
assert.is_true(c1.needs.social < oldSocial)
end)
it('should handle audio generation', function()
local purr = audio.createPurrSound()
assert.are_not_equal(nil, purr)
local meow = audio.createMeowSound()
assert.are_not_equal(nil, meow)
local click = audio.createClickSound()
assert.are_not_equal(nil, click)
local ambient = audio.createAmbientSound()
assert.are_not_equal(nil, ambient)
end)
it('should handle serialization edge cases', function()
local c = utils.createMockCat()
c.personality = nil
local serialized = c:serialize()
assert.are_equal('social', serialized.personalityId)
end)
it('should handle rapid updates', function()
core.state.cats = {}
for i = 1, 8 do
table.insert(core.state.cats, utils.createMockCat())
end
for i = 1, 100 do
core.update(0.016)
end
assert.is_true(#core.state.cats > 0)
end)
it('should handle 25+ furniture items', function()
core.state.furniture = {}
for i = 1, 25 do
local furn = utils.createMockFurniture({
position = { x = i * 0.5, y = 0, z = 0 }
})
table.insert(core.state.furniture, furn)
end
assert.are.equal(25, #core.state.furniture)
end)
it('should handle multiple save/load cycles', function()
for i = 1, 5 do
core.state.cats = {}
core.state.furniture = {}
core.spawnCat()
core.addFurniture('bed', { x = 0, y = 0, z = 0 })
core.saveGame()
core.loadGame()
end
end)
it('should handle nil core in all modules', function()
_G.core = nil
cat.updateCats(0.1)
house.checkCollision({ 0, 0, 0 }, { 1, 1, 1 })
house.canPlaceFurniture({ 0, 0, 0 }, { 1, 1, 1 })
house.placeFurniture('bed', { 0, 0, 0 })
house.interactWithFurniture({ 0, 0, 0 })
ui.renderHUD(test_helper.utils.createMockPass())
ui.renderInventory(test_helper.utils.createMockPass(), 1280, 720, 0.72)
ui.renderCatLog(test_helper.utils.createMockPass(), 1280, 0.72)
ui.handleMousePress(100, 100, 1)
voxel.renderWorld(test_helper.utils.createMockPass())
voxel.renderFurniture(test_helper.utils.createMockPass())
voxel.renderCats(test_helper.utils.createMockPass())
voxel.raycast({ 0, 0, 0 }, { 0, 0, -1 }, 10)
audio.playPurr({ 0, 0, 0 })
audio.playMeow({ 0, 0, 0 })
audio.playClick()
end)
it('should handle nil house module', function()
_G.house = nil
core.addFurniture('bed', { 0, 0, 0 })
end)
it('should handle nil ui module', function()
_G.ui = nil
core.update(0.1)
end)
it('should handle nil voxel module', function()
_G.voxel = nil
core.update(0.1)
end)
it('should handle nil audio module', function()
_G.audio = nil
core.update(0.1)
end)
it('should handle nil cat module', function()
_G.cat = nil
core.update(0.1)
end)
it('should handle zero distance in movement', function()
local c = utils.createMockCat({
position = { x = 0, y = 0, z = 0 },
targetPosition = { x = 0, y = 0, z = 0 }
})
c.state = 'moving'
c:update(0.1)
end)
it('should handle nil furniture in raycast', function()
core.state.furniture = nil
local results = voxel.raycast({ 0, 0, 0 }, { 0, 0, -1 }, 10)
assert.are.equal('table', type(results))
end)
it('should handle nil cats in raycast', function()
core.state.cats = nil
local results = voxel.raycast({ 0, 0, 0 }, { 0, 0, -1 }, 10)
assert.are.equal('table', type(results))
end)
it('should handle nil core in JSON encoding', function()
local encoded = json.encode({ core = core })
assert.are_not_equal(nil, encoded:find('version'))
end)
it('should handle nil core in JSON decoding', function()
local decoded = json.decode(encoded)
assert.are_not_equal(nil, decoded)
end)
it('should handle empty core state', function()
core.state = {}
core.calculateHouseHappiness()
assert.are.equal(0, core.state.house.totalHappiness)
end)
it('should handle nil core.state', function()
core.state = nil
core.update(0.1)
end)
it('should handle nil core.CONFIG', function()
core.CONFIG = nil
core.update(0.1)
end)
it('should handle nil core.personalities', function()
core.personalities = nil
local c = cat.createCat(1, 'Test', nil)
assert.are_equal('social', c.personality.id)
end)
it('should handle nil core.furnitureTypes', function()
core.furnitureTypes = nil
local furn = house.placeFurniture('bed', { 0, 0, 0 })
assert.are_equal(nil, furn)
end)
end)
+189
View File
@@ -0,0 +1,189 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
describe('Integration: Game Flow', function()
local core, cat, house, ui, voxel, audio, json
setup(function()
mocks.setupFilesystem()
core = require('src.core')
cat = require('src.cat.init')
house = require('src.house.init')
ui = require('src.ui.init')
voxel = require('src.voxel.init')
audio = require('src.audio.init')
json = require('src.json')
end)
it('should initialize full game', function()
core.init()
assert.are.equal(2, #core.state.cats)
assert.are.equal(0, #core.state.furniture)
end)
it('should add furniture and render', function()
core.init()
local furn = core.addFurniture('bed', { x = 0, y = 0, z = 0 })
assert.are_not.equal(nil, furn)
local pass = test_helper.utils.createMockPass()
voxel.renderFurniture(pass)
end)
it('should update cats and render', function()
core.init()
cat.updateCats(0.1)
local pass = test_helper.utils.createMockPass()
voxel.renderCats(pass)
end)
it('should render complete UI', function()
core.init()
local pass = test_helper.utils.createMockPass()
ui.renderHUD(pass)
end)
it('should save and load game state', function()
core.init()
-- Add some furniture
core.addFurniture('bed', { x = 0, y = 0, z = 0 })
core.addFurniture('cat_tree', { x = 1, y = 0, z = 1 })
-- Spawn more cats
core.spawnCat()
core.spawnCat()
-- Save
local saveResult = core.saveGame()
assert.is_true(saveResult)
-- Load
local loadResult = core.loadGame()
assert.is_true(loadResult)
-- Verify state
assert.are.equal(4, #core.state.cats)
assert.are.equal(2, #core.state.furniture)
end)
it('should handle cat interaction with furniture', function()
core.init()
local furn = core.addFurniture('bed', { x = 0, y = 0, z = 0 })
local catObj = core.state.cats[1]
-- Move cat near furniture
catObj.position = { x = 0.1, y = 0, z = 0.1 }
-- AI should seek furniture
catObj:aiBehavior()
-- Should have target position
assert.are_not.equal(nil, catObj.targetPosition)
end)
it('should handle cat personality compatibility', function()
core.init()
local c1 = core.state.cats[1]
local c2 = utils.createMockCat({
personality = { id = 'social', compatibility = { ['social'] = 0.5 } }
})
c2.position = { x = 0.5, y = 0, z = 0 }
table.insert(core.state.cats, c2)
c1:updateNeeds()
assert.is_true(c1.needs.social >= 0.7)
end)
it('should handle furniture upgrades', function()
core.init()
local furn = core.addFurniture('bed', { x = 0, y = 0, z = 0 })
local originalContribution = furn.happinessContribution
furn:upgrade()
assert.are.equal(2, furn.upgradeLevel)
assert.is_true(furn.happinessContribution > originalContribution)
furn:upgrade()
assert.are.equal(3, furn.upgradeLevel)
local result = furn:upgrade()
assert.is_false(result)
end)
it('should render audio on pet', function()
local catObj = utils.createMockCat()
local result = catObj:pet()
assert.is_true(result)
end)
it('should handle UI interaction', function()
core.init()
ui.showInventory = true
-- Simulate clicking inventory item
local scale = 0.72
local startX = 640 - 300 * scale + 20 * scale
local startY = 360 - 200 * scale + 80 * scale
local itemY = startY + (1 - 1) * 50 * scale
ui.handleMousePress(startX + 10, itemY + 10, 1)
assert.are_not.equal(nil, ui.selectedFurniture)
end)
it('should handle empty cat list', function()
core.state.cats = {}
core.calculateHouseHappiness()
assert.are.equal(0, core.state.house.totalHappiness)
end)
it('should handle empty furniture list', function()
core.state.furniture = {}
local pass = test_helper.utils.createMockPass()
voxel.renderFurniture(pass)
end)
it('should handle nil core in modules', function()
_G.core = nil
cat.updateCats(0.1)
house.checkCollision({ 0, 0, 0 }, { 1, 1, 1 })
ui.renderHUD(test_helper.utils.createMockPass())
end)
it('should handle nil core in house functions', function()
_G.core = nil
local furn = house.createFurniture(nil, { x = 0, y = 0, z = 0 })
assert.are.equal(nil, furn)
local canPlace, reason = house.canPlaceFurniture({ 0, 0, 0 }, { 1, 1, 1 })
assert.is_false(canPlace)
end)
it('should handle nil core in ui functions', function()
_G.core = nil
ui.renderHUD(test_helper.utils.createMockPass())
ui.renderInventory(test_helper.utils.createMockPass(), 1280, 720, 0.72)
ui.renderCatLog(test_helper.utils.createMockPass(), 1280, 0.72)
end)
it('should handle nil core in voxel functions', function()
_G.core = nil
voxel.renderWorld(test_helper.utils.createMockPass())
voxel.renderFurniture(test_helper.utils.createMockPass())
voxel.renderCats(test_helper.utils.createMockPass())
end)
it('should handle nil core in audio functions', function()
_G.core = nil
audio.playPurr({ 0, 0, 0 })
audio.playMeow({ 0, 0, 0 })
audio.playClick()
end)
end)
+103
View File
@@ -0,0 +1,103 @@
-- Cat Haven - Test Runner
-- Runs all tests and generates reports
local busted = require('busted')
local lfs = require('lfs')
local testDirs = {
'test/spec',
'test/integration',
'test/e2e'
}
local testFiles = {}
-- Find all test files
for _, dir in ipairs(testDirs) do
if lfs.attributes(dir, 'mode') == 'directory' then
for file in lfs.dir(dir) do
if file:match('.*_spec%.lua$') then
table.insert(testFiles, dir .. '/' .. file)
end
end
end
end
-- Run tests
local success, results = busted.run(testFiles, {
reporter = 'tap',
color = false
})
-- Generate JUnit XML report
local function generateJUnitXML(results)
local xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
xml = xml .. '<testsuites name="Cat Haven Tests">\n'
local totalTests = 0
local totalFailures = 0
local totalErrors = 0
for _, suite in ipairs(results.suites) do
totalTests = totalTests + suite.tests
totalFailures = totalFailures + suite.failures
totalErrors = totalErrors + suite.errors
end
xml = xml .. ' <testsuite name="Cat Haven" tests="' .. totalTests .. '" failures="' .. totalFailures .. '" errors="' .. totalErrors .. '">\n'
for _, suite in ipairs(results.suites) do
xml = xml .. ' <testcase name="' .. suite.name .. '" classname="' .. suite.file .. '">\n'
if suite.errors > 0 then
xml = xml .. ' <error message="Test suite error" type="error">\n'
xml = xml .. ' ' .. suite.error .. '\n'
xml = xml .. ' </error>\n'
end
if suite.failures > 0 then
for _, test in ipairs(suite.tests) do
if test.status == 'failed' then
xml = xml .. ' <failure message="' .. test.message .. '" type="failure">\n'
xml = xml .. ' ' .. test.trace .. '\n'
xml = xml .. ' </failure>\n'
end
end
end
xml = xml .. ' </testcase>\n'
end
xml = xml .. ' </testsuite>\n'
xml = xml .. '</testsuites>\n'
return xml
end
-- Write XML report
local xmlReport = generateJUnitXML(results)
local file = io.open('test-report.xml', 'w')
if file then
file:write(xmlReport)
file:close()
print('\nXML report written to test-report.xml')
end
-- Output summary
print('\n========================================')
print('Test Summary')
print('========================================')
print('Suites:', results.suites)
print('Tests:', results.tests)
print('Passed:', results.passed)
print('Failed:', results.failures)
print('Errors:', results.errors)
print('Skipped:', results.skipped)
if results.failures > 0 or results.errors > 0 then
print('\n❌ Tests failed')
return 1
else
print('\n✅ All tests passed')
return 0
end
+80
View File
@@ -0,0 +1,80 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
local utils = test_helper.utils
describe('Audio Module', function()
local audio
before_each(function()
audio = require('src.audio.init')
end)
it('should create purr sound', function()
local sound = audio.createPurrSound()
assert.are_equal('source', sound.type)
assert.is_true(sound.spatial)
assert.are_equal(5.0, sound.maxDistance)
end)
it('should create meow sound', function()
local sound = audio.createMeowSound()
assert.are_equal('source', sound.type)
assert.is_true(sound.spatial)
assert.are_equal(3.0, sound.maxDistance)
end)
it('should create click sound', function()
local sound = audio.createClickSound()
assert.are_equal('source', sound.type)
assert.is_true(sound.spatial)
assert.are_equal(2.0, sound.maxDistance)
end)
it('should create ambient sound', function()
local sound = audio.createAmbientSound()
assert.are_equal('source', sound.type)
assert.is_false(sound.spatial)
assert.is_true(sound.looping)
end)
it('should play purr at position', function()
audio.playPurr({ 1, 2, 3 })
end)
it('should play meow at position', function()
audio.playMeow({ 1, 2, 3 })
end)
it('should play click sound', function()
audio.playClick()
end)
it('should handle nil position in playPurr', function()
audio.playPurr(nil)
end)
it('should handle nil position in playMeow', function()
audio.playMeow(nil)
end)
it('should handle nil sounds', function()
audio.sounds = {}
audio.playPurr({ 1, 2, 3 })
audio.playMeow({ 1, 2, 3 })
end)
it('should handle nil sounds.purr', function()
audio.sounds.purr = nil
audio.playPurr({ 1, 2, 3 })
end)
it('should handle nil sounds.meow', function()
audio.sounds.meow = nil
audio.playMeow({ 1, 2, 3 })
end)
it('should handle nil sounds.click', function()
audio.sounds.click = nil
audio.playClick()
end)
end)
+155
View File
@@ -0,0 +1,155 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
local utils = test_helper.utils
describe('Cat Module', function()
local cat
setup(function()
cat = require('src.cat.init')
end)
it('should create cat with default personality', function()
local c = cat.createCat(1, 'Test Cat', nil)
assert.are.equal(1, c.id)
assert.are.equal('Test Cat', c.name)
assert.are.equal('social', c.personality.id)
end)
it('should create cat with custom personality', function()
local personality = { id = 'lone_wolf', color = { 0.5, 0.5, 0.6 } }
local c = cat.createCat(2, 'Lone Cat', personality)
assert.are.equal('lone_wolf', c.personality.id)
assert.are.equal(0.5, c.personality.color[1])
end)
it('should update cat needs over time', function()
local c = cat.createCat(1, 'Test Cat', nil)
local oldComfort = c.needs.comfort
c:update(1.0)
assert.is_true(c.needs.comfort < oldComfort)
end)
it('should transition to sleep state', function()
local c = cat.createCat(1, 'Test Cat', nil)
c.needs.comfort = 0.1
c:update(1.0)
assert.is_true(c.isSleeping)
assert.are.equal('sleep', c.state)
end)
it('should wake up after sleep timer', function()
local c = cat.createCat(1, 'Test Cat', nil)
c.needs.comfort = 0.1
c.isSleeping = true
c.sleepTimer = 0.5
c:update(1.0)
assert.is_false(c.isSleeping)
assert.are.equal('idle', c.state)
end)
it('should move toward target position', function()
local c = cat.createCat(1, 'Test Cat', nil)
c.targetPosition = { x = 1, y = 0, z = 1 }
local oldX = c.position.x
local oldZ = c.position.z
c.state = 'moving'
c:update(0.1)
assert.is_true(c.position.x ~= oldX or c.position.z ~= oldZ)
end)
it('should handle zero distance movement', function()
local c = cat.createCat(1, 'Test Cat', nil)
c.targetPosition = { x = 0, y = 0, z = 0 }
c.position = { x = 0, y = 0, z = 0 }
c.state = 'moving'
c:update(0.1)
assert.is_true(true) -- Should not crash
end)
it('should seek furniture based on needs', function()
mocks.setupFilesystem()
require('src.core')
local c = cat.createCat(1, 'Test Cat', nil)
c.needs.comfort = 0.1
c:aiBehavior()
-- Should set targetPosition if furniture found
end)
it('should check personality compatibility', function()
mocks.setupFilesystem()
require('src.core')
local c1 = cat.createCat(1, 'Test Cat 1', { id = 'social', compatibility = { ['social'] = 0.5 } })
local c2 = cat.createCat(2, 'Test Cat 2', { id = 'social', compatibility = { ['social'] = 0.5 } })
c1.position = { x = 0, y = 0, z = 0 }
c2.position = { x = 0.5, y = 0, z = 0 }
c1:updateNeeds()
assert.is_true(c1.needs.social >= 0.7)
end)
it('should handle incompatible cats', function()
mocks.setupFilesystem()
require('src.core')
local c1 = cat.createCat(1, 'Test Cat 1', { id = 'lone_wolf', compatibility = { ['social'] = -0.5 } })
local c2 = cat.createCat(2, 'Test Cat 2', { id = 'social' })
c1.position = { x = 0, y = 0, z = 0 }
c2.position = { x = 0.5, y = 0, z = 0 }
local oldSocial = c1.needs.social
c1:updateNeeds()
assert.is_true(c1.needs.social < oldSocial)
end)
it('should pet cat and increase mood', function()
local c = cat.createCat(1, 'Test Cat', nil)
local oldMood = c.mood
c:pet()
assert.is_true(c.mood > oldMood)
end)
it('should serialize cat', function()
local c = cat.createCat(1, 'Test Cat', nil)
local serialized = c:serialize()
assert.are.equal(1, serialized.id)
assert.are.equal('Test Cat', serialized.name)
assert.are.equal('social', serialized.personalityId)
end)
it('should deserialize cat', function()
mocks.setupFilesystem()
require('src.core')
local original = cat.createCat(1, 'Test Cat', nil)
local serialized = original:serialize()
local deserialized = cat.deserialize(serialized)
assert.are.equal(original.id, deserialized.id)
assert.are.equal(original.name, deserialized.name)
end)
it('should handle nil personality in deserialize', function()
local data = {
id = 1,
name = 'Test',
personalityId = 'invalid',
position = { x = 0, y = 0, z = 0 }
}
local c = cat.deserialize(data)
assert.are_not.equal(nil, c)
assert.are.equal('social', c.personality.id)
end)
it('should get cat animation', function()
local c = cat.createCat(1, 'Test Cat', nil)
local anim = cat.getCatAnimation(c, 0)
assert.are.equal('table', type(anim))
assert.are.equal('number', type(anim.bounce))
end)
it('should handle nil cat in animation', function()
local anim = cat.getCatAnimation(nil, 0)
assert.are.equal(0, anim.bounce)
end)
it('should handle nil input in updateCats', function()
cat.updateCats(0.1)
assert.is_true(true) -- Should not crash
end)
end)
+160
View File
@@ -0,0 +1,160 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
local utils = test_helper.utils
describe('Core Module', function()
local core
setup(function()
mocks.setupFilesystem()
core = require('src.core')
end)
it('should initialize game state', function()
core.init()
assert.are.equal(0, core.state.gameTime)
assert.are.equal(2, #core.state.cats)
assert.are.equal(0, #core.state.furniture)
end)
it('should spawn cats', function()
local initialCount = #core.state.cats
core.spawnCat()
assert.are.equal(initialCount + 1, #core.state.cats)
end)
it('should generate cat names', function()
local name = core.generateCatName()
assert.are_not.equal(nil, name:find(' '))
end)
it('should remove cats', function()
local catId = core.state.cats[1].id
local result = core.removeCat(catId)
assert.is_true(result)
assert.are.equal(1, #core.state.cats)
end)
it('should add furniture', function()
local furn = core.addFurniture('bed', { x = 0, y = 0, z = 0 })
assert.are_not.equal(nil, furn)
end)
it('should reject invalid furniture type', function()
local furn = core.addFurniture('invalid_type', { x = 0, y = 0, z = 0 })
assert.is_false(furn)
end)
it('should save game', function()
local result = core.saveGame()
assert.is_true(result)
end)
it('should load game', function()
core.saveGame()
local result = core.loadGame()
assert.is_true(result)
end)
it('should handle missing save file', function()
local result = core.loadGame()
assert.is_false(result)
end)
it('should toggle pause', function()
core.togglePause()
assert.is_true(core.state.isPaused)
core.togglePause()
assert.is_false(core.state.isPaused)
end)
it('should get happiness percentage', function()
local happiness = core.getHappinessPercentage()
assert.is_true(happiness >= 0)
assert.is_true(happiness <= 100)
end)
it('should calculate happiness with zero cats', function()
core.state.cats = {}
core.calculateHouseHappiness()
assert.are.equal(0, core.state.house.totalHappiness)
end)
it('should check cat departures', function()
local cat = utils.createMockCat({ needs = { comfort = 0.05, hunger = 0.5 } })
table.insert(core.state.cats, cat)
local initialCount = #core.state.cats
core.checkCatDepartures()
assert.is_true(#core.state.cats < initialCount)
end)
it('should handle nil cat in update', function()
core.state.cats = { nil }
core.update(0.1)
end)
it('should handle nil cat in happiness calculation', function()
core.state.cats = { nil }
core.calculateHouseHappiness()
end)
it('should handle nil cat in removeCat', function()
local result = core.removeCat(999)
assert.is_false(result)
end)
it('should handle nil core in update', function()
_G.core = nil
core.update(0.1)
end)
it('should handle nil cat module', function()
_G.cat = nil
core.update(0.1)
end)
it('should handle nil ui module', function()
_G.ui = nil
core.update(0.1)
end)
it('should handle nil voxel module', function()
_G.voxel = nil
core.update(0.1)
end)
it('should handle nil audio module', function()
_G.audio = nil
core.update(0.1)
end)
it('should handle nil house module', function()
_G.house = nil
core.update(0.1)
end)
it('should handle empty furniture list', function()
core.state.furniture = {}
core.calculateHouseHappiness()
end)
it('should handle nil needs in cat', function()
local cat = utils.createMockCat()
cat.needs = nil
table.insert(core.state.cats, cat)
core.calculateHouseHappiness()
end)
it('should handle nil personality in cat', function()
local cat = utils.createMockCat()
cat.personality = nil
table.insert(core.state.cats, cat)
cat:updateNeeds()
end)
it('should handle nil furniture in cat ai behavior', function()
core.state.furniture = nil
local cat = utils.createMockCat()
cat:aiBehavior()
end)
end)
+124
View File
@@ -0,0 +1,124 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
local utils = test_helper.utils
describe('House Module', function()
local house
before_each(function()
house = require('src.house.init')
end)
it('should create furniture', function()
local type = { id = 'bed', name = 'Bed', baseHappiness = 0.7, size = { 1, 0.3, 1 } }
local furn = house.createFurniture(type, { x = 0, y = 0, z = 0 })
assert.are.equal('bed', furn.type.id)
assert.are.equal(1, furn.upgradeLevel)
end)
it('should upgrade furniture', function()
local type = { id = 'bed', name = 'Bed', baseHappiness = 0.7, size = { 1, 0.3, 1 } }
local furn = house.createFurniture(type, { x = 0, y = 0, z = 0 })
local oldContribution = furn.happinessContribution
furn:upgrade()
assert.are.equal(2, furn.upgradeLevel)
assert.is_true(furn.happinessContribution > oldContribution)
end)
it('should limit furniture upgrades to 3', function()
local type = { id = 'bed', name = 'Bed', baseHappiness = 0.7, size = { 1, 0.3, 1 } }
local furn = house.createFurniture(type, { x = 0, y = 0, z = 0 })
furn:upgrade()
furn:upgrade()
local result = furn:upgrade()
assert.is_false(result)
assert.are.equal(3, furn.upgradeLevel)
end)
it('should serialize furniture', function()
local type = { id = 'bed', name = 'Bed', baseHappiness = 0.7, size = { 1, 0.3, 1 } }
local furn = house.createFurniture(type, { x = 0, y = 0, z = 0 })
local serialized = furn:serialize()
assert.are.equal('bed', serialized.typeId)
assert.are.equal(1, serialized.upgradeLevel)
end)
it('should deserialize furniture', function()
mocks.setupFilesystem()
require('src.core')
local type = { id = 'bed', name = 'Bed', baseHappiness = 0.7, size = { 1, 0.3, 1 } }
local furn = house.createFurniture(type, { x = 0, y = 0, z = 0 })
local serialized = furn:serialize()
local deserialized = house.deserializeFurniture(serialized)
assert.are.equal('bed', deserialized.type.id)
assert.are.equal(1, deserialized.upgradeLevel)
end)
it('should check furniture placement bounds', function()
local canPlace, reason = house.canPlaceFurniture({ x = 0, y = 0, z = 0 }, { 1, 1, 1 })
assert.is_true(canPlace)
end)
it('should reject furniture out of bounds', function()
local canPlace, reason = house.canPlaceFurniture({ x = 100, y = 0, z = 100 }, { 1, 1, 1 })
assert.is_false(canPlace)
assert.are.equal('out of bounds', reason)
end)
it('should detect furniture collision', function()
mocks.setupFilesystem()
require('src.core')
local type = { id = 'bed', name = 'Bed', baseHappiness = 0.7, size = { 1, 0.3, 1 } }
house.createFurniture(type, { x = 0, y = 0, z = 0 })
local canPlace, reason = house.canPlaceFurniture({ x = 0, y = 0, z = 0 }, { 1, 1, 1 })
assert.is_false(canPlace)
assert.are.equal('collision', reason)
end)
it('should place furniture', function()
mocks.setupFilesystem()
require('src.core')
local furn = house.placeFurniture('bed', { x = 0, y = 0, z = 0 })
assert.are_not.equal(nil, furn)
end)
it('should reject invalid furniture type', function()
local furn = house.placeFurniture('invalid_type', { x = 0, y = 0, z = 0 })
assert.are.equal(nil, furn)
end)
it('should detect collision with furniture', function()
local result = house.checkCollision({ x = 0, y = 0, z = 0 }, { 1, 1, 1 })
assert.is_false(result)
end)
it('should interact with furniture', function()
local furn = house.interactWithFurniture({ x = 0, y = 0, z = 0 })
assert.are.equal(nil, furn)
end)
it('should handle nil furniture type', function()
local furn = house.createFurniture(nil, { x = 0, y = 0, z = 0 })
assert.are.equal(nil, furn)
end)
it('should handle nil data in deserialize', function()
local furn = house.deserializeFurniture(nil)
assert.are.equal(nil, furn)
end)
it('should handle nil core in canPlaceFurniture', function()
local canPlace, reason = house.canPlaceFurniture({ x = 0, y = 0, z = 0 }, { 1, 1, 1 })
assert.is_false(canPlace)
end)
it('should handle nil core in checkCollision', function()
local result = house.checkCollision({ x = 0, y = 0, z = 0 }, { 1, 1, 1 })
assert.is_false(result)
end)
it('should handle nil core in interactWithFurniture', function()
local furn = house.interactWithFurniture({ x = 0, y = 0, z = 0 })
assert.are.equal(nil, furn)
end)
end)
+146
View File
@@ -0,0 +1,146 @@
describe('JSON Module', function()
local json
setup(function()
json = require('src.json')
end)
it('should encode strings', function()
assert.are.equal('"hello"', json.encode('hello'))
assert.are.equal('"hello world"', json.encode('hello world'))
end)
it('should encode numbers', function()
assert.are.equal('123', json.encode(123))
assert.are.equal('12.5', json.encode(12.5))
assert.are.equal('-42', json.encode(-42))
end)
it('should encode booleans', function()
assert.are.equal('true', json.encode(true))
assert.are.equal('false', json.encode(false))
end)
it('should encode null', function()
assert.are.equal('null', json.encode(nil))
end)
it('should encode arrays', function()
local arr = { 1, 2, 3 }
local encoded = json.encode(arr)
assert.are.equal('{ 1, 2, 3 }', encoded)
end)
it('should encode objects', function()
local obj = { name = 'test', value = 42 }
local encoded = json.encode(obj)
assert.are_not.equal(nil, encoded:find('name'))
assert.are_not.equal(nil, encoded:find('value'))
end)
it('should decode strings', function()
local decoded = json.decode('"hello"')
assert.are.equal('hello', decoded)
end)
it('should decode numbers', function()
local decoded = json.decode('123')
assert.are.equal(123, decoded)
local decoded2 = json.decode('12.5')
assert.are.equal(12.5, decoded2)
end)
it('should decode booleans', function()
local decoded = json.decode('true')
assert.are.equal(true, decoded)
local decoded2 = json.decode('false')
assert.are.equal(false, decoded2)
end)
it('should decode null', function()
local decoded = json.decode('null')
assert.are.equal(nil, decoded)
end)
it('should decode arrays', function()
local decoded = json.decode('{ 1, 2, 3 }')
assert.are.equal(1, decoded[1])
assert.are.equal(2, decoded[2])
assert.are.equal(3, decoded[3])
end)
it('should decode objects', function()
local decoded = json.decode('{ "name": "test", "value": 42 }')
assert.are.equal('test', decoded.name)
assert.are.equal(42, decoded.value)
end)
it('should handle round-trip encoding/decoding', function()
local original = {
name = 'Test Cat',
age = 3,
active = true,
tags = { 'cute', 'playful' },
stats = { comfort = 0.8, hunger = 0.6 }
}
local encoded = json.encode(original)
local decoded = json.decode(encoded)
assert.are.equal(original.name, decoded.name)
assert.are.equal(original.age, decoded.age)
assert.are.equal(original.active, decoded.active)
assert.are.equal(original.tags[1], decoded.tags[1])
assert.are.equal(original.stats.comfort, decoded.stats.comfort)
end)
it('should handle escaped characters in strings', function()
local original = 'hello\nworld\t"quoted"'
local encoded = json.encode(original)
local decoded = json.decode(encoded)
assert.are.equal(original, decoded)
end)
it('should handle empty arrays', function()
local original = {}
local encoded = json.encode(original)
local decoded = json.decode(encoded)
assert.are.equal(0, #decoded)
end)
it('should handle nested structures', function()
local original = {
cats = {
{ name = 'Cat1', personality = { id = 'social' } },
{ name = 'Cat2', personality = { id = 'lone_wolf' } }
}
}
local encoded = json.encode(original)
local decoded = json.decode(encoded)
assert.are.equal('Cat1', decoded.cats[1].name)
assert.are.equal('social', decoded.cats[1].personality.id)
end)
it('should handle non-string keys by converting to strings', function()
local original = { [123] = 'value', [true] = 'bool' }
local encoded = json.encode(original)
local decoded = json.decode(encoded)
assert.are.equal('value', decoded['123'])
assert.are.equal('bool', decoded['true'])
end)
it('should handle NaN and infinity as null', function()
assert.are.equal('null', json.encode(0/0))
assert.are.equal('null', json.encode(math.huge))
assert.are.equal('null', json.encode(-math.huge))
end)
it('should return error for invalid JSON', function()
local decoded, err = json.decode('{ invalid }')
assert.are_not.equal(nil, err)
assert.are.equal(nil, decoded)
end)
it('should return error for non-string input', function()
local decoded, err = json.encode({ function() end })
assert.are.equal('null', decoded)
end)
end)
+98
View File
@@ -0,0 +1,98 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
local utils = test_helper.utils
describe('UI Module', function()
local ui
local core
before_each(function()
ui = require('src.ui.init')
mocks.setupFilesystem()
core = require('src.core')
end)
it('should render HUD', function()
local pass = utils.createMockPass()
ui.renderHUD(pass)
end)
it('should render HUD with zero cats', function()
core.state.cats = {}
local pass = utils.createMockPass()
ui.renderHUD(pass)
end)
it('should render inventory', function()
local pass = utils.createMockPass()
ui.renderInventory(pass, 1280, 720, 0.72)
end)
it('should render cat log', function()
local pass = utils.createMockPass()
ui.renderCatLog(pass, 1280, 0.72)
end)
it('should render placement preview', function()
local pass = utils.createMockPass()
ui.renderPlacementPreview(pass, 1280, 720)
end)
it('should get world ray', function()
local start, endPos = ui.getWorldRay(1280, 720)
assert.are.equal('table', type(start))
assert.are.equal('table', type(endPos))
end)
it('should handle mouse press in inventory', function()
ui.showInventory = true
ui.selectedFurniture = nil
ui.handleMousePress(1000, 500, 1)
end)
it('should handle mouse press on close button', function()
ui.showInventory = true
ui.handleMousePress(1200, 300, 1)
assert.is_false(ui.showInventory)
end)
it('should toggle inventory', function()
ui.toggleInventory()
assert.is_true(ui.showInventory)
ui.toggleInventory()
assert.is_false(ui.showInventory)
end)
it('should toggle cat log', function()
ui.toggleCatLog()
assert.is_false(ui.showCatLog)
ui.toggleCatLog()
assert.is_true(ui.showCatLog)
end)
it('should handle nil core in renderHUD', function()
_G.core = nil
local pass = utils.createMockPass()
ui.renderHUD(pass)
end)
it('should handle nil pass in renderHUD', function()
ui.renderHUD(nil)
end)
it('should handle nil pass in renderInventory', function()
ui.renderInventory(nil, 1280, 720, 0.72)
end)
it('should handle nil pass in renderCatLog', function()
ui.renderCatLog(nil, 1280, 0.72)
end)
it('should handle nil pass in renderPlacementPreview', function()
ui.renderPlacementPreview(nil, 1280, 720)
end)
it('should handle nil core in handleMousePress', function()
ui.handleMousePress(100, 100, 1)
end)
end)
+101
View File
@@ -0,0 +1,101 @@
local test_helper = require('test.test_helper')
local mocks = test_helper.mocks
local utils = test_helper.utils
describe('Voxel Module', function()
local voxel
before_each(function()
-- Mock LOVR
_G.lovr = mocks.lovr
voxel = require('src.voxel.init')
end)
it('should create voxel texture', function()
local color = { 1, 0, 0 }
local texture = voxel.createVoxelTexture(color)
assert.are.equal('texture', texture.type)
assert.are.equal(64, texture.image.width)
assert.are.equal(64, texture.image.height)
end)
it('should initialize textures for furniture', function()
mocks.setupFilesystem()
require('src.core')
voxel.init()
assert.are_not.equal(nil, voxel.textures['bed'])
assert.are_not.equal(nil, voxel.textures['cat_tree'])
end)
it('should create cube mesh', function()
local mesh = voxel.cubeMesh
assert.are.equal('mesh', mesh.type)
assert.are.equal(36, #mesh.indices) -- 12 triangles
end)
it('should render world', function()
mocks.setupFilesystem()
require('src.core')
local pass = utils.createMockPass()
voxel.renderWorld(pass)
-- Should draw floor and walls
end)
it('should render furniture', function()
mocks.setupFilesystem()
require('src.core')
local pass = utils.createMockPass()
voxel.renderFurniture(pass)
end)
it('should render cats', function()
mocks.setupFilesystem()
require('src.core')
local pass = utils.createMockPass()
voxel.renderCats(pass)
end)
it('should raycast furniture', function()
local start = { 0, 0, 0 }
local direction = { 0, 0, -1 }
local maxDistance = 10
local result = voxel.raycast(start, direction, maxDistance)
assert.are.equal('table', type(result))
end)
it('should raycast cats', function()
local start = { 0, 0, 0 }
local direction = { 0, 0, -1 }
local maxDistance = 10
local result = voxel.raycast(start, direction, maxDistance)
assert.are.equal('table', type(result))
end)
it('should handle nil inputs', function()
local result = voxel.raycast(nil, nil, nil)
assert.are.equal('table', type(result))
assert.are.equal(0, #result)
end)
it('should handle zero distance direction', function()
local start = { 0, 0, 0 }
local direction = { 0, 0, 0 }
local maxDistance = 10
local result = voxel.raycast(start, direction, maxDistance)
assert.are.equal('table', type(result))
end)
it('should sort raycast results by distance', function()
local start = { 0, 0, 0 }
local direction = { 0, 0, -1 }
local maxDistance = 10
local result = voxel.raycast(start, direction, maxDistance)
for i = 2, #result do
assert.is_true(result[i].distance >= result[i-1].distance)
end
end)
end)
+243
View File
@@ -0,0 +1,243 @@
-- Cat Haven - Test Helper
-- LOVR API mocks and test utilities
local mocks = {}
-- Mock LOVR graphics API
mocks.lovr = {
graphics = {
newImage = function(pixels, width, height, options)
return { type = 'image', pixels = pixels, width = width, height = height }
end,
newTexture = function(image)
return { type = 'texture', image = image }
end,
newMesh = function(vertices, indices, mode)
return { type = 'mesh', vertices = vertices, indices = indices, mode = mode }
end,
newMaterial = function(texture)
return { type = 'material', texture = texture }
end,
newModel = function(mesh)
return { type = 'model', mesh = mesh }
end,
box = function(mode, x, y, z, w, h, d, color, thickness)
return { type = 'box', mode = mode, x = x, y = y, z = z, w = w, h = h, d = d, color = color }
end,
text = function(text, x, y, z, size, color, align)
return { type = 'text', text = text, x = x, y = y, z = z, size = size, color = color, align = align }
end
},
audio = {
newSource = function(data, bufferType, sampleRate, channels)
return {
type = 'source',
data = data,
bufferType = bufferType,
sampleRate = sampleRate,
channels = channels,
spatial = false,
looping = false,
volume = 1.0,
position = { 0, 0, 0 },
rolloff = 1.0,
maxDistance = 10.0
}
end
},
system = {
getWindowDimensions = function()
return 1280, 720
end,
getMousePosition = function()
return 640, 360
end
},
headset = {
isDown = function(button)
return false
end
},
filesystem = {
newFile = function(filename, mode)
return {
filename = filename,
mode = mode,
content = '',
write = function(self, data)
self.content = self.content .. data
end,
read = function(self, spec)
if spec == '*a' then return self.content end
end,
close = function(self)
end
}
end
},
data = {
newImage = function(pixels, width, height, options)
return { type = 'image', pixels = pixels, width = width, height = height }
end,
newModelData = function()
return { type = 'modeldata' }
end
}
}
-- Mock LOVR filesystem for save/load tests
mocks.setupFilesystem = function(saveData)
local originalNewFile = mocks.lovr.filesystem.newFile
mocks.lovr.filesystem.newFile = function(filename, mode)
if filename == 'savegame.json' then
if mode == 'w' then
return {
filename = filename,
mode = mode,
content = '',
write = function(self, data)
self.content = data
end,
read = function(self, spec)
return saveData or ''
end,
close = function(self)
end
}
elseif mode == 'r' then
return {
filename = filename,
mode = mode,
content = saveData or '',
write = function(self, data)
end,
read = function(self, spec)
if spec == '*a' then return self.content end
end,
close = function(self)
end
}
end
end
return originalNewFile(filename, mode)
end
end
-- Reset mocks
mocks.reset = function()
mocks.lovr.filesystem.newFile = function(filename, mode)
return {
filename = filename,
mode = mode,
content = '',
write = function(self, data)
self.content = self.content .. data
end,
read = function(self, spec)
if spec == '*a' then return self.content end
end,
close = function(self)
end
}
end
end
-- Test utilities
local utils = {}
utils.assertDeepEqual = function(actual, expected, msg)
if type(actual) ~= 'table' or type(expected) ~= 'table' then
assert(actual == expected, msg or 'Values not equal')
return
end
for k, v in pairs(actual) do
if type(v) == 'table' then
utils.assertDeepEqual(v, expected[k], msg .. ': table[' .. k .. ']')
else
assert(v == expected[k], msg .. ': field[' .. k .. '] expected ' .. tostring(expected[k]) .. ', got ' .. tostring(v))
end
end
for k, v in pairs(expected) do
if actual[k] == nil then
assert(false, msg .. ': missing field[' .. k .. ']')
end
end
end
utils.createMockPass = function()
return {
box = function(mode, x, y, z, w, h, d, color, thickness)
return { mode = mode, x = x, y = y, z = z, w = w, h = h, d = d, color = color }
end,
text = function(text, x, y, z, size, color, align)
return { text = text, x = x, y = y, z = z, size = size, color = color, align = align }
end
}
end
utils.createMockCat = function(overrides)
return {
id = overrides.id or 1,
name = overrides.name or 'Test Cat',
personality = overrides.personality or { id = 'social', color = { 0.9, 0.6, 0.7 } },
position = overrides.position or { x = 0, y = 0, z = 0 },
targetPosition = overrides.targetPosition,
state = overrides.state or 'idle',
mood = overrides.mood or 0.8,
needs = overrides.needs or { comfort = 0.7, social = 0.7, hunger = 0.7, fun = 0.7 },
favoriteSpots = overrides.favoriteSpots or {},
history = overrides.history or { arrivals = 1, departures = 0, totalHappiness = 0 },
animationTime = overrides.animationTime or 0,
isSleeping = overrides.isSleeping or false,
sleepTimer = overrides.sleepTimer or 0,
update = function(self, dt) end,
updateNeeds = function(self) end,
serialize = function(self)
return {
id = self.id,
name = self.name,
personalityId = self.personality.id,
position = self.position,
state = self.state,
mood = self.mood,
needs = self.needs,
favoriteSpots = self.favoriteSpots,
history = self.history,
isSleeping = self.isSleeping
}
end
}
end
utils.createMockFurniture = function(overrides)
return {
type = overrides.type or { id = 'bed', name = 'Cozy Bed', baseHappiness = 0.7, comfortBonus = 0.6, socialBonus = 0.1, funBonus = 0.1, maxOccupants = 2, color = { 0.8, 0.6, 0.7 }, size = { 1, 0.3, 1 } },
position = overrides.position or { x = 0, y = 0, z = 0 },
state = overrides.state or 'placed',
upgradeLevel = overrides.upgradeLevel or 1,
occupant = overrides.occupant,
happinessContribution = overrides.happinessContribution or 0.7,
upgrade = function(self)
if self.upgradeLevel < 3 then
self.upgradeLevel = self.upgradeLevel + 1
self.happinessContribution = self.type.baseHappiness * self.upgradeLevel * 0.8
return true
end
return false
end,
serialize = function(self)
return {
typeId = self.type.id,
position = self.position,
state = self.state,
upgradeLevel = self.upgradeLevel,
occupant = self.occupant and self.occupant.id or nil,
happinessContribution = self.happinessContribution
}
end
}
end
return { mocks = mocks, utils = utils }