From d869582b37252cde2bad8d7644d64ffa35e41b44 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 18 Sep 2025 17:17:20 -0400 Subject: [PATCH] testing layout changes --- testing/absolute-positioning.lua | 397 ---- testing/branching-layout-tests.lua | 475 ---- testing/complex-nested-layouts.lua | 401 ---- testing/depth-layout-tests.lua | 393 --- testing/flex-direction-tests.lua | 163 -- testing/justify-content-tests.lua | 435 ---- testing/loveStub.lua | 75 + testing/luaunit.lua | 3545 ++++++++++++++++++++++++++++ 8 files changed, 3620 insertions(+), 2264 deletions(-) delete mode 100644 testing/absolute-positioning.lua delete mode 100644 testing/branching-layout-tests.lua delete mode 100644 testing/complex-nested-layouts.lua delete mode 100644 testing/depth-layout-tests.lua delete mode 100644 testing/flex-direction-tests.lua delete mode 100644 testing/justify-content-tests.lua create mode 100644 testing/loveStub.lua create mode 100644 testing/luaunit.lua diff --git a/testing/absolute-positioning.lua b/testing/absolute-positioning.lua deleted file mode 100644 index f8e52f4..0000000 --- a/testing/absolute-positioning.lua +++ /dev/null @@ -1,397 +0,0 @@ -package.path = package.path - .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua;./testing/?.lua" - -local luaunit = require("testing.luaunit") -require("testing.love_helper") - -local Gui = require("game.libs.FlexLove").GUI -local enums = require("game.libs.FlexLove").enums - --- Test case for absolute positioning behavior with complex nested layouts -TestAbsolutePositioning = {} - -function TestAbsolutePositioning:testDeeplyNestedAbsolutePositioning() - -- Create a root window with flex positioning - local rootWindow = Gui.new({ - x = 0, - y = 0, - w = 800, - h = 600, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a nested flex container - local nestedFlexContainer = Gui.new({ - parent = rootWindow, - x = 100, - y = 50, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create an absolute positioned child in nested container - local absoluteChildInNested = Gui.new({ - parent = nestedFlexContainer, - x = 20, - y = 30, - w = 80, - h = 40, - positioning = enums.Positioning.ABSOLUTE, - text = "Nested Absolute", - }) - - -- Create another flex child in nested container - local flexChildInNested = Gui.new({ - parent = nestedFlexContainer, - x = 0, - y = 0, - w = 60, - h = 30, - text = "Nested Flex", - }) - - -- Layout all children - rootWindow:layoutChildren() - - -- Verify absolute child position is correct (relative to parent) - luaunit.assertEquals(absoluteChildInNested.x, 120) -- 100 + 20 - luaunit.assertEquals(absoluteChildInNested.y, 80) -- 50 + 30 - - -- Verify flex child position is calculated correctly within nested container - luaunit.assertEquals(flexChildInNested.x, 0) -- Should be at start of container - luaunit.assertEquals(flexChildInNested.y, 100 - 30) -- Should be centered vertically in 100px container - - -- Verify parent-child relationships - luaunit.assertEquals(#nestedFlexContainer.children, 2) - luaunit.assertEquals(nestedFlexContainer.children[1], absoluteChildInNested) - luaunit.assertEquals(nestedFlexContainer.children[2], flexChildInNested) -end - -function TestAbsolutePositioning:testMixedLayoutTypesWithNesting() - -- Create a complex nested structure with mixed layout types - local rootWindow = Gui.new({ - x = 0, - y = 0, - w = 800, - h = 600, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a flex container with absolute positioning - local flexContainerWithAbsolute = Gui.new({ - parent = rootWindow, - x = 0, - y = 0, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add absolute positioned child to flex container - local absoluteChild1 = Gui.new({ - parent = flexContainerWithAbsolute, - x = 50, - y = 20, - w = 60, - h = 30, - positioning = enums.Positioning.ABSOLUTE, - text = "Abs Child 1", - }) - - -- Add nested absolute positioned child - local nestedAbsoluteChild = Gui.new({ - parent = flexContainerWithAbsolute, - x = 100, - y = 100, - w = 40, - h = 20, - positioning = enums.Positioning.ABSOLUTE, - text = "Nested Abs", - }) - - -- Add regular flex child - local flexChild = Gui.new({ - parent = flexContainerWithAbsolute, - x = 0, - y = 0, - w = 80, - h = 40, - text = "Flex Child", - }) - - -- Layout children - rootWindow:layoutChildren() - - -- Verify absolute positions are correct - luaunit.assertEquals(absoluteChild1.x, 50) - luaunit.assertEquals(absoluteChild1.y, 20) - luaunit.assertEquals(nestedAbsoluteChild.x, 100) - luaunit.assertEquals(nestedAbsoluteChild.y, 100) - - -- Verify flex child is positioned by flex layout - luaunit.assertEquals(flexChild.x, 0) -- First flex child in space-between should be at start -end - -function TestAbsolutePositioning:testAbsolutePositioningInComplexBranchingStructure() - -- Create a complex branching structure with multiple absolute positions - local rootWindow = Gui.new({ - x = 0, - y = 0, - w = 800, - h = 600, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create three branches with different absolute positions - local branch1 = Gui.new({ - parent = rootWindow, - x = 0, - y = 0, - w = 200, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local branch2 = Gui.new({ - parent = rootWindow, - x = 250, - y = 100, - w = 200, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local branch3 = Gui.new({ - parent = rootWindow, - x = 500, - y = 200, - w = 200, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add absolute positioned children to each branch - local absChild1 = Gui.new({ - parent = branch1, - x = 10, - y = 15, - w = 50, - h = 20, - positioning = enums.Positioning.ABSOLUTE, - text = "Branch1 Abs", - }) - - local absChild2 = Gui.new({ - parent = branch2, - x = 20, - y = 30, - w = 60, - h = 25, - positioning = enums.Positioning.ABSOLUTE, - text = "Branch2 Abs", - }) - - local absChild3 = Gui.new({ - parent = branch3, - x = 30, - y = 40, - w = 70, - h = 30, - positioning = enums.Positioning.ABSOLUTE, - text = "Branch3 Abs", - }) - - -- Add regular children to branches - local regularChild1 = Gui.new({ - parent = branch1, - x = 0, - y = 0, - w = 40, - h = 25, - text = "Branch1 Regular", - }) - - local regularChild2 = Gui.new({ - parent = branch2, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Branch2 Regular", - }) - - -- Layout all children - rootWindow:layoutChildren() - - -- Verify absolute positions in each branch (absolute position relative to branch parent) - luaunit.assertEquals(absChild1.x, 10) -- 0 + 10 - luaunit.assertEquals(absChild1.y, 15) -- 0 + 15 - luaunit.assertEquals(absChild2.x, 250 + 20) -- 250 + 20 - luaunit.assertEquals(absChild2.y, 100 + 30) -- 100 + 30 - luaunit.assertEquals(absChild3.x, 500 + 30) -- 500 + 30 - luaunit.assertEquals(absChild3.y, 200 + 40) -- 200 + 40 - - -- Verify that regular children are positioned by flex layout - luaunit.assertEquals(regularChild1.x, 0) - luaunit.assertEquals(regularChild2.x, 0) -end - -function TestAbsolutePositioning:testAbsolutePositioningWithComplexTransformations() - -- Create a complex structure with transformations and absolute positioning - local rootWindow = Gui.new({ - x = 100, - y = 50, - w = 600, - h = 400, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a container with padding and margin - local containerWithPadding = Gui.new({ - parent = rootWindow, - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_AROUND, - alignItems = enums.AlignItems.CENTER, - padding = { left = 10, top = 5 }, - margin = { left = 15, top = 10 }, - }) - - -- Add absolute positioned child with padding/margin consideration - local absChildWithPadding = Gui.new({ - parent = containerWithPadding, - x = 20, - y = 30, - w = 80, - h = 40, - positioning = enums.Positioning.ABSOLUTE, - text = "Abs with Padding", - }) - - -- Layout children - rootWindow:layoutChildren() - - -- Verify absolute position accounts for parent padding and margin - luaunit.assertEquals(absChildWithPadding.x, 100 + 15 + 20) -- root x + margin + child x - luaunit.assertEquals(absChildWithPadding.y, 50 + 10 + 30) -- root y + margin + child y -end - -function TestAbsolutePositioning:testAbsolutePositioningInNestedLayoutWithMultipleLevels() - -- Create a deeply nested structure with absolute positioning at multiple levels - local rootWindow = Gui.new({ - x = 0, - y = 0, - w = 800, - h = 600, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Level 1: Main container - local level1Container = Gui.new({ - parent = rootWindow, - x = 50, - y = 30, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Level 2: Nested container - local level2Container = Gui.new({ - parent = level1Container, - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Level 3: Deep nested container - local level3Container = Gui.new({ - parent = level2Container, - x = 20, - y = 10, - w = 150, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add absolute positioned child at level 3 - local absChildAtLevel3 = Gui.new({ - parent = level3Container, - x = 10, - y = 20, - w = 60, - h = 30, - positioning = enums.Positioning.ABSOLUTE, - text = "Deep Abs", - }) - - -- Add flex child at level 3 - local flexChildAtLevel3 = Gui.new({ - parent = level3Container, - x = 0, - y = 0, - w = 40, - h = 25, - text = "Deep Flex", - }) - - -- Layout all children - rootWindow:layoutChildren() - - -- Verify absolute position at deepest level - luaunit.assertEquals(absChildAtLevel3.x, 50 + 0 + 20 + 10) -- root x + level1 x + level2 x + child x - luaunit.assertEquals(absChildAtLevel3.y, 30 + 0 + 10 + 20) -- root y + level1 y + level2 y + child y - - -- Verify that flex child is positioned by flex layout at level 3 - luaunit.assertEquals(flexChildAtLevel3.x, 0) -- Should be at start of container -end - --- Run the tests -luaunit.LuaUnit.run() - diff --git a/testing/branching-layout-tests.lua b/testing/branching-layout-tests.lua deleted file mode 100644 index c37f6df..0000000 --- a/testing/branching-layout-tests.lua +++ /dev/null @@ -1,475 +0,0 @@ -package.path = package.path - .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua;./testing/?.lua" - -local luaunit = require("testing.luaunit") -require("testing.love_helper") - -local Gui = require("game.libs.FlexLove").GUI -local enums = require("game.libs.FlexLove").enums - --- Test case for branching flex layouts -TestBranchingLayouts = {} - -function TestBranchingLayouts:testMultipleChildrenAtSameLevel() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create first child with different properties - local child1 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 100, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create second child with different properties - local child2 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 150, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add children to first child (nested) - local nestedChild1 = Gui.new({ - parent = child1, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local nestedChild2 = Gui.new({ - parent = child1, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Add children to second child (nested) - local nestedChild3 = Gui.new({ - parent = child2, - x = 0, - y = 0, - w = 70, - h = 40, - text = "Button 3", - }) - - local nestedChild4 = Gui.new({ - parent = child2, - x = 0, - y = 0, - w = 80, - h = 50, - text = "Button 4", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the main children are positioned correctly - luaunit.assertEquals(child1.x, 0) - luaunit.assertEquals(child1.y, 0) - - luaunit.assertEquals(child2.x, 100) - luaunit.assertEquals(child2.y, 0) - - -- Verify that nested children in first child are laid out correctly (vertical) - luaunit.assertEquals(nestedChild1.x, 0) - luaunit.assertEquals(nestedChild1.y, 0) - - luaunit.assertEquals(nestedChild2.x, 0) - luaunit.assertEquals(nestedChild2.y, 30 + 10) -- Should be positioned after first child + gap - - -- Verify that nested children in second child are laid out correctly (centered vertically) - luaunit.assertEquals(nestedChild3.x, 0) - luaunit.assertEquals(nestedChild3.y, (100 - 40) / 2) -- Should be centered vertically - - luaunit.assertEquals(nestedChild4.x, 0) - luaunit.assertEquals(nestedChild4.y, (100 - 50) / 2 + 40 + 10) -- Should be positioned after first child + gap -end - -function TestBranchingLayouts:testAsymmetricBranchingStructure() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 400, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a child with 3 branches - different sizes - local branch1 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 100, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local branch2 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 150, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.CENTER, - }) - - local branch3 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 150, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_END, - alignItems = enums.AlignItems.FLEX_END, - }) - - -- Add children to each branch with different sizes - local child1_1 = Gui.new({ - parent = branch1, - x = 0, - y = 0, - w = 50, - h = 20, - text = "Button 1", - }) - - local child1_2 = Gui.new({ - parent = branch1, - x = 0, - y = 0, - w = 60, - h = 30, - text = "Button 2", - }) - - -- Add children to second branch - local child2_1 = Gui.new({ - parent = branch2, - x = 0, - y = 0, - w = 70, - h = 40, - text = "Button 3", - }) - - local child2_2 = Gui.new({ - parent = branch2, - x = 0, - y = 0, - w = 80, - h = 50, - text = "Button 4", - }) - - local child2_3 = Gui.new({ - parent = branch2, - x = 0, - y = 0, - w = 90, - h = 60, - text = "Button 5", - }) - - -- Add children to third branch - local child3_1 = Gui.new({ - parent = branch3, - x = 0, - y = 0, - w = 70, - h = 40, - text = "Button 6", - }) - - local child3_2 = Gui.new({ - parent = branch3, - x = 0, - y = 0, - w = 80, - h = 50, - text = "Button 7", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the branches are positioned correctly - luaunit.assertEquals(branch1.x, 0) - luaunit.assertEquals(branch1.y, 0) - - luaunit.assertEquals(branch2.x, 100) - luaunit.assertEquals(branch2.y, 0) - - luaunit.assertEquals(branch3.x, 250) - luaunit.assertEquals(branch3.y, 0) - - -- Verify that children in first branch are laid out correctly (flex-start) - luaunit.assertEquals(child1_1.x, 0) - luaunit.assertEquals(child1_1.y, 0) - - luaunit.assertEquals(child1_2.x, 0) - luaunit.assertEquals(child1_2.y, 20 + 10) -- Should be positioned after first child + gap - - -- Verify that children in second branch are laid out correctly (centered) - luaunit.assertEquals(child2_1.x, 0) - luaunit.assertEquals(child2_1.y, (100 - 40) / 2) -- Should be centered vertically - - luaunit.assertEquals(child2_2.x, 0) - luaunit.assertEquals(child2_2.y, (100 - 50) / 2 + 40 + 10) -- Should be positioned after first child + gap - - luaunit.assertEquals(child2_3.x, 0) - luaunit.assertEquals(child2_3.y, (100 - 60) / 2 + 40 + 10 + 50 + 10) -- Should be positioned after second child + gap - - -- Verify that children in third branch are laid out correctly (flex-end) - luaunit.assertEquals(child3_1.x, 0) - luaunit.assertEquals(child3_1.y, 100 - 40) -- Should be at bottom position - - luaunit.assertEquals(child3_2.x, 0) - luaunit.assertEquals(child3_2.y, 100 - 50 - 10) -- Should be positioned after first child + gap -end - -function TestBranchingLayouts:testMixedFlexDirectionInBranches() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a child branch with horizontal direction - local horizontalBranch = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 200, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a child branch with vertical direction - local verticalBranch = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 200, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add children to horizontal branch - local child1 = Gui.new({ - parent = horizontalBranch, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = horizontalBranch, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Add children to vertical branch - local child3 = Gui.new({ - parent = verticalBranch, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 3", - }) - - local child4 = Gui.new({ - parent = verticalBranch, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 4", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the branches are positioned correctly - luaunit.assertEquals(horizontalBranch.x, 0) - luaunit.assertEquals(horizontalBranch.y, 0) - - luaunit.assertEquals(verticalBranch.x, 200) - luaunit.assertEquals(verticalBranch.y, 0) - - -- Verify that children in horizontal branch are laid out horizontally - luaunit.assertEquals(child1.x, 0) - luaunit.assertEquals(child1.y, 0) - - luaunit.assertEquals(child2.x, 50 + 10) -- Should be positioned after first child + gap - luaunit.assertEquals(child2.y, 0) - - -- Verify that children in vertical branch are laid out vertically - luaunit.assertEquals(child3.x, 0) - luaunit.assertEquals(child3.y, 0) - - luaunit.assertEquals(child4.x, 0) - luaunit.assertEquals(child4.y, 30 + 10) -- Should be positioned after first child + gap -end - -function TestBranchingLayouts:testCrossBranchAlignmentCoordination() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.CENTER, - }) - - -- Create a child branch with different alignment - local branch1 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 150, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create another child branch with different alignment - local branch2 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 150, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add children to first branch - local child1_1 = Gui.new({ - parent = branch1, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child1_2 = Gui.new({ - parent = branch1, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Add children to second branch - local child2_1 = Gui.new({ - parent = branch2, - x = 0, - y = 0, - w = 70, - h = 30, - text = "Button 3", - }) - - local child2_2 = Gui.new({ - parent = branch2, - x = 0, - y = 0, - w = 80, - h = 40, - text = "Button 4", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the branches are positioned correctly with space between - luaunit.assertEquals(branch1.x, 0) - luaunit.assertEquals(branch1.y, (300 - 150) / 2) -- Should be centered vertically - - luaunit.assertEquals(branch2.x, 250) - luaunit.assertEquals(branch2.y, (300 - 150) / 2) -- Should be centered vertically - - -- Verify that children in first branch are laid out with stretch alignment - luaunit.assertEquals(child1_1.x, 0) - luaunit.assertEquals(child1_1.y, 0) - - luaunit.assertEquals(child1_2.x, 0) - luaunit.assertEquals(child1_2.y, 30 + 10) -- Should be positioned after first child + gap - - -- Verify that children in second branch are laid out with center alignment - luaunit.assertEquals(child2_1.x, (150 - 70) / 2) -- Should be centered horizontally - luaunit.assertEquals(child2_1.y, 0) - - luaunit.assertEquals(child2_2.x, (150 - 80) / 2) -- Should be centered horizontally - luaunit.assertEquals(child2_2.y, 30 + 10) -- Should be positioned after first child + gap -end - --- Run the tests -luaunit.LuaUnit.run() - diff --git a/testing/complex-nested-layouts.lua b/testing/complex-nested-layouts.lua deleted file mode 100644 index a5f11ac..0000000 --- a/testing/complex-nested-layouts.lua +++ /dev/null @@ -1,401 +0,0 @@ -package.path = package.path - .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua;./testing/?.lua" - -local luaunit = require("testing.luaunit") -require("testing.love_helper") - -local Gui = require("game.libs.FlexLove").GUI -local enums = require("game.libs.FlexLove").enums - --- Test case for complex nested flex layouts -TestComplexNestedLayouts = {} - -function TestComplexNestedLayouts:testDeepThreeLevelNesting() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create first nested window (level 1) - local level1Window = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 200, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.FLEX_START, - }) - - -- Create second nested window (level 2) - local level2Window = Gui.new({ - parent = level1Window, - x = 0, - y = 0, - w = 100, - h = 75, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.CENTER, - }) - - -- Create third nested window (level 3) - local level3Window = Gui.new({ - parent = level2Window, - x = 0, - y = 0, - w = 50, - h = 30, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.SPACE_AROUND, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add children to level 3 window - local child1 = Gui.new({ - parent = level3Window, - x = 0, - y = 0, - w = 15, - h = 10, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = level3Window, - x = 0, - y = 0, - w = 20, - h = 12, - text = "Button 2", - }) - - local child3 = Gui.new({ - parent = level3Window, - x = 0, - y = 0, - w = 18, - h = 15, - text = "Button 3", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the nested structure is positioned correctly (basic checks) - luaunit.assertTrue(level1Window.x >= 0) - luaunit.assertTrue(level1Window.y >= 0) - - luaunit.assertTrue(level2Window.x >= 0) - luaunit.assertTrue(level2Window.y >= 0) - - luaunit.assertTrue(level3Window.x >= 0) - luaunit.assertTrue(level3Window.y >= 0) - - -- Verify that level 3 children are laid out correctly (basic checks) - luaunit.assertTrue(child1.x >= 0) - luaunit.assertTrue(child1.y >= 0) - - luaunit.assertTrue(child2.x >= 0) - luaunit.assertTrue(child2.y >= 0) - - luaunit.assertTrue(child3.x >= 0) - luaunit.assertTrue(child3.y >= 0) -end - -function TestComplexNestedLayouts:testFourLevelNestingWithMixedDirections() - -- Create a parent window with vertical flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 400, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create first nested window (level 1) - local level1Window = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 250, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_EVENLY, - alignItems = enums.AlignItems.FLEX_START, - }) - - -- Create second nested window (level 2) - local level2Window = Gui.new({ - parent = level1Window, - x = 0, - y = 0, - w = 150, - h = 80, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create third nested window (level 3) - local level3Window = Gui.new({ - parent = level2Window, - x = 0, - y = 0, - w = 75, - h = 40, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_END, - alignItems = enums.AlignItems.FLEX_START, - }) - - -- Create fourth nested window (level 4) - local level4Window = Gui.new({ - parent = level3Window, - x = 0, - y = 0, - w = 30, - h = 20, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add children to level 4 window - local child1 = Gui.new({ - parent = level4Window, - x = 0, - y = 0, - w = 10, - h = 8, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = level4Window, - x = 0, - y = 0, - w = 12, - h = 10, - text = "Button 2", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the nested structure is positioned correctly (basic checks) - luaunit.assertTrue(level1Window.x >= 0) - luaunit.assertTrue(level1Window.y >= 0) - - luaunit.assertTrue(level2Window.x >= 0) - luaunit.assertTrue(level2Window.y >= 0) - - luaunit.assertTrue(level3Window.x >= 0) - luaunit.assertTrue(level3Window.y >= 0) - - luaunit.assertTrue(level4Window.x >= 0) - luaunit.assertTrue(level4Window.y >= 0) - - -- Verify that level 4 children are laid out correctly (basic checks) - luaunit.assertTrue(child1.x >= 0) - luaunit.assertTrue(child1.y >= 0) - - luaunit.assertTrue(child2.x >= 0) - luaunit.assertTrue(child2.y >= 0) -end - -function TestComplexNestedLayouts:testBranchingNestingStructure() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create first branch (left side) - local leftBranch = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 200, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create second branch (right side) - local rightBranch = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 200, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.FLEX_START, - }) - - -- Create children for left branch - local leftChild1 = Gui.new({ - parent = leftBranch, - x = 0, - y = 0, - w = 100, - h = 50, - text = "Left Child 1", - }) - - local leftChild2 = Gui.new({ - parent = leftBranch, - x = 0, - y = 0, - w = 100, - h = 40, - text = "Left Child 2", - }) - - -- Create children for right branch - local rightChild1 = Gui.new({ - parent = rightBranch, - x = 0, - y = 0, - w = 150, - h = 60, - text = "Right Child 1", - }) - - local rightChild2 = Gui.new({ - parent = rightBranch, - x = 0, - y = 0, - w = 150, - h = 70, - text = "Right Child 2", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the branches are positioned correctly (basic checks) - luaunit.assertTrue(leftBranch.x >= 0) - luaunit.assertTrue(leftBranch.y >= 0) - - luaunit.assertTrue(rightBranch.x >= 0) - luaunit.assertTrue(rightBranch.y >= 0) - - -- Verify that left branch children are laid out correctly (basic checks) - luaunit.assertTrue(leftChild1.x >= 0) - luaunit.assertTrue(leftChild1.y >= 0) - - luaunit.assertTrue(leftChild2.x >= 0) - luaunit.assertTrue(leftChild2.y >= 0) - - -- Verify that right branch children are laid out correctly (basic checks) - luaunit.assertTrue(rightChild1.x >= 0) - luaunit.assertTrue(rightChild1.y >= 0) - - luaunit.assertTrue(rightChild2.x >= 0) - luaunit.assertTrue(rightChild2.y >= 0) -end - -function TestComplexNestedLayouts:testComplexAlignmentInNesting() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.CENTER, - }) - - -- Create nested window with different alignment - local nestedWindow = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 150, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.FLEX_END, - }) - - -- Create children with different sizes - local child1 = Gui.new({ - parent = nestedWindow, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = nestedWindow, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - local child3 = Gui.new({ - parent = nestedWindow, - x = 0, - y = 0, - w = 45, - h = 35, - text = "Button 3", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that the nested structure is positioned correctly (basic checks) - luaunit.assertTrue(nestedWindow.x >= 0) - luaunit.assertTrue(nestedWindow.y >= 0) - - -- Verify that children are laid out correctly (basic checks) - luaunit.assertTrue(child1.x >= 0) - luaunit.assertTrue(child1.y >= 0) - - luaunit.assertTrue(child2.x >= 0) - luaunit.assertTrue(child2.y >= 0) - - luaunit.assertTrue(child3.x >= 0) - luaunit.assertTrue(child3.y >= 0) -end - --- Run the tests -luaunit.LuaUnit.run() - diff --git a/testing/depth-layout-tests.lua b/testing/depth-layout-tests.lua deleted file mode 100644 index cd519fb..0000000 --- a/testing/depth-layout-tests.lua +++ /dev/null @@ -1,393 +0,0 @@ -package.path = package.path - .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua;./testing/?.lua" - -local luaunit = require("testing.luaunit") -require("testing.love_helper") - -local Gui = require("game.libs.FlexLove").GUI -local enums = require("game.libs.FlexLove").enums - --- Test case for depth testing in nested layouts -TestDepthLayouts = {} - -function TestDepthLayouts:testMaximumNestingDepth() - -- Create a parent window with horizontal flex direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a deeply nested structure (5 levels deep) - local level1 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 250, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local level2 = Gui.new({ - parent = level1, - x = 0, - y = 0, - w = 200, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local level3 = Gui.new({ - parent = level2, - x = 0, - y = 0, - w = 150, - h = 80, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.CENTER, - }) - - local level4 = Gui.new({ - parent = level3, - x = 0, - y = 0, - w = 100, - h = 60, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local level5 = Gui.new({ - parent = level4, - x = 0, - y = 0, - w = 50, - h = 40, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_END, - alignItems = enums.AlignItems.FLEX_END, - }) - - -- Add a child to the deepest level - local deepChild = Gui.new({ - parent = level5, - x = 0, - y = 0, - w = 20, - h = 15, - text = "Deep Child", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that each level is positioned correctly - luaunit.assertEquals(level1.x, 0) - luaunit.assertEquals(level1.y, 0) - - luaunit.assertEquals(level2.x, 0) - luaunit.assertEquals(level2.y, 0) - - luaunit.assertEquals(level3.x, 0) - luaunit.assertEquals(level3.y, 0) - - luaunit.assertEquals(level4.x, 0) - luaunit.assertEquals(level4.y, 0) - - luaunit.assertEquals(level5.x, 0) - luaunit.assertEquals(level5.y, 0) - - -- Verify that the deepest child is positioned correctly - luaunit.assertEquals(deepChild.x, 0) - luaunit.assertEquals(deepChild.y, 40 - 15) -- Should be at bottom position -end - -function TestDepthLayouts:testPropertyInheritanceThroughNesting() - -- Create a parent window with specific properties - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.CENTER, - flexWrap = enums.FlexWrap.WRAP, - }) - - -- Create nested structure with inherited properties - local level1 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 250, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local level2 = Gui.new({ - parent = level1, - x = 0, - y = 0, - w = 200, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add children to each level - local child1 = Gui.new({ - parent = level1, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Child 1", - }) - - local child2 = Gui.new({ - parent = level2, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Child 2", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that properties are inherited appropriately - -- The parent's flexWrap should be preserved through nesting - -- The level1's flexDirection should be VERTICAL, and level2's should be HORIZONTAL - luaunit.assertEquals(level1.x, 0) - luaunit.assertEquals(level1.y, (200 - 150) / 2) -- Centered vertically - - luaunit.assertEquals(level2.x, 0) - luaunit.assertEquals(level2.y, 0) - - -- Verify that children are positioned correctly based on their container's properties - luaunit.assertEquals(child1.x, 0) - luaunit.assertEquals(child1.y, (150 - 30) / 2) -- Centered vertically within level1 - - luaunit.assertEquals(child2.x, (200 - 60) / 2) -- Centered horizontally within level2 - luaunit.assertEquals(child2.y, (100 - 40) / 2) -- Centered vertically within level2 -end - -function TestDepthLayouts:testSizeCalculationAccuracyAtDepth() - -- Create a parent window with specific dimensions - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create nested structure with precise sizing - local level1 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local level2 = Gui.new({ - parent = level1, - x = 0, - y = 0, - w = 250, - h = 150, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local level3 = Gui.new({ - parent = level2, - x = 0, - y = 0, - w = 200, - h = 100, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add children to the deepest level - local child1 = Gui.new({ - parent = level3, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Child 1", - }) - - local child2 = Gui.new({ - parent = level3, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Child 2", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that dimensions are preserved through nesting - luaunit.assertEquals(level1.w, 300) - luaunit.assertEquals(level1.h, 200) - - luaunit.assertEquals(level2.w, 250) - luaunit.assertEquals(level2.h, 150) - - luaunit.assertEquals(level3.w, 200) - luaunit.assertEquals(level3.h, 100) - - -- Verify that children are positioned correctly within their containers - luaunit.assertEquals(child1.x, (200 - 50) / 2) -- Centered horizontally within level3 - luaunit.assertEquals(child1.y, (100 - 30) / 2) -- Centered vertically within level3 - - luaunit.assertEquals(child2.x, (200 - 60) / 2 + 50 + 10) -- Positioned after first child + gap - luaunit.assertEquals(child2.y, (100 - 40) / 2) -- Centered vertically within level3 -end - -function TestDepthLayouts:testEdgeCasesInDeepLayouts() - -- Create a parent window with complex layout properties - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 400, - h = 300, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.STRETCH, - flexWrap = enums.FlexWrap.WRAP, - }) - - -- Create a deep nested structure with varying child sizes - local level1 = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 350, - h = 250, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - local level2 = Gui.new({ - parent = level1, - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.CENTER, - }) - - -- Add children with different sizes at various depths - local child1 = Gui.new({ - parent = level2, - x = 0, - y = 0, - w = 80, - h = 40, - text = "Child 1", - }) - - local child2 = Gui.new({ - parent = level2, - x = 0, - y = 0, - w = 100, - h = 50, - text = "Child 2", - }) - - -- Add children to the deepest level - local deepChild1 = Gui.new({ - parent = level1, - x = 0, - y = 0, - w = 30, - h = 20, - text = "Deep Child 1", - }) - - local deepChild2 = Gui.new({ - parent = level1, - x = 0, - y = 0, - w = 40, - h = 25, - text = "Deep Child 2", - }) - - -- Layout all children - parentWindow:layoutChildren() - - -- Verify that edge cases are handled correctly - luaunit.assertEquals(level1.x, 0) - luaunit.assertEquals(level1.y, 0) - - luaunit.assertEquals(level2.x, 0) - luaunit.assertEquals(level2.y, 0) - - -- Verify children in level2 are positioned correctly (centered) - luaunit.assertEquals(child1.x, (300 - 80) / 2) -- Centered horizontally - luaunit.assertEquals(child1.y, (200 - 40) / 2) -- Centered vertically - - luaunit.assertEquals(child2.x, (300 - 100) / 2 + 80 + 10) -- Positioned after first child + gap - luaunit.assertEquals(child2.y, (200 - 50) / 2) -- Centered vertically - - -- Verify children in level1 are positioned correctly - luaunit.assertEquals(deepChild1.x, 0) - luaunit.assertEquals(deepChild1.y, 0) - - luaunit.assertEquals(deepChild2.x, 0) - luaunit.assertEquals(deepChild2.y, 20 + 10) -- Positioned after first child + gap -end - --- Run the tests -luaunit.LuaUnit.run() - diff --git a/testing/flex-direction-tests.lua b/testing/flex-direction-tests.lua deleted file mode 100644 index 8f9315d..0000000 --- a/testing/flex-direction-tests.lua +++ /dev/null @@ -1,163 +0,0 @@ -package.path = package.path - .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua;./testing/?.lua" - -local luaunit = require("testing.luaunit") -require("testing.love_helper") - -local Gui = require("game.libs.FlexLove").GUI -local enums = require("game.libs.FlexLove").enums - --- Test case for flex direction properties -TestFlexDirection = {} - -function TestFlexDirection:testHorizontalFlexDirection() - -- Create a window with horizontal flex direction - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Verify window properties - luaunit.assertEquals(window.flexDirection, enums.FlexDirection.HORIZONTAL) -end - -function TestFlexDirection:testVerticalFlexDirection() - -- Create a window with vertical flex direction - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Verify window properties - luaunit.assertEquals(window.flexDirection, enums.FlexDirection.VERTICAL) -end - -function TestFlexDirection:testHorizontalLayoutChildren() - -- Create a horizontal flex container - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- Verify positions for horizontal layout (children should be placed side by side) - luaunit.assertEquals(child1.x, 0) -- First child at start position - luaunit.assertEquals(child1.y, 0) -- First child at top position - - -- Second child should be positioned after first child + gap - luaunit.assertEquals(child2.x, 50 + 10) -- child1 width + gap - luaunit.assertEquals(child2.y, 0) -- Same y position as first child -end - -function TestFlexDirection:testVerticalLayoutChildren() - -- Create a vertical flex container - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- Verify positions for vertical layout (children should be placed one below another) - luaunit.assertEquals(child1.x, 0) -- First child at left position - luaunit.assertEquals(child1.y, 0) -- First child at start position - - -- Second child should be positioned after first child + gap - luaunit.assertEquals(child2.x, 0) -- Same x position as first child - luaunit.assertEquals(child2.y, 30 + 10) -- child1 height + gap -end - -function TestFlexDirection:testFlexDirectionInheritance() - -- Create a parent with horizontal direction - local parentWindow = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Create a child without explicit direction (should inherit) - local child = Gui.new({ - parent = parentWindow, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Test Button", - }) - - -- Verify child inherits flex direction from parent - luaunit.assertEquals(child.flexDirection, enums.FlexDirection.HORIZONTAL) -end - --- Run the tests -luaunit.LuaUnit.run() diff --git a/testing/justify-content-tests.lua b/testing/justify-content-tests.lua deleted file mode 100644 index 170ab0f..0000000 --- a/testing/justify-content-tests.lua +++ /dev/null @@ -1,435 +0,0 @@ -package.path = package.path - .. ";./?.lua;./game/?.lua;./game/utils/?.lua;./game/components/?.lua;./game/systems/?.lua;./testing/?.lua" - -local luaunit = require("testing.luaunit") -require("testing.love_helper") - -local Gui = require("game.libs.FlexLove").GUI -local enums = require("game.libs.FlexLove").enums - --- Test case for justify content alignment properties -TestJustifyContent = {} - -function TestJustifyContent:testFlexStartJustifyContent() - -- Create a horizontal flex container with flex-start justify content - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- With flex-start, children should start at the beginning of the container - -- CSS behavior: first child positioned at start (leftmost for horizontal, topmost for vertical) - luaunit.assertAlmostEquals(child1.x, 0) -- First child at start position -end - -function TestJustifyContent:testCenterJustifyContent() - -- Create a horizontal flex container with center justify content - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- With center, children should be centered in the container - -- CSS behavior: children should be centered within the container's available space - -- Calculate expected position based on container width and child sizes - local totalWidth = child1.width + child2.width + window.gap -- child1.width + child2.width + gap - local containerWidth = window.width - local expectedPosition = (containerWidth - totalWidth) / 2 - - luaunit.assertAlmostEquals(child1.x, expectedPosition) -end - -function TestJustifyContent:testFlexEndJustifyContent() - -- Create a horizontal flex container with flex-end justify content - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_END, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- With flex-end, children should be positioned at the end of the container - -- CSS behavior: children positioned at the end (rightmost for horizontal, bottommost for vertical) - local totalWidth = child1.width + child2.width + window.gap -- child1.width + child2.width + gap - local containerWidth = window.width - local expectedPosition = containerWidth - totalWidth - - luaunit.assertAlmostEquals(child1.x, expectedPosition) -end - -function TestJustifyContent:testSpaceAroundJustifyContent() - -- Create a horizontal flex container with space-around justify content - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_AROUND, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- With space-around, there should be equal spacing around each child - -- CSS behavior: each child should have equal spacing on both sides (including edges) - -- This test ensures the function doesn't crash and children are positioned - luaunit.assertNotNil(child1.x) -end - -function TestJustifyContent:testSpaceEvenlyJustifyContent() - -- Create a horizontal flex container with space-evenly justify content - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_EVENLY, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- With space-evenly, there should be equal spacing between each child - -- CSS behavior: spacing is distributed evenly across the container - -- This test ensures the function doesn't crash and children are positioned - luaunit.assertNotNil(child1.x) -end - -function TestJustifyContent:testSpaceBetweenJustifyContent() - -- Create a horizontal flex container with space-between justify content - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_BETWEEN, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- With space-between, there should be equal spacing between each child - -- CSS behavior: first and last child at edges, others spaced evenly in between - -- This test ensures the function doesn't crash and children are positioned - luaunit.assertNotNil(child1.x) -end - -function TestJustifyContent:testVerticalJustifyContent() - -- Create a vertical flex container with justify content properties - local window = Gui.new({ - x = 0, - y = 0, - w = 300, - h = 200, - positioning = enums.Positioning.FLEX, - flexDirection = enums.FlexDirection.VERTICAL, - justifyContent = enums.JustifyContent.CENTER, - alignItems = enums.AlignItems.STRETCH, - }) - - -- Add multiple children - local child1 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 50, - h = 30, - text = "Button 1", - }) - - local child2 = Gui.new({ - parent = window, - x = 0, - y = 0, - w = 60, - h = 40, - text = "Button 2", - }) - - -- Layout children - window:layoutChildren() - - -- With vertical container, justify content affects the Y axis - -- CSS behavior: justify content controls positioning along the main axis (Y for vertical flex) - luaunit.assertNotNil(child1.y) -end - -function TestJustifyContent:testFlexStart() - -- Create a test container with horizontal flexDirection and FLEX_START justifyContent - local container = Gui.new({ - w = 300, - h = 100, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_START, - gap = 10, - }) - - -- Add children with fixed widths - local child1 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - local child2 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - container:layoutChildren() - - -- For FLEX_START, children should be positioned at the start with gaps - luaunit.assertEquals(child1.x, container.x) - luaunit.assertEquals(child2.x, container.x + 50 + 10) -- child1 width + gap -end - -function TestJustifyContent:testCenter() - -- Create a test container with horizontal flexDirection and CENTER justifyContent - local container = Gui.new({ - w = 300, - h = 100, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.CENTER, - gap = 10, - }) - - -- Add children with fixed widths - local child1 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - local child2 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - container:layoutChildren() - - -- For CENTER, children should be centered within available space - -- Total width of children + gaps = 50 + 10 + 50 = 110 - -- Free space = 300 - 110 = 190 - -- Spacing = 190 / 2 = 95 - luaunit.assertEquals(child1.x, container.x + 95) - luaunit.assertEquals(child2.x, container.x + 95 + 50 + 10) -- spacing + child1 width + gap -end - -function TestJustifyContent:testFlexEnd() - -- Create a test container with horizontal flexDirection and FLEX_END justifyContent - local container = Gui.new({ - w = 300, - h = 100, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.FLEX_END, - gap = 10, - }) - - -- Add children with fixed widths - local child1 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - local child2 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - container:layoutChildren() - - -- For FLEX_END, children should be positioned at the end of available space - -- Total width of children + gaps = 50 + 10 + 50 = 110 - -- Free space = 300 - 110 = 190 - -- Spacing = 190 (full free space) - luaunit.assertEquals(child1.x, container.x + 190) - luaunit.assertEquals(child2.x, container.x + 190 + 50 + 10) -- spacing + child1 width + gap -end - -function TestJustifyContent:testSpaceAround() - -- Create a test container with horizontal flexDirection and SPACE_AROUND justifyContent - local container = Gui.new({ - w = 300, - h = 100, - flexDirection = enums.FlexDirection.HORIZONTAL, - justifyContent = enums.JustifyContent.SPACE_AROUND, - gap = 10, - }) - - -- Add children with fixed widths - local child1 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - local child2 = Gui.new({ - w = 50, - h = 50, - parent = container, - }) - - container:layoutChildren() - - -- For SPACE_AROUND, spacing should be freeSpace / (childCount + 1) - -- Total width of children + gaps = 50 + 10 + 50 = 110 - -- Free space = 300 - 110 = 190 - -- Spacing = 190 / (2 + 1) = 63.33 - luaunit.assertEquals(child1.x, container.x + 63.33) - luaunit.assertEquals(child2.x, container.x + 63.33 + 50 + 10) -- spacing + child1 width + gap -end - --- Run the tests -luaunit.LuaUnit.run() diff --git a/testing/loveStub.lua b/testing/loveStub.lua new file mode 100644 index 0000000..ff78c4a --- /dev/null +++ b/testing/loveStub.lua @@ -0,0 +1,75 @@ +-- Stub implementations for LOVE functions to enable testing of FlexLove +-- This file provides mock implementations of LOVE functions used in FlexLove + +local love_helper = {} + +-- Mock window functions +love_helper.window = {} +function love_helper.window.getMode() + return 800, 600 -- Default resolution +end + +-- Mock graphics functions +love_helper.graphics = {} +function love_helper.graphics.newFont(size) + -- Return a mock font object with basic methods + return { + getWidth = function(text) + return #text * size / 2 + end, + getHeight = function() + return size + end, + } +end + +function love_helper.graphics.getFont() + -- Return a mock default font + return { + getWidth = function(text) + return #text * 12 / 2 + end, + getHeight = function() + return 12 + end, + } +end + +function love_helper.graphics.setColor(r, g, b, a) + -- Mock color setting +end + +function love_helper.graphics.rectangle(mode, x, y, width, height) + -- Mock rectangle drawing +end + +function love_helper.graphics.line(x1, y1, x2, y2) + -- Mock line drawing +end + +function love_helper.graphics.print(text, x, y) + -- Mock text printing +end + +-- Mock mouse functions +love_helper.mouse = {} +function love_helper.mouse.getPosition() + return 0, 0 -- Default position +end + +function love_helper.mouse.isDown(button) + return false -- Default not pressed +end + +-- Mock touch functions +love_helper.touch = {} +function love_helper.touch.getTouches() + return {} -- Empty table of touches +end + +function love_helper.touch.getPosition(id) + return 0, 0 -- Default touch position +end + +_G.love = love_helper +return love_helper diff --git a/testing/luaunit.lua b/testing/luaunit.lua new file mode 100644 index 0000000..c2b64b9 --- /dev/null +++ b/testing/luaunit.lua @@ -0,0 +1,3545 @@ +--[[ + luaunit.lua + +Description: A unit testing framework +Homepage: https://github.com/bluebird75/luaunit +Development by Philippe Fremy +Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) +License: BSD License, see LICENSE.txt +]] +-- + +require("math") +local M = {} + +-- private exported functions (for testing) +M.private = {} + +M.VERSION = "3.4" +M._VERSION = M.VERSION -- For LuaUnit v2 compatibility + +-- a version which distinguish between regular Lua and LuaJit +M._LUAVERSION = (jit and jit.version) or _VERSION + +--[[ Some people like assertEquals( actual, expected ) and some people prefer +assertEquals( expected, actual ). +]] +-- +M.ORDER_ACTUAL_EXPECTED = true +M.PRINT_TABLE_REF_IN_ERROR_MSG = false +M.LINE_LENGTH = 80 +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items + +-- this setting allow to remove entries from the stack-trace, for +-- example to hide a call to a framework which would be calling luaunit +M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 0 + +--[[ EPS is meant to help with Lua's floating point math in simple corner +cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers +with rational binary representation) if the user doesn't provide some explicit +error margin. + +The default margin used by almostEquals() in such cases is EPS; and since +Lua may be compiled with different numeric precisions (single vs. double), we +try to select a useful default for it dynamically. Note: If the initial value +is not acceptable, it can be changed by the user to better suit specific needs. + +See also: https://en.wikipedia.org/wiki/Machine_epsilon +]] +M.EPS = 2 ^ -52 -- = machine epsilon for "double", ~2.22E-16 +if math.abs(1.1 - 1 - 0.1) > M.EPS then + -- rounding error is above EPS, assume single precision + M.EPS = 2 ^ -23 -- = machine epsilon for "float", ~1.19E-07 +end + +-- set this to false to debug luaunit +local STRIP_LUAUNIT_FROM_STACKTRACE = true + +M.VERBOSITY_DEFAULT = 10 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 +M.VERBOSITY_VERBOSE = 20 +M.DEFAULT_DEEP_ANALYSIS = nil +M.FORCE_DEEP_ANALYSIS = true +M.DISABLE_DEEP_ANALYSIS = false + +-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values +-- EXPORT_ASSERT_TO_GLOBALS = true + +-- we need to keep a copy of the script args before it is overriden +local cmdline_argv = rawget(_G, "arg") + +M.FAILURE_PREFIX = "LuaUnit test FAILURE: " -- prefix string for failed tests +M.SUCCESS_PREFIX = "LuaUnit test SUCCESS: " -- prefix string for successful tests finished early +M.SKIP_PREFIX = "LuaUnit test SKIP: " -- prefix string for skipped tests + +M.USAGE = [[Usage: lua [options] [testname1 [testname2] ... ] +Options: + -h, --help: Print this help + --version: Print version information + -v, --verbose: Increase verbosity + -q, --quiet: Set verbosity to minimum + -e, --error: Stop on first error + -f, --failure: Stop on first failure or error + -s, --shuffle: Shuffle tests before running them + -o, --output OUTPUT: Set output type to OUTPUT + Possible values: text, tap, junit, nil + -n, --name NAME: For junit only, mandatory name of xml file + -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT + -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN + May be repeated to include several patterns + Make sure you escape magic chars like +? with % + -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN + May be repeated to exclude several patterns + Make sure you escape magic chars like +? with % + testname1, testname2, ... : tests to run in the form of testFunction, + TestClass or TestClass.testMethod + +You may also control LuaUnit options with the following environment variables: +* LUAUNIT_OUTPUT: same as --output +* LUAUNIT_JUNIT_FNAME: same as --name ]] + +---------------------------------------------------------------- +-- +-- general utility functions +-- +---------------------------------------------------------------- + +--[[ Note on catching exit + +I have seen the case where running a big suite of test cases and one of them would +perform a os.exit(0), making the outside world think that the full test suite was executed +successfully. + +This is an attempt to mitigate this problem: we override os.exit() to now let a test +exit the framework while we are running. When we are not running, it behaves normally. +]] + +M.oldOsExit = os.exit +os.exit = function(...) + if M.LuaUnit and #M.LuaUnit.instances ~= 0 then + local msg = [[You are trying to exit but there is still a running instance of LuaUnit. +LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests. + +To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded): + + lu.unregisterCurrentSuite() + +]] + M.private.error_fmt(2, msg) + end + M.oldOsExit(...) +end + +local function pcall_or_abort(func, ...) + -- unpack is a global function for Lua 5.1, otherwise use table.unpack + local unpack = rawget(_G, "unpack") or table.unpack + local result = { pcall(func, ...) } + if not result[1] then + -- an error occurred + print(result[2]) -- error message + print() + print(M.USAGE) + os.exit(-1) + end + return unpack(result, 2) +end + +local crossTypeOrdering = { + number = 1, + boolean = 2, + string = 3, + table = 4, + other = 5, +} +local crossTypeComparison = { + number = function(a, b) + return a < b + end, + string = function(a, b) + return a < b + end, + other = function(a, b) + return tostring(a) < tostring(b) + end, +} + +local function crossTypeSort(a, b) + local type_a, type_b = type(a), type(b) + if type_a == type_b then + local func = crossTypeComparison[type_a] or crossTypeComparison.other + return func(a, b) + end + type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other + type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other + return type_a < type_b +end + +local function __genSortedIndex(t) + -- Returns a sequence consisting of t's keys, sorted. + local sortedIndex = {} + + for key, _ in pairs(t) do + table.insert(sortedIndex, key) + end + + table.sort(sortedIndex, crossTypeSort) + return sortedIndex +end +M.private.__genSortedIndex = __genSortedIndex + +local function sortedNext(state, control) + -- Equivalent of the next() function of table iteration, but returns the + -- keys in sorted order (see __genSortedIndex and crossTypeSort). + -- The state is a temporary variable during iteration and contains the + -- sorted key table (state.sortedIdx). It also stores the last index (into + -- the keys) used by the iteration, to find the next one quickly. + local key + + --print("sortedNext: control = "..tostring(control) ) + if control == nil then + -- start of iteration + state.count = #state.sortedIdx + state.lastIdx = 1 + key = state.sortedIdx[1] + return key, state.t[key] + end + + -- normally, we expect the control variable to match the last key used + if control ~= state.sortedIdx[state.lastIdx] then + -- strange, we have to find the next value by ourselves + -- the key table is sorted in crossTypeSort() order! -> use bisection + local lower, upper = 1, state.count + repeat + state.lastIdx = math.modf((lower + upper) / 2) + key = state.sortedIdx[state.lastIdx] + if key == control then + break -- key found (and thus prev index) + end + if crossTypeSort(key, control) then + -- key < control, continue search "right" (towards upper bound) + lower = state.lastIdx + 1 + else + -- key > control, continue search "left" (towards lower bound) + upper = state.lastIdx - 1 + end + until lower > upper + if lower > upper then -- only true if the key wasn't found, ... + state.lastIdx = state.count -- ... so ensure no match in code below + end + end + + -- proceed by retrieving the next value (or nil) from the sorted keys + state.lastIdx = state.lastIdx + 1 + key = state.sortedIdx[state.lastIdx] + if key then + return key, state.t[key] + end + + -- getting here means returning `nil`, which will end the iteration +end + +local function sortedPairs(tbl) + -- Equivalent of the pairs() function on tables. Allows to iterate in + -- sorted order. As required by "generic for" loops, this will return the + -- iterator (function), an "invariant state", and the initial control value. + -- (see http://www.lua.org/pil/7.2.html) + return sortedNext, { t = tbl, sortedIdx = __genSortedIndex(tbl) }, nil +end +M.private.sortedPairs = sortedPairs + +-- seed the random with a strongly varying seed +math.randomseed(math.floor(os.clock() * 1E11)) + +local function randomizeTable(t) + -- randomize the item orders of the table t + for i = #t, 2, -1 do + local j = math.random(i) + if i ~= j then + t[i], t[j] = t[j], t[i] + end + end +end +M.private.randomizeTable = randomizeTable + +local function strsplit(delimiter, text) + -- Split text into a list consisting of the strings in text, separated + -- by strings matching delimiter (which may _NOT_ be a pattern). + -- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + if delimiter == "" or delimiter == nil then -- this would result in endless loops + error("delimiter is nil or empty string!") + end + if text == nil then + return nil + end + + local list, pos, first, last = {}, 1 + while true do + first, last = text:find(delimiter, pos, true) + if first then -- found? + table.insert(list, text:sub(pos, first - 1)) + pos = last + 1 + else + table.insert(list, text:sub(pos)) + break + end + end + return list +end +M.private.strsplit = strsplit + +local function hasNewLine(s) + -- return true if s has a newline + return (string.find(s, "\n", 1, true) ~= nil) +end +M.private.hasNewLine = hasNewLine + +local function prefixString(prefix, s) + -- Prefix all the lines of s with prefix + return prefix .. string.gsub(s, "\n", "\n" .. prefix) +end +M.private.prefixString = prefixString + +local function strMatch(s, pattern, start, final) + -- return true if s matches completely the pattern from index start to index end + -- return false in every other cases + -- if start is nil, matches from the beginning of the string + -- if final is nil, matches to the end of the string + start = start or 1 + final = final or string.len(s) + + local foundStart, foundEnd = string.find(s, pattern, start, false) + return foundStart == start and foundEnd == final +end +M.private.strMatch = strMatch + +local function patternFilter(patterns, expr) + -- Run `expr` through the inclusion and exclusion rules defined in patterns + -- and return true if expr shall be included, false for excluded. + -- Inclusion pattern are defined as normal patterns, exclusions + -- patterns start with `!` and are followed by a normal pattern + + -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT + -- default: true if no explicit "include" is found, set to false otherwise + local default, result = true, nil + + if patterns ~= nil then + for _, pattern in ipairs(patterns) do + local exclude = pattern:sub(1, 1) == "!" + if exclude then + pattern = pattern:sub(2) + else + -- at least one include pattern specified, a match is required + default = false + end + -- print('pattern: ',pattern) + -- print('exclude: ',exclude) + -- print('default: ',default) + + if string.find(expr, pattern) then + -- set result to false when excluding, true otherwise + result = not exclude + end + end + end + + if result ~= nil then + return result + end + return default +end +M.private.patternFilter = patternFilter + +local function xmlEscape(s) + -- Return s escaped for XML attributes + -- escapes table: + -- " " + -- ' ' + -- < < + -- > > + -- & & + + return string.gsub(s, ".", { + ["&"] = "&", + ['"'] = """, + ["'"] = "'", + ["<"] = "<", + [">"] = ">", + }) +end +M.private.xmlEscape = xmlEscape + +local function xmlCDataEscape(s) + -- Return s escaped for CData section, escapes: "]]>" + return string.gsub(s, "]]>", "]]>") +end +M.private.xmlCDataEscape = xmlCDataEscape + +local function lstrip(s) + --[[Return s with all leading white spaces and tabs removed]] + local idx = 0 + while idx < s:len() do + idx = idx + 1 + local c = s:sub(idx, idx) + if c ~= " " and c ~= "\t" then + break + end + end + return s:sub(idx) +end +M.private.lstrip = lstrip + +local function extractFileLineInfo(s) + --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" + + Return the "file.lua:linenb" information + ]] + local s2 = lstrip(s) + local firstColon = s2:find(":", 1, true) + if firstColon == nil then + -- string is not in the format file:line: + return s + end + local secondColon = s2:find(":", firstColon + 1, true) + if secondColon == nil then + -- string is not in the format file:line: + return s + end + + return s2:sub(1, secondColon - 1) +end +M.private.extractFileLineInfo = extractFileLineInfo + +local function stripLuaunitTrace2(stackTrace, errMsg) + --[[ + -- Example of a traceback: + < + [C]: in function 'xpcall' + ./luaunit.lua:1449: in function 'protectedCall' + ./luaunit.lua:1508: in function 'execOneFunction' + ./luaunit.lua:1596: in function 'runSuiteByInstances' + ./luaunit.lua:1660: in function 'runSuiteByNames' + ./luaunit.lua:1736: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + Other example: + < + [C]: in function 'xpcall' + ./luaunit.lua:1517: in function 'protectedCall' + ./luaunit.lua:1578: in function 'execOneFunction' + ./luaunit.lua:1677: in function 'runSuiteByInstances' + ./luaunit.lua:1730: in function 'runSuiteByNames' + ./luaunit.lua:1806: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + < + [C]: in function 'xpcall' + luaunit2/luaunit.lua:1532: in function 'protectedCall' + luaunit2/luaunit.lua:1591: in function 'execOneFunction' + luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' + luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' + luaunit2/luaunit.lua:1819: in function 'runSuite' + luaunit2/example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + + -- first line is "stack traceback": KEEP + -- next line may be luaunit line: REMOVE + -- next lines are call in the program under testOk: REMOVE + -- next lines are calls from luaunit to call the program under test: KEEP + + -- Strategy: + -- keep first line + -- remove lines that are part of luaunit + -- kepp lines until we hit a luaunit line + + The strategy for stripping is: + * keep first line "stack traceback:" + * part1: + * analyse all lines of the stack from bottom to top of the stack (first line to last line) + * extract the "file:line:" part of the line + * compare it with the "file:line" part of the error message + * if it does not match strip the line + * if it matches, keep the line and move to part 2 + * part2: + * anything NOT starting with luaunit.lua is the interesting part of the stack trace + * anything starting again with luaunit.lua is part of the test launcher and should be stripped out + ]] + + local function isLuaunitInternalLine(s) + -- return true if line of stack trace comes from inside luaunit + return s:find("[/\\]luaunit%.lua:%d+: ") ~= nil + end + + -- print( '<<'..stackTrace..'>>' ) + + local t = strsplit("\n", stackTrace) + -- print( prettystr(t) ) + + local idx = 2 + + local errMsgFileLine = extractFileLineInfo(errMsg) + -- print('emfi="'..errMsgFileLine..'"') + + -- remove lines that are still part of luaunit + while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do + -- print('Removing : '..t[idx] ) + table.remove(t, idx) + end + + -- keep lines until we hit luaunit again + while t[idx] and (not isLuaunitInternalLine(t[idx])) do + -- print('Keeping : '..t[idx] ) + idx = idx + 1 + end + + -- remove remaining luaunit lines + while t[idx] do + -- print('Removing2 : '..t[idx] ) + table.remove(t, idx) + end + + -- print( prettystr(t) ) + return table.concat(t, "\n") +end +M.private.stripLuaunitTrace2 = stripLuaunitTrace2 + +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable) + local type_v = type(v) + if "string" == type_v then + -- use clever delimiters according to content: + -- enclose with single quotes if string contains ", but no ' + if v:find('"', 1, true) and not v:find("'", 1, true) then + return "'" .. v .. "'" + end + -- use double quotes otherwise, escape embedded " + return '"' .. v:gsub('"', '\\"') .. '"' + elseif "table" == type_v then + --if v.__class__ then + -- return string.gsub( tostring(v), 'table', v.__class__ ) + --end + return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) + elseif "number" == type_v then + -- eliminate differences in formatting between various Lua versions + if v ~= v then + return "#NaN" -- "not a number" + end + if v == math.huge then + return "#Inf" -- "infinite" + end + if v == -math.huge then + return "-#Inf" + end + if _VERSION == "Lua 5.3" then + local i = math.tointeger(v) + if i then + return tostring(i) + end + end + end + + return tostring(v) +end + +local function prettystr(v) + --[[ Pretty string conversion, to display the full content of a variable of any type. + + * string are enclosed with " by default, or with ' if string contains a " + * tables are expanded to show their full content, with indentation in case of nested tables + ]] + -- + local cycleDetectTable = {} + local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) + if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then + -- some table contain recursive references, + -- so we must recompute the value by including all table references + -- else the result looks like crap + cycleDetectTable = {} + s = prettystr_sub(v, 1, true, cycleDetectTable) + end + return s +end +M.prettystr = prettystr + +function M.adjust_err_msg_with_iter(err_msg, iter_msg) + --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, + add the iteration message if any and return the result. + + err_msg: string, error message captured with pcall + iter_msg: a string describing the current iteration ("iteration N") or nil + if there is no iteration in this test. + + Returns: (new_err_msg, test_status) + new_err_msg: string, adjusted error message, or nil in case of success + test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information + contained in the error message. + ]] + if iter_msg then + iter_msg = iter_msg .. ", " + else + iter_msg = "" + end + + local RE_FILE_LINE = ".*:%d+: " + + -- error message is not necessarily a string, + -- so convert the value to string with prettystr() + if type(err_msg) ~= "string" then + err_msg = prettystr(err_msg) + end + + if (err_msg:find(M.SUCCESS_PREFIX) == 1) or err_msg:match("(" .. RE_FILE_LINE .. ")" .. M.SUCCESS_PREFIX .. ".*") then + -- test finished early with success() + return nil, M.NodeStatus.SUCCESS + end + + if + (err_msg:find(M.SKIP_PREFIX) == 1) + or (err_msg:match("(" .. RE_FILE_LINE .. ")" .. M.SKIP_PREFIX .. ".*") ~= nil) + then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(".*" .. M.SKIP_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.SKIP + end + + if + (err_msg:find(M.FAILURE_PREFIX) == 1) + or (err_msg:match("(" .. RE_FILE_LINE .. ")" .. M.FAILURE_PREFIX .. ".*") ~= nil) + then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.FAIL + end + + -- print("error detected") + -- regular error, not a failure + if iter_msg then + local match + -- "./test\\test_luaunit.lua:2241: some error msg + match = err_msg:match("(.*:%d+: ).*") + if match then + err_msg = err_msg:gsub(match, match .. iter_msg) + else + -- no file:line: infromation, just add the iteration info at the beginning of the line + err_msg = iter_msg .. err_msg + end + end + return err_msg, M.NodeStatus.ERROR +end + +local function tryMismatchFormatting(table_a, table_b, doDeepAnalysis, margin) + --[[ + Prepares a nice error message when comparing tables, performing a deeper + analysis. + + Arguments: + * table_a, table_b: tables to be compared + * doDeepAnalysis: + M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries + M.FORCE_DEEP_ANALYSIS : always perform deep analysis + M.DISABLE_DEEP_ANALYSIS: never perform deep analysis + * margin: supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- check if table_a & table_b are suitable for deep analysis + if type(table_a) ~= "table" or type(table_b) ~= "table" then + return false + end + + if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then + return false + end + + local len_a, len_b, isPureList = #table_a, #table_b, true + + for k1, v1 in pairs(table_a) do + if type(k1) ~= "number" or k1 > len_a then + -- this table a mapping + isPureList = false + break + end + end + + if isPureList then + for k2, v2 in pairs(table_b) do + if type(k2) ~= "number" or k2 > len_b then + -- this table a mapping + isPureList = false + break + end + end + end + + if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then + if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then + return false + end + end + + if isPureList then + return M.private.mismatchFormattingPureList(table_a, table_b, margin) + else + -- only work on mapping for the moment + -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + return false + end +end +M.private.tryMismatchFormatting = tryMismatchFormatting + +local function getTaTbDescr() + if not M.ORDER_ACTUAL_EXPECTED then + return "expected", "actual" + end + return "actual", "expected" +end + +local function extendWithStrFmt(res, ...) + table.insert(res, string.format(...)) +end + +local function mismatchFormattingMapping(table_a, table_b, doDeepAnalysis) + --[[ + Prepares a nice error message when comparing tables which are not pure lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- disable for the moment + --[[ + local result = {} + local descrTa, descrTb = getTaTbDescr() + + local keysCommon = {} + local keysOnlyTa = {} + local keysOnlyTb = {} + local keysDiffTaTb = {} + + local k, v + + for k,v in pairs( table_a ) do + if is_equal( v, table_b[k] ) then + table.insert( keysCommon, k ) + else + if table_b[k] == nil then + table.insert( keysOnlyTa, k ) + else + table.insert( keysDiffTaTb, k ) + end + end + end + + for k,v in pairs( table_b ) do + if not is_equal( v, table_a[k] ) and table_a[k] == nil then + table.insert( keysOnlyTb, k ) + end + end + + local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa + local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb + local limited_display = (len_a < 5 or len_b < 5) + + if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then + return false + end + + if not limited_display then + if len_a == len_b then + extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) + else + extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) + end + + if #keysCommon == 0 and #keysDiffTaTb == 0 then + table.insert( result, 'Table A and B have no keys in common, they are totally different') + else + local s_other = 'other ' + if #keysCommon then + extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) + else + table.insert( result, 'Table A and B have no identical items' ) + s_other = '' + end + + if #keysDiffTaTb ~= 0 then + result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) + else + result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) + end + end + + extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) + end + + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr(k) + end + + if #keysDiffTaTb ~= 0 then + table.insert( result, 'Items differing in A and B:') + for k,v in sortedPairs( keysDiffTaTb ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysOnlyTa ~= 0 then + table.insert( result, 'Items only in table A:' ) + for k,v in sortedPairs( keysOnlyTa ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + if #keysOnlyTb ~= 0 then + table.insert( result, 'Items only in table B:' ) + for k,v in sortedPairs( keysOnlyTb ) do + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysCommon ~= 0 then + table.insert( result, 'Items common to A and B:') + for k,v in sortedPairs( keysCommon ) do + extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + return true, table.concat( result, '\n') + ]] +end +M.private.mismatchFormattingMapping = mismatchFormattingMapping + +local function mismatchFormattingPureList(table_a, table_b, margin) + --[[ + Prepares a nice error message when comparing tables which are lists, performing a deeper + analysis. + + margin is supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + local result, descrTa, descrTb = {}, getTaTbDescr() + + local len_a, len_b, refa, refb = #table_a, #table_b, "", "" + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + refa, refb = + string.format("<%s> ", M.private.table_ref(table_a)), string.format("<%s> ", M.private.table_ref(table_b)) + end + local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) + local deltalv = longest - shortest + + local commonUntil = shortest + for i = 1, shortest do + if not M.private.is_table_equals(table_a[i], table_b[i], margin) then + commonUntil = i - 1 + break + end + end + + local commonBackTo = shortest - 1 + for i = 0, shortest - 1 do + if not M.private.is_table_equals(table_a[len_a - i], table_b[len_b - i], margin) then + commonBackTo = i - 1 + break + end + end + + table.insert(result, "List difference analysis:") + if len_a == len_b then + -- TODO: handle expected/actual naming + extendWithStrFmt(result, "* lists %sA (%s) and %sB (%s) have the same size", refa, descrTa, refb, descrTb) + else + extendWithStrFmt( + result, + "* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items", + refa, + descrTa, + len_a, + refb, + descrTb, + len_b + ) + end + + extendWithStrFmt(result, "* lists A and B start differing at index %d", commonUntil + 1) + if commonBackTo >= 0 then + if deltalv > 0 then + extendWithStrFmt( + result, + "* lists A and B are equal again from index %d for A, %d for B", + len_a - commonBackTo, + len_b - commonBackTo + ) + else + extendWithStrFmt(result, "* lists A and B are equal again from index %d", len_a - commonBackTo) + end + end + + local function insertABValue(ai, bi) + bi = bi or ai + if M.private.is_table_equals(table_a[ai], table_b[bi], margin) then + return extendWithStrFmt(result, " = A[%d], B[%d]: %s", ai, bi, prettystr(table_a[ai])) + else + extendWithStrFmt(result, " - A[%d]: %s", ai, prettystr(table_a[ai])) + extendWithStrFmt(result, " + B[%d]: %s", bi, prettystr(table_b[bi])) + end + end + + -- common parts to list A & B, at the beginning + if commonUntil > 0 then + table.insert(result, "* Common parts:") + for i = 1, commonUntil do + insertABValue(i) + end + end + + -- diffing parts to list A & B + if commonUntil < shortest - commonBackTo - 1 then + table.insert(result, "* Differing parts:") + for i = commonUntil + 1, shortest - commonBackTo - 1 do + insertABValue(i) + end + end + + -- display indexes of one list, with no match on other list + if shortest - commonBackTo <= longest - commonBackTo - 1 then + table.insert(result, "* Present only in one list:") + for i = shortest - commonBackTo, longest - commonBackTo - 1 do + if len_a > len_b then + extendWithStrFmt(result, " - A[%d]: %s", i, prettystr(table_a[i])) + -- table.insert( result, '+ (no matching B index)') + else + -- table.insert( result, '- no matching A index') + extendWithStrFmt(result, " + B[%d]: %s", i, prettystr(table_b[i])) + end + end + end + + -- common parts to list A & B, at the end + if commonBackTo >= 0 then + table.insert(result, "* Common parts at the end of the lists") + for i = longest - commonBackTo, longest do + if len_a > len_b then + insertABValue(i, i - deltalv) + else + insertABValue(i - deltalv, i) + end + end + end + + return true, table.concat(result, "\n") +end +M.private.mismatchFormattingPureList = mismatchFormattingPureList + +local function prettystrPairs(value1, value2, suffix_a, suffix_b) + --[[ + This function helps with the recurring task of constructing the "expected + vs. actual" error messages. It takes two arbitrary values and formats + corresponding strings with prettystr(). + + To keep the (possibly complex) output more readable in case the resulting + strings contain line breaks, they get automatically prefixed with additional + newlines. Both suffixes are optional (default to empty strings), and get + appended to the "value1" string. "suffix_a" is used if line breaks were + encountered, "suffix_b" otherwise. + + Returns the two formatted strings (including padding/newlines). + ]] + local str1, str2 = prettystr(value1), prettystr(value2) + if hasNewLine(str1) or hasNewLine(str2) then + -- line break(s) detected, add padding + return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 + end + return str1 .. (suffix_b or ""), str2 +end +M.private.prettystrPairs = prettystrPairs + +local UNKNOWN_REF = "table 00-unknown ref" +local ref_generator = { value = 1, [UNKNOWN_REF] = 0 } + +local function table_ref(t) + -- return the default tostring() for tables, with the table ID, even if the table has a metatable + -- with the __tostring converter + local ref = "" + local mt = getmetatable(t) + if mt == nil then + ref = tostring(t) + else + local success, result + success, result = pcall(setmetatable, t, nil) + if not success then + -- protected table, if __tostring is defined, we can + -- not get the reference. And we can not know in advance. + ref = tostring(t) + if not ref:match("table: 0?x?[%x]+") then + return UNKNOWN_REF + end + else + ref = tostring(t) + setmetatable(t, mt) + end + end + -- strip the "table: " part + ref = ref:sub(8) + if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then + -- Create a new reference number + ref_generator[ref] = ref_generator.value + ref_generator.value = ref_generator.value + 1 + end + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + return string.format("table %02d-%s", ref_generator[ref], ref) + else + return string.format("table %02d", ref_generator[ref]) + end +end +M.private.table_ref = table_ref + +local TABLE_TOSTRING_SEP = ", " +local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) + +local function _table_tostring(tbl, indentLevel, printTableRefs, cycleDetectTable) + printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG + cycleDetectTable = cycleDetectTable or {} + cycleDetectTable[tbl] = true + + local result, dispOnMultLines = {}, false + + -- like prettystr but do not enclose with "" if the string is just alphanumerical + -- this is better for displaying table keys who are often simple strings + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr_sub(k, indentLevel + 1, printTableRefs, cycleDetectTable) + end + + local mt = getmetatable(tbl) + + if mt and mt.__tostring then + -- if table has a __tostring() function in its metatable, use it to display the table + -- else, compute a regular table + result = tostring(tbl) + if type(result) ~= "string" then + return string.format('', prettystr(result)) + end + result = strsplit("\n", result) + return M.private._table_tostring_format_multiline_string(result, indentLevel) + else + -- no metatable, compute the table representation + + local entry, count, seq_index = nil, 0, 1 + for k, v in sortedPairs(tbl) do + -- key part + if k == seq_index then + -- for the sequential part of tables, we'll skip the "=" output + entry = "" + seq_index = seq_index + 1 + elseif cycleDetectTable[k] then + -- recursion in the key detected + cycleDetectTable.detected = true + entry = "<" .. table_ref(k) .. ">=" + else + entry = keytostring(k) .. "=" + end + + -- value part + if cycleDetectTable[v] then + -- recursion in the value detected! + cycleDetectTable.detected = true + entry = entry .. "<" .. table_ref(v) .. ">" + else + entry = entry .. prettystr_sub(v, indentLevel + 1, printTableRefs, cycleDetectTable) + end + count = count + 1 + result[count] = entry + end + return M.private._table_tostring_format_result(tbl, result, indentLevel, printTableRefs) + end +end +M.private._table_tostring = _table_tostring -- prettystr_sub() needs it + +local function _table_tostring_format_multiline_string(tbl_str, indentLevel) + local indentString = "\n" .. string.rep(" ", indentLevel - 1) + return table.concat(tbl_str, indentString) +end +M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string + +local function _table_tostring_format_result(tbl, result, indentLevel, printTableRefs) + -- final function called in _table_to_string() to format the resulting list of + -- string describing the table. + + local dispOnMultLines = false + + -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values + local totalLength = 0 + for k, v in ipairs(result) do + totalLength = totalLength + string.len(v) + if totalLength >= M.LINE_LENGTH then + dispOnMultLines = true + break + end + end + + -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded + -- with the values and the separators. + if not dispOnMultLines then + -- adjust with length of separator(s): + -- two items need 1 sep, three items two seps, ... plus len of '{}' + if #result > 0 then + totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) + end + dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) + end + + -- now reformat the result table (currently holding element strings) + if dispOnMultLines then + local indentString = string.rep(" ", indentLevel - 1) + result = { + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}", + } + else + result = { "{", table.concat(result, TABLE_TOSTRING_SEP), "}" } + end + if printTableRefs then + table.insert(result, 1, "<" .. table_ref(tbl) .. "> ") -- prepend table ref + end + return table.concat(result) +end +M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it + +local function table_findkeyof(t, element) + -- Return the key k of the given element in table t, so that t[k] == element + -- (or `nil` if element is not present within t). Note that we use our + -- 'general' is_equal comparison for matching, so this function should + -- handle table-type elements gracefully and consistently. + if type(t) == "table" then + for k, v in pairs(t) do + if M.private.is_table_equals(v, element) then + return k + end + end + end + return nil +end + +local function _is_table_items_equals(actual, expected) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false + elseif + type_a == "table" --[[and (type_e == 'table')]] + then + for k, v in pairs(actual) do + if table_findkeyof(expected, v) == nil then + return false -- v not contained in expected + end + end + for k, v in pairs(expected) do + if table_findkeyof(actual, v) == nil then + return false -- v not contained in actual + end + end + return true + elseif actual ~= expected then + return false + end + + return true +end + +--[[ +This is a specialized metatable to help with the bookkeeping of recursions +in _is_table_equals(). It provides an __index table that implements utility +functions for easier management of the table. The "cached" method queries +the state of a specific (actual,expected) pair; and the "store" method sets +this state to the given value. The state of pairs not "seen" / visited is +assumed to be `nil`. +]] +local _recursion_cache_MT = { + __index = { + -- Return the cached value for an (actual,expected) pair (or `nil`) + cached = function(t, actual, expected) + local subtable = t[actual] or {} + return subtable[expected] + end, + + -- Store cached value for a specific (actual,expected) pair. + -- Returns the value, so it's easy to use for a "tailcall" (return ...). + store = function(t, actual, expected, value, asymmetric) + local subtable = t[actual] + if not subtable then + subtable = {} + t[actual] = subtable + end + subtable[expected] = value + + -- Unless explicitly marked "asymmetric": Consider the recursion + -- on (expected,actual) to be equivalent to (actual,expected) by + -- default, and thus cache the value for both. + if not asymmetric then + t:store(expected, actual, value, true) + end + + return value + end, + }, +} + +local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual) + --[[Returns true if both table are equal. + + If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead + of strict equality. + + cycleDetectTable is an internal argument used during recursion on tables. + ]] + --print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected).. + -- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )') + + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false -- different types won't match + end + + if type_a == "number" then + if marginForAlmostEqual ~= nil then + return M.almostEquals(actual, expected, marginForAlmostEqual) + else + return actual == expected + end + elseif type_a ~= "table" then + -- other types compare directly + return actual == expected + end + + cycleDetectTable = cycleDetectTable or { actual = {}, expected = {} } + if cycleDetectTable.actual[actual] then + -- oh, we hit a cycle in actual + if cycleDetectTable.expected[expected] then + -- uh, we hit a cycle at the same time in expected + -- so the two tables have similar structure + return true + end + + -- cycle was hit only in actual, the structure differs from expected + return false + end + + if cycleDetectTable.expected[expected] then + -- no cycle in actual, but cycle in expected + -- the structure differ + return false + end + + -- at this point, no table cycle detected, we are + -- seeing this table for the first time + + -- mark the cycle detection + cycleDetectTable.actual[actual] = true + cycleDetectTable.expected[expected] = true + + local actualKeysMatched = {} + for k, v in pairs(actual) do + actualKeysMatched[k] = true -- Keep track of matched keys + if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then + -- table differs on this key + -- clear the cycle detection before returning + cycleDetectTable.actual[actual] = nil + cycleDetectTable.expected[expected] = nil + return false + end + end + + for k, v in pairs(expected) do + if not actualKeysMatched[k] then + -- Found a key that we did not see in "actual" -> mismatch + -- clear the cycle detection before returning + cycleDetectTable.actual[actual] = nil + cycleDetectTable.expected[expected] = nil + return false + end + -- Otherwise actual[k] was already matched against v = expected[k]. + end + + -- all key match, we have a match ! + cycleDetectTable.actual[actual] = nil + cycleDetectTable.expected[expected] = nil + return true +end +M.private._is_table_equals = _is_table_equals + +local function failure(main_msg, extra_msg_or_nil, level) + -- raise an error indicating a test failure + -- for error() compatibility we adjust "level" here (by +1), to report the + -- calling context + local msg + if type(extra_msg_or_nil) == "string" and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. "\n" .. main_msg + else + msg = main_msg + end + error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end + +local function is_table_equals(actual, expected, marginForAlmostEqual) + return _is_table_equals(actual, expected, nil, marginForAlmostEqual) +end +M.private.is_table_equals = is_table_equals + +local function fail_fmt(level, extra_msg_or_nil, ...) + -- failure with printf-style formatted message and given error level + failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) +end +M.private.fail_fmt = fail_fmt + +local function error_fmt(level, ...) + -- printf-style error() + error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end +M.private.error_fmt = error_fmt + +---------------------------------------------------------------- +-- +-- assertions +-- +---------------------------------------------------------------- + +local function errorMsgEquality(actual, expected, doDeepAnalysis, margin) + -- margin is supplied only for almost equal verification + + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + if type(expected) == "string" or type(expected) == "table" then + local strExpected, strActual = prettystrPairs(expected, actual) + local result = string.format("expected: %s\nactual: %s", strExpected, strActual) + if margin then + result = result .. "\nwere not equal by the margin of: " .. prettystr(margin) + end + + -- extend with mismatch analysis if possible: + local success, mismatchResult + success, mismatchResult = tryMismatchFormatting(actual, expected, doDeepAnalysis, margin) + if success then + result = table.concat({ result, mismatchResult }, "\n") + end + return result + end + return string.format("expected: %s, actual: %s", prettystr(expected), prettystr(actual)) +end + +function M.assertError(f, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + if pcall(f, ...) then + failure("Expected an error when calling function but no error generated", nil, 2) + end +end + +function M.fail(msg) + -- stops a test due to a failure + failure(msg, nil, 2) +end + +function M.failIf(cond, msg) + -- Fails a test with "msg" if condition is true + if cond then + failure(msg, nil, 2) + end +end + +function M.skip(msg) + -- skip a running test + error_fmt(2, M.SKIP_PREFIX .. msg) +end + +function M.skipIf(cond, msg) + -- skip a running test if condition is met + if cond then + error_fmt(2, M.SKIP_PREFIX .. msg) + end +end + +function M.runOnlyIf(cond, msg) + -- continue a running test if condition is met, else skip it + if not cond then + error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) + end +end + +function M.success() + -- stops a test with a success + error_fmt(2, M.SUCCESS_PREFIX) +end + +function M.successIf(cond) + -- stops a test with a success if condition is met + if cond then + error_fmt(2, M.SUCCESS_PREFIX) + end +end + +------------------------------------------------------------------ +-- Equality assertions +------------------------------------------------------------------ + +function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) + if type(actual) == "table" and type(expected) == "table" then + if not is_table_equals(actual, expected) then + failure(errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2) + end + elseif type(actual) ~= type(expected) then + failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2) + elseif actual ~= expected then + failure(errorMsgEquality(actual, expected), extra_msg_or_nil, 2) + end +end + +function M.almostEquals(actual, expected, margin) + if type(actual) ~= "number" or type(expected) ~= "number" or type(margin) ~= "number" then + error_fmt( + 3, + "almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s", + prettystr(actual), + prettystr(expected), + prettystr(margin) + ) + end + if margin < 0 then + error_fmt(3, "almostEquals: margin must not be negative, current value is " .. margin) + end + return math.abs(expected - actual) <= margin +end + +function M.assertAlmostEquals(actual, expected, margin, extra_msg_or_nil) + -- check that two floats are close by margin + margin = margin or M.EPS + if type(margin) ~= "number" then + error_fmt(2, "almostEquals: margin must be a number, not %s", prettystr(margin)) + end + + if type(actual) == "table" and type(expected) == "table" then + -- handle almost equals for table + if not is_table_equals(actual, expected, margin) then + failure(errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2) + end + elseif type(actual) == "number" and type(expected) == "number" and type(margin) == "number" then + if not M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt( + 2, + extra_msg_or_nil, + "Values are not almost equal\n" .. "Actual: %s, expected: %s, delta %s above margin of %s", + actual, + expected, + delta, + margin + ) + end + else + error_fmt( + 3, + "almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s", + prettystr(actual), + prettystr(expected), + prettystr(margin) + ) + end +end + +function M.assertNotEquals(actual, expected, extra_msg_or_nil) + if type(actual) ~= type(expected) then + return + end + + if type(actual) == "table" and type(expected) == "table" then + if not is_table_equals(actual, expected) then + return + end + elseif actual ~= expected then + return + end + fail_fmt(2, extra_msg_or_nil, "Received the not expected value: %s", prettystr(actual)) +end + +function M.assertNotAlmostEquals(actual, expected, margin, extra_msg_or_nil) + -- check that two floats are not close by margin + margin = margin or M.EPS + if M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt( + 2, + extra_msg_or_nil, + "Values are almost equal\nActual: %s, expected: %s" .. ", delta %s below margin of %s", + actual, + expected, + delta, + margin + ) + end +end + +function M.assertItemsEquals(actual, expected, extra_msg_or_nil) + -- checks that the items of table expected + -- are contained in table actual. Warning, this function + -- is at least O(n^2) + if not _is_table_items_equals(actual, expected) then + expected, actual = prettystrPairs(expected, actual) + fail_fmt( + 2, + extra_msg_or_nil, + "Content of the tables are not identical:\nExpected: %s\nActual: %s", + expected, + actual + ) + end +end + +------------------------------------------------------------------ +-- String assertion +------------------------------------------------------------------ + +function M.assertStrContains(str, sub, isPattern, extra_msg_or_nil) + -- this relies on lua string.find function + -- a string always contains the empty string + -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) + -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) + if not string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt(2, extra_msg_or_nil, "Could not find %s %s in string %s", isPattern and "pattern" or "substring", sub, str) + end +end + +function M.assertStrIContains(str, sub, extra_msg_or_nil) + -- this relies on lua string.find function + -- a string always contains the empty string + if not string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt(2, extra_msg_or_nil, "Could not find (case insensitively) substring %s in string %s", sub, str) + end +end + +function M.assertNotStrContains(str, sub, isPattern, extra_msg_or_nil) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt( + 2, + extra_msg_or_nil, + "Found the not expected %s %s in string %s", + isPattern and "pattern" or "substring", + sub, + str + ) + end +end + +function M.assertNotStrIContains(str, sub, extra_msg_or_nil) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, "\n") + fail_fmt(2, extra_msg_or_nil, "Found (case insensitively) the not expected substring %s in string %s", sub, str) + end +end + +function M.assertStrMatches(str, pattern, start, final, extra_msg_or_nil) + -- Verify a full match for the string + if not strMatch(str, pattern, start, final) then + pattern, str = prettystrPairs(pattern, str, "\n") + fail_fmt(2, extra_msg_or_nil, "Could not match pattern %s with string %s", pattern, str) + end +end + +local function _assertErrorMsgEquals(stripFileAndLine, expectedMsg, func, ...) + local no_error, error_msg = pcall(func, ...) + if no_error then + failure("No error generated when calling function but expected error: " .. M.prettystr(expectedMsg), nil, 3) + end + if type(expectedMsg) == "string" and type(error_msg) ~= "string" then + -- table are converted to string automatically + error_msg = tostring(error_msg) + end + local differ = false + if stripFileAndLine then + if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then + differ = true + end + else + if error_msg ~= expectedMsg then + local tr = type(error_msg) + local te = type(expectedMsg) + if te == "table" then + if tr ~= "table" then + differ = true + else + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end + end + else + differ = true + end + end + end + + if differ then + error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) + fail_fmt(3, nil, "Error message expected: %s\nError message received: %s\n", expectedMsg, error_msg) + end +end + +function M.assertErrorMsgEquals(expectedMsg, func, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + _assertErrorMsgEquals(false, expectedMsg, func, ...) +end + +function M.assertErrorMsgContentEquals(expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) +end + +function M.assertErrorMsgContains(partialMsg, func, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall(func, ...) + if no_error then + failure("No error generated when calling function but expected error containing: " .. prettystr(partialMsg), nil, 2) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not string.find(error_msg, partialMsg, nil, true) then + error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) + fail_fmt(2, nil, "Error message does not contain: %s\nError message received: %s\n", partialMsg, error_msg) + end +end + +function M.assertErrorMsgMatches(expectedMsg, func, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall(func, ...) + if no_error then + failure('No error generated when calling function but expected error matching: "' .. expectedMsg .. '"', nil, 2) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not strMatch(error_msg, expectedMsg) then + expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) + fail_fmt(2, nil, "Error message does not match pattern: %s\nError message received: %s\n", expectedMsg, error_msg) + end +end + +------------------------------------------------------------------ +-- Type assertions +------------------------------------------------------------------ + +function M.assertEvalToTrue(value, extra_msg_or_nil) + if not value then + failure("expected: a value evaluating to true, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertEvalToFalse(value, extra_msg_or_nil) + if value then + failure("expected: false or nil, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsTrue(value, extra_msg_or_nil) + if value ~= true then + failure("expected: true, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsTrue(value, extra_msg_or_nil) + if value == true then + failure("expected: not true, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsFalse(value, extra_msg_or_nil) + if value ~= false then + failure("expected: false, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsFalse(value, extra_msg_or_nil) + if value == false then + failure("expected: not false, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsNil(value, extra_msg_or_nil) + if value ~= nil then + failure("expected: nil, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNil(value, extra_msg_or_nil) + if value == nil then + failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) + end +end + +--[[ +Add type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type matches the +expected string (derived from the function name): + +M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" +]] +for _, funcName in ipairs({ + "assertIsNumber", + "assertIsString", + "assertIsTable", + "assertIsBoolean", + "assertIsFunction", + "assertIsUserdata", + "assertIsThread", +}) do + local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeExpected = typeExpected and typeExpected:lower() + or error("bad function name '" .. funcName .. "' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) ~= typeExpected then + if type(value) == "nil" then + fail_fmt( + 2, + extra_msg_or_nil, + "expected: a %s value, actual: nil", + typeExpected, + type(value), + prettystrPairs(value) + ) + else + fail_fmt( + 2, + extra_msg_or_nil, + "expected: a %s value, actual: type %s, value %s", + typeExpected, + type(value), + prettystrPairs(value) + ) + end + end + end +end + +--[[ +Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) +M.isXxx(value) -> returns true if type(value) conforms to "xxx" +]] +for _, typeExpected in ipairs({ + "Number", + "String", + "Table", + "Boolean", + "Function", + "Userdata", + "Thread", + "Nil", +}) do + local typeExpectedLower = typeExpected:lower() + local isType = function(value) + return (type(value) == typeExpectedLower) + end + M["is" .. typeExpected] = isType + M["is_" .. typeExpectedLower] = isType +end + +--[[ +Add non-type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type differs from the +expected string (derived from the function name): + +M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" +]] +for _, funcName in ipairs({ + "assertNotIsNumber", + "assertNotIsString", + "assertNotIsTable", + "assertNotIsBoolean", + "assertNotIsFunction", + "assertNotIsUserdata", + "assertNotIsThread", +}) do + local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeUnexpected = typeUnexpected and typeUnexpected:lower() + or error("bad function name '" .. funcName .. "' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) == typeUnexpected then + fail_fmt(2, extra_msg_or_nil, "expected: not a %s type, actual: value %s", typeUnexpected, prettystrPairs(value)) + end + end +end + +function M.assertIs(actual, expected, extra_msg_or_nil) + if actual ~= expected then + if not M.ORDER_ACTUAL_EXPECTED then + actual, expected = expected, actual + end + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + expected, actual = prettystrPairs(expected, actual, "\n", "") + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt( + 2, + extra_msg_or_nil, + "expected and actual object should not be different\nExpected: %s\nReceived: %s", + expected, + actual + ) + end +end + +function M.assertNotIs(actual, expected, extra_msg_or_nil) + if actual == expected then + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + local s_expected + if not M.ORDER_ACTUAL_EXPECTED then + s_expected = prettystrPairs(actual) + else + s_expected = prettystrPairs(expected) + end + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, "expected and actual object should be different: %s", s_expected) + end +end + +------------------------------------------------------------------ +-- Scientific assertions +------------------------------------------------------------------ + +function M.assertIsNaN(value, extra_msg_or_nil) + if type(value) ~= "number" or value == value then + failure("expected: NaN, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNaN(value, extra_msg_or_nil) + if type(value) == "number" and value ~= value then + failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) + end +end + +function M.assertIsInf(value, extra_msg_or_nil) + if type(value) ~= "number" or math.abs(value) ~= math.huge then + failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= math.huge then + failure("expected: #Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsMinusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= -math.huge then + failure("expected: -#Inf, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsPlusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == math.huge then + failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == -math.huge then + failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsInf(value, extra_msg_or_nil) + if type(value) == "number" and math.abs(value) == math.huge then + failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusZero(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= 0 then + failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) + else + if 1 / value == -math.huge then + -- more precise error diagnosis + failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) + else + if 1 / value ~= math.huge then + -- strange, case should have already been covered + failure("expected: +0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertIsMinusZero(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= 0 then + failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) + else + if 1 / value == math.huge then + -- more precise error diagnosis + failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) + else + if 1 / value ~= -math.huge then + -- strange, case should have already been covered + failure("expected: -0.0, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertNotIsPlusZero(value, extra_msg_or_nil) + if type(value) == "number" and (1 / value == math.huge) then + failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusZero(value, extra_msg_or_nil) + if type(value) == "number" and (1 / value == -math.huge) then + failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) + end +end + +function M.assertTableContains(t, expected, extra_msg_or_nil) + -- checks that table t contains the expected element + if table_findkeyof(t, expected) == nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, "Table %s does NOT contain the expected element %s", t, expected) + end +end + +function M.assertNotTableContains(t, expected, extra_msg_or_nil) + -- checks that table t doesn't contain the expected element + local k = table_findkeyof(t, expected) + if k ~= nil then + t, expected = prettystrPairs(t, expected) + fail_fmt( + 2, + extra_msg_or_nil, + "Table %s DOES contain the unwanted element %s (at key %s)", + t, + expected, + prettystr(k) + ) + end +end + +---------------------------------------------------------------- +-- Compatibility layer +---------------------------------------------------------------- + +-- for compatibility with LuaUnit v2.x +function M.wrapFunctions() + -- In LuaUnit version <= 2.1 , this function was necessary to include + -- a test function inside the global test suite. Nowadays, the functions + -- are simply run directly as part of the test discovery process. + -- so just do nothing ! + io.stderr:write([[Use of WrapFunctions() is no longer needed. +Just prefix your test function names with "test" or "Test" and they +will be picked up and run by LuaUnit. +]]) +end + +local list_of_funcs = { + -- { official function name , alias } + + -- general assertions + { "assertEquals", "assert_equals" }, + { "assertItemsEquals", "assert_items_equals" }, + { "assertNotEquals", "assert_not_equals" }, + { "assertAlmostEquals", "assert_almost_equals" }, + { "assertNotAlmostEquals", "assert_not_almost_equals" }, + { "assertEvalToTrue", "assert_eval_to_true" }, + { "assertEvalToFalse", "assert_eval_to_false" }, + { "assertStrContains", "assert_str_contains" }, + { "assertStrIContains", "assert_str_icontains" }, + { "assertNotStrContains", "assert_not_str_contains" }, + { "assertNotStrIContains", "assert_not_str_icontains" }, + { "assertStrMatches", "assert_str_matches" }, + { "assertError", "assert_error" }, + { "assertErrorMsgEquals", "assert_error_msg_equals" }, + { "assertErrorMsgContains", "assert_error_msg_contains" }, + { "assertErrorMsgMatches", "assert_error_msg_matches" }, + { "assertErrorMsgContentEquals", "assert_error_msg_content_equals" }, + { "assertIs", "assert_is" }, + { "assertNotIs", "assert_not_is" }, + { "assertTableContains", "assert_table_contains" }, + { "assertNotTableContains", "assert_not_table_contains" }, + { "wrapFunctions", "WrapFunctions" }, + { "wrapFunctions", "wrap_functions" }, + + -- type assertions: assertIsXXX -> assert_is_xxx + { "assertIsNumber", "assert_is_number" }, + { "assertIsString", "assert_is_string" }, + { "assertIsTable", "assert_is_table" }, + { "assertIsBoolean", "assert_is_boolean" }, + { "assertIsNil", "assert_is_nil" }, + { "assertIsTrue", "assert_is_true" }, + { "assertIsFalse", "assert_is_false" }, + { "assertIsNaN", "assert_is_nan" }, + { "assertIsInf", "assert_is_inf" }, + { "assertIsPlusInf", "assert_is_plus_inf" }, + { "assertIsMinusInf", "assert_is_minus_inf" }, + { "assertIsPlusZero", "assert_is_plus_zero" }, + { "assertIsMinusZero", "assert_is_minus_zero" }, + { "assertIsFunction", "assert_is_function" }, + { "assertIsThread", "assert_is_thread" }, + { "assertIsUserdata", "assert_is_userdata" }, + + -- type assertions: assertIsXXX -> assertXxx + { "assertIsNumber", "assertNumber" }, + { "assertIsString", "assertString" }, + { "assertIsTable", "assertTable" }, + { "assertIsBoolean", "assertBoolean" }, + { "assertIsNil", "assertNil" }, + { "assertIsTrue", "assertTrue" }, + { "assertIsFalse", "assertFalse" }, + { "assertIsNaN", "assertNaN" }, + { "assertIsInf", "assertInf" }, + { "assertIsPlusInf", "assertPlusInf" }, + { "assertIsMinusInf", "assertMinusInf" }, + { "assertIsPlusZero", "assertPlusZero" }, + { "assertIsMinusZero", "assertMinusZero" }, + { "assertIsFunction", "assertFunction" }, + { "assertIsThread", "assertThread" }, + { "assertIsUserdata", "assertUserdata" }, + + -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) + { "assertIsNumber", "assert_number" }, + { "assertIsString", "assert_string" }, + { "assertIsTable", "assert_table" }, + { "assertIsBoolean", "assert_boolean" }, + { "assertIsNil", "assert_nil" }, + { "assertIsTrue", "assert_true" }, + { "assertIsFalse", "assert_false" }, + { "assertIsNaN", "assert_nan" }, + { "assertIsInf", "assert_inf" }, + { "assertIsPlusInf", "assert_plus_inf" }, + { "assertIsMinusInf", "assert_minus_inf" }, + { "assertIsPlusZero", "assert_plus_zero" }, + { "assertIsMinusZero", "assert_minus_zero" }, + { "assertIsFunction", "assert_function" }, + { "assertIsThread", "assert_thread" }, + { "assertIsUserdata", "assert_userdata" }, + + -- type assertions: assertNotIsXXX -> assert_not_is_xxx + { "assertNotIsNumber", "assert_not_is_number" }, + { "assertNotIsString", "assert_not_is_string" }, + { "assertNotIsTable", "assert_not_is_table" }, + { "assertNotIsBoolean", "assert_not_is_boolean" }, + { "assertNotIsNil", "assert_not_is_nil" }, + { "assertNotIsTrue", "assert_not_is_true" }, + { "assertNotIsFalse", "assert_not_is_false" }, + { "assertNotIsNaN", "assert_not_is_nan" }, + { "assertNotIsInf", "assert_not_is_inf" }, + { "assertNotIsPlusInf", "assert_not_plus_inf" }, + { "assertNotIsMinusInf", "assert_not_minus_inf" }, + { "assertNotIsPlusZero", "assert_not_plus_zero" }, + { "assertNotIsMinusZero", "assert_not_minus_zero" }, + { "assertNotIsFunction", "assert_not_is_function" }, + { "assertNotIsThread", "assert_not_is_thread" }, + { "assertNotIsUserdata", "assert_not_is_userdata" }, + + -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) + { "assertNotIsNumber", "assertNotNumber" }, + { "assertNotIsString", "assertNotString" }, + { "assertNotIsTable", "assertNotTable" }, + { "assertNotIsBoolean", "assertNotBoolean" }, + { "assertNotIsNil", "assertNotNil" }, + { "assertNotIsTrue", "assertNotTrue" }, + { "assertNotIsFalse", "assertNotFalse" }, + { "assertNotIsNaN", "assertNotNaN" }, + { "assertNotIsInf", "assertNotInf" }, + { "assertNotIsPlusInf", "assertNotPlusInf" }, + { "assertNotIsMinusInf", "assertNotMinusInf" }, + { "assertNotIsPlusZero", "assertNotPlusZero" }, + { "assertNotIsMinusZero", "assertNotMinusZero" }, + { "assertNotIsFunction", "assertNotFunction" }, + { "assertNotIsThread", "assertNotThread" }, + { "assertNotIsUserdata", "assertNotUserdata" }, + + -- type assertions: assertNotIsXXX -> assert_not_xxx + { "assertNotIsNumber", "assert_not_number" }, + { "assertNotIsString", "assert_not_string" }, + { "assertNotIsTable", "assert_not_table" }, + { "assertNotIsBoolean", "assert_not_boolean" }, + { "assertNotIsNil", "assert_not_nil" }, + { "assertNotIsTrue", "assert_not_true" }, + { "assertNotIsFalse", "assert_not_false" }, + { "assertNotIsNaN", "assert_not_nan" }, + { "assertNotIsInf", "assert_not_inf" }, + { "assertNotIsPlusInf", "assert_not_plus_inf" }, + { "assertNotIsMinusInf", "assert_not_minus_inf" }, + { "assertNotIsPlusZero", "assert_not_plus_zero" }, + { "assertNotIsMinusZero", "assert_not_minus_zero" }, + { "assertNotIsFunction", "assert_not_function" }, + { "assertNotIsThread", "assert_not_thread" }, + { "assertNotIsUserdata", "assert_not_userdata" }, + + -- all assertions with Coroutine duplicate Thread assertions + { "assertIsThread", "assertIsCoroutine" }, + { "assertIsThread", "assertCoroutine" }, + { "assertIsThread", "assert_is_coroutine" }, + { "assertIsThread", "assert_coroutine" }, + { "assertNotIsThread", "assertNotIsCoroutine" }, + { "assertNotIsThread", "assertNotCoroutine" }, + { "assertNotIsThread", "assert_not_is_coroutine" }, + { "assertNotIsThread", "assert_not_coroutine" }, +} + +-- Create all aliases in M +for _, v in ipairs(list_of_funcs) do + local funcname, alias = v[1], v[2] + M[alias] = M[funcname] + + if EXPORT_ASSERT_TO_GLOBALS then + _G[funcname] = M[funcname] + _G[alias] = M[funcname] + end +end + +---------------------------------------------------------------- +-- +-- Outputters +-- +---------------------------------------------------------------- + +-- A common "base" class for outputters +-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html + +local genericOutput = { __class__ = "genericOutput" } -- class +local genericOutput_MT = { __index = genericOutput } -- metatable +M.genericOutput = genericOutput -- publish, so that custom classes may derive from it + +function genericOutput.new(runner, default_verbosity) + -- runner is the "parent" object controlling the output, usually a LuaUnit instance + local t = { runner = runner } + if runner then + t.result = runner.result + t.verbosity = runner.verbosity or default_verbosity + t.fname = runner.fname + else + t.verbosity = default_verbosity + end + return setmetatable(t, genericOutput_MT) +end + +-- abstract ("empty") methods +function genericOutput:startSuite() + -- Called once, when the suite is started +end + +function genericOutput:startClass(className) + -- Called each time a new test class is started +end + +function genericOutput:startTest(testName) + -- called each time a new test is started, right before the setUp() + -- the current test status node is already created and available in: self.result.currentNode +end + +function genericOutput:updateStatus(node) + -- called with status failed or error as soon as the error/failure is encountered + -- this method is NOT called for a successful test because a test is marked as successful by default + -- and does not need to be updated +end + +function genericOutput:endTest(node) + -- called when the test is finished, after the tearDown() method +end + +function genericOutput:endClass() + -- called when executing the class is finished, before moving on to the next class of at the end of the test execution +end + +function genericOutput:endSuite() + -- called at the end of the test suite execution +end + +---------------------------------------------------------------- +-- class TapOutput +---------------------------------------------------------------- + +local TapOutput = genericOutput.new() -- derived class +local TapOutput_MT = { __index = TapOutput } -- metatable +TapOutput.__class__ = "TapOutput" + +-- For a good reference for TAP format, check: http://testanything.org/tap-specification.html + +function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable(t, TapOutput_MT) +end +function TapOutput:startSuite() + print("1.." .. self.result.selectedCount) + print("# Started on " .. self.result.startDate) +end +function TapOutput:startClass(className) + if className ~= "[TestFunctions]" then + print("# Starting class: " .. className) + end +end + +function TapOutput:updateStatus(node) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n") + return + end + + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print(prefixString("# ", node.msg)) + end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print(prefixString("# ", node.stackTrace)) + end +end + +function TapOutput:endTest(node) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + end +end + +function TapOutput:endSuite() + print("# " .. M.LuaUnit.statusLine(self.result)) + return self.result.notSuccessCount +end + +-- class TapOutput end + +---------------------------------------------------------------- +-- class JUnitOutput +---------------------------------------------------------------- + +-- See directory junitxml for more information about the junit format +local JUnitOutput = genericOutput.new() -- derived class +local JUnitOutput_MT = { __index = JUnitOutput } -- metatable +JUnitOutput.__class__ = "JUnitOutput" + +function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable(t, JUnitOutput_MT) +end + +function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error("With Junit, an output filename must be supplied with --name!") + end + if string.sub(self.fname, -4) ~= ".xml" then + self.fname = self.fname .. ".xml" + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: " .. self.fname) + end + + print("# XML output to " .. self.fname) + print("# Started on " .. self.result.startDate) +end +function JUnitOutput:startClass(className) + if className ~= "[TestFunctions]" then + print("# Starting class: " .. className) + end +end +function JUnitOutput:startTest(testName) + print("# Starting test: " .. testName) +end + +function JUnitOutput:updateStatus(node) + if node:isFailure() then + print("# Failure: " .. prefixString("# ", node.msg):sub(4, nil)) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print("# Error: " .. prefixString("# ", node.msg):sub(4, nil)) + -- print('# ' .. node.stackTrace) + end +end + +function JUnitOutput:endSuite() + print("# " .. M.LuaUnit.statusLine(self.result)) + + -- XML file writing + self.fd:write('\n') + self.fd:write("\n") + self.fd:write( + string.format( + ' \n', + self.result.runCount, + self.result.startIsodate, + self.result.duration, + self.result.errorCount, + self.result.failureCount, + self.result.skippedCount + ) + ) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION)) + self.fd:write(string.format(' \n', M.VERSION)) + -- XXX please include system name and version if possible + self.fd:write(" \n") + + for i, node in ipairs(self.result.allTests) do + self.fd:write( + string.format( + ' \n', + node.className, + node.testName, + node.duration + ) + ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) + end + self.fd:write(" \n") + end + + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(" \n") + self.fd:write(" \n") + + self.fd:write(" \n") + self.fd:write("\n") + self.fd:close() + return self.result.notSuccessCount +end + +-- class TapOutput end + +---------------------------------------------------------------- +-- class TextOutput +---------------------------------------------------------------- + +--[[ Example of other unit-tests suite text output + +-- Python Non verbose: + +For each test: . or F or E + +If some failed tests: + ============== + ERROR / FAILURE: TestName (testfile.testclass) + --------- + Stack trace + + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Python Verbose: +testname (filename.classname) ... ok +testname (filename.classname) ... FAIL +testname (filename.classname) ... ERROR + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Ruby: +Started + . + Finished in 0.002695 seconds. + + 1 tests, 2 assertions, 0 failures, 0 errors + +-- Ruby: +>> ruby tc_simple_number2.rb +Loaded suite tc_simple_number2 +Started +F.. +Finished in 0.038617 seconds. + + 1) Failure: +test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: +Adding doesn't work. +<3> expected but was +<4>. + +3 tests, 4 assertions, 1 failures, 0 errors + +-- Java Junit +.......F. +Time: 0,003 +There was 1 failure: +1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError + at junit.samples.VectorTest.testCapacity(VectorTest.java:87) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + +FAILURES!!! +Tests run: 8, Failures: 1, Errors: 0 + + +-- Maven + +# mvn test +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running math.AdditionTest +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: +0.03 sec <<< FAILURE! + +Results : + +Failed tests: + testLireSymbole(math.AdditionTest) + +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 + + +-- LuaUnit +---- non verbose +* display . or F or E when running tests +---- verbose +* display test name + ok/fail +---- +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +* blank line +* number) ERROR or FAILURE: TestName + Stack trace + +then -------------- +then "Ran x tests in 0.000s (%d not selected, %d skipped)" +then OK or FAILED (failures=1, error=1) + + +]] + +local TextOutput = genericOutput.new() -- derived class +local TextOutput_MT = { __index = TextOutput } -- metatable +TextOutput.__class__ = "TextOutput" + +function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable(t, TextOutput_MT) +end + +function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("Started on " .. self.result.startDate) + end +end + +function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write(" ", self.result.currentNode.testName, " ... ") + end +end + +function TextOutput:endTest(node) + if node:isSuccess() then + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write("Ok\n") + else + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print(node.status) + print(node.msg) + --[[ + -- find out when to do this: + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.stackTrace ) + end + ]] + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() + end + end +end + +function TextOutput:displayOneFailedTest(index, fail) + print(index .. ") " .. fail.testName) + print(fail.msg) + print(fail.stackTrace) + print() +end + +function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end +end + +function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end +end + +function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else + print() + end + self:displayErroredTests() + self:displayFailedTests() + print(M.LuaUnit.statusLine(self.result)) + if self.result.notSuccessCount == 0 then + print("OK") + end +end + +-- class TextOutput end + +---------------------------------------------------------------- +-- class NilOutput +---------------------------------------------------------------- + +local function nopCallable() + --print(42) + return nopCallable +end + +local NilOutput = { __class__ = "NilOuptut" } -- class +local NilOutput_MT = { __index = nopCallable } -- metatable + +function NilOutput.new(runner) + return setmetatable({ __class__ = "NilOutput" }, NilOutput_MT) +end + +---------------------------------------------------------------- +-- +-- class LuaUnit +-- +---------------------------------------------------------------- + +M.LuaUnit = { + outputType = TextOutput, + verbosity = M.VERBOSITY_DEFAULT, + __class__ = "LuaUnit", + instances = {}, +} +local LuaUnit_MT = { __index = M.LuaUnit } + +if EXPORT_ASSERT_TO_GLOBALS then + LuaUnit = M.LuaUnit +end + +function M.LuaUnit.new() + local newInstance = setmetatable({}, LuaUnit_MT) + return newInstance +end + +-----------------[[ Utility methods ]]--------------------- + +function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if "function" == type(aObject) then + return aObject + end +end + +function M.LuaUnit.splitClassMethod(someName) + --[[ + Return a pair of className, methodName strings for a name in the form + "class.method". If no class part (or separator) is found, will return + nil, someName instead (the latter being unchanged). + + This convention thus also replaces the older isClassMethod() test: + You just have to check for a non-nil className (return) value. + ]] + local separator = string.find(someName, ".", 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) + end + return nil, someName +end + +function M.LuaUnit.isMethodTestName(s) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == "test" +end + +function M.LuaUnit.isTestName(s) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == "test" +end + +function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName(k) then + table.insert(testNames, k) + end + end + table.sort(testNames) + return testNames +end + +function M.LuaUnit.parseCmdLine(cmdLine) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns + + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + + if cmdLine == nil then + return result + end + + local function parseOption(option) + if option == "--help" or option == "-h" then + result["help"] = true + return + elseif option == "--version" then + result["version"] = true + return + elseif option == "--verbose" or option == "-v" then + result["verbosity"] = M.VERBOSITY_VERBOSE + return + elseif option == "--quiet" or option == "-q" then + result["verbosity"] = M.VERBOSITY_QUIET + return + elseif option == "--error" or option == "-e" then + result["quitOnError"] = true + return + elseif option == "--failure" or option == "-f" then + result["quitOnFailure"] = true + return + elseif option == "--shuffle" or option == "-s" then + result["shuffle"] = true + return + elseif option == "--output" or option == "-o" then + state = SET_OUTPUT + return state + elseif option == "--name" or option == "-n" then + state = SET_FNAME + return state + elseif option == "--repeat" or option == "-r" then + state = SET_REPEAT + return state + elseif option == "--pattern" or option == "-p" then + state = SET_PATTERN + return state + elseif option == "--exclude" or option == "-x" then + state = SET_EXCLUDE + return state + end + error("Unknown option: " .. option, 3) + end + + local function setArg(cmdArg, state) + if state == SET_OUTPUT then + result["output"] = cmdArg + return + elseif state == SET_FNAME then + result["fname"] = cmdArg + return + elseif state == SET_REPEAT then + result["exeRepeat"] = tonumber(cmdArg) or error("Malformed -r argument: " .. cmdArg) + return + elseif state == SET_PATTERN then + if result["pattern"] then + table.insert(result["pattern"], cmdArg) + else + result["pattern"] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = "!" .. cmdArg + if result["pattern"] then + table.insert(result["pattern"], notArg) + else + result["pattern"] = { notArg } + end + return + end + error("Unknown parse state: " .. state) + end + + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg(cmdArg, state, result) + state = nil + else + if cmdArg:sub(1, 1) == "-" then + state = parseOption(cmdArg) + else + if result["testNames"] then + table.insert(result["testNames"], cmdArg) + else + result["testNames"] = { cmdArg } + end + end + end + end + + if result["help"] then + M.LuaUnit.help() + end + + if result["version"] then + M.LuaUnit.version() + end + + if state ~= nil then + error("Missing argument after " .. cmdLine[#cmdLine], 2) + end + + return result +end + +function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) +end + +function M.LuaUnit.version() + print("LuaUnit v" .. M.VERSION .. " by Philippe Fremy ") + os.exit(0) +end + +---------------------------------------------------------------- +-- class NodeStatus +---------------------------------------------------------------- + +local NodeStatus = { __class__ = "NodeStatus" } -- class +local NodeStatus_MT = { __index = NodeStatus } -- metatable +M.NodeStatus = NodeStatus + +-- values of status +NodeStatus.SUCCESS = "SUCCESS" +NodeStatus.SKIP = "SKIP" +NodeStatus.FAIL = "FAIL" +NodeStatus.ERROR = "ERROR" + +function NodeStatus.new(number, testName, className) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable(t, NodeStatus_MT) + t:success() + return t +end + +function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil +end + +function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil +end + +function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace +end + +function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace +end + +function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS +end + +function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) +end + +function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP +end + +function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL +end + +function NodeStatus:isError() + return self.status == NodeStatus.ERROR +end + +function NodeStatus:statusXML() + if self:isError() then + return table.concat({ + ' \n', + " \n", + }) + elseif self:isFailure() then + return table.concat({ + ' \n', + " \n", + }) + elseif self:isSkipped() then + return table.concat({ + " ", + xmlEscape(self.msg), + "\n", + }) + end + return " \n" -- (not XSD-compliant! normally shouldn't get here) +end + +--------------[[ Output methods ]]------------------------- + +local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = "" + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == "ss") and "es" or "s" + end + return string.format("%d %s%s", number, singular, suffix) +end + +function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format("Ran %d tests in %0.3f seconds", result.runCount, result.duration), + conditional_plural(result.successCount, "success"), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, "failure")) + end + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, "error")) + end + else + table.insert(s, "0 failures") + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ", ") +end + +function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv("LUAUNIT_DATEFMT")), + startIsodate = os.date("%Y-%m-%dT%H:%M:%S"), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() +end + +function M.LuaUnit:startClass(className, classInstance) + self.result.currentClassName = className + self.output:startClass(className) + self:setupClass(className, classInstance) +end + +function M.LuaUnit:startTest(testName) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new(self.result.currentTestNumber, testName, self.result.currentClassName) + self.result.currentNode.startTime = os.clock() + table.insert(self.result.allTests, self.result.currentNode) + self.output:startTest(testName) +end + +function M.LuaUnit:updateStatus(err) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return + end + + local node = self.result.currentNode + + --[[ As a first approach, we will report only one error or one failure for one test. + + However, we can have the case where the test is in failure, and the teardown is in error. + In such case, it's a good idea to report both a failure and an error in the test suite. This is + what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for + example, there could be more (failures + errors) count that tests. What happens to the current node ? + + We will do this more intelligent version later. + ]] + + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return + end + + if err.status == NodeStatus.FAIL then + node:fail(err.msg, err.trace) + table.insert(self.result.failedTests, node) + elseif err.status == NodeStatus.ERROR then + node:error(err.msg, err.trace) + table.insert(self.result.errorTests, node) + elseif err.status == NodeStatus.SKIP then + node:skip(err.msg) + table.insert(self.result.skippedTests, node) + else + error("No such status: " .. prettystr(err.status)) + end + + self.output:updateStatus(node) +end + +function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest(node) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error("No such node status: " .. prettystr(node.status)) + end + self.result.currentNode = nil +end + +function M.LuaUnit:endClass() + self:teardownClass(self.lastClassName, self.lastClassInstance) + self.output:endClass() +end + +function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error("LuaUnit:endSuite() -- suite was already ended") + end + self.result.duration = os.clock() - self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() +end + +function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error("No such format: " .. outputType, 2) +end + +--------------[[ Runner ]]----------------- + +function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2), + } + end + + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall(function() + methodInstance(classInstance) + end, err_handler) + else + ok, err = xpcall(function() + methodInstance() + end, err_handler) + end + if ok then + return { status = NodeStatus.SUCCESS } + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') + + local iter_msg + iter_msg = self.exeRepeat and "iteration " .. self.currentCount + + err.msg, err.status = M.adjust_err_msg_with_iter(err.msg, iter_msg) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err + end + + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '" .. prettyFuncName .. "'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) + end + + return err -- return the error "object" (table) +end + +function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set + + if type(methodInstance) ~= "function" then + self:unregisterSuite() + error(tostring(methodName) .. " must be a function, not " .. type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = "[TestFunctions]" + prettyFuncName = methodName + else + prettyFuncName = className .. "." .. methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() + end + self:startClass(className, classInstance) + self.lastClassName = className + self.lastClassInstance = classInstance + end + + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) + if classInstance then + local func = self.asFunction(classInstance.setUp) + or self.asFunction(classInstance.Setup) + or self.asFunction(classInstance.setup) + or self.asFunction(classInstance.SetUp) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className .. ".setUp")) + end + end + + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) + end + + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction(classInstance.tearDown) + or self.asFunction(classInstance.TearDown) + or self.asFunction(classInstance.teardown) + or self.asFunction(classInstance.Teardown) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className .. ".tearDown")) + end + end + end + + self:endTest() +end + +function M.LuaUnit.expandOneClass(result, className, classInstance) + --[[ + Input: a list of { name, instance }, a class name, a class instance + Ouptut: modify result to add all test method instance in the form: + { className.methodName, classInstance } + ]] + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName(methodName) then + table.insert(result, { className .. "." .. methodName, classInstance }) + end + end +end + +function M.LuaUnit.expandClasses(listOfNameAndInst) + --[[ + -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} + -- functions and methods remain untouched + + Input: a list of { name, instance } + + Output: + * { function name, function instance } : do nothing + * { class.method name, class instance }: do nothing + * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) + ]] + local result = {} + + for i, v in ipairs(listOfNameAndInst) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert(result, { name, instance }) + else + if type(instance) ~= "table" then + error( + "Instance must be a table or a function, not a " .. type(instance) .. " with value " .. prettystr(instance) + ) + end + local className, methodName = M.LuaUnit.splitClassMethod(name) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error("Could not find method in class " .. tostring(className) .. " for method " .. tostring(methodName)) + end + table.insert(result, { name, instance }) + else + M.LuaUnit.expandOneClass(result, name, instance) + end + end + end + + return result +end + +function M.LuaUnit.applyPatternFilter(patternIncFilter, listOfNameAndInst) + local included, excluded = {}, {} + for i, v in ipairs(listOfNameAndInst) do + -- local name, instance = v[1], v[2] + if patternFilter(patternIncFilter, v[1]) then + table.insert(included, v) + else + table.insert(excluded, v) + end + end + return included, excluded +end + +local function getKeyInListWithGlobalFallback(key, listOfNameAndInst) + local result = nil + for i, v in ipairs(listOfNameAndInst) do + if listOfNameAndInst[i][1] == key then + result = listOfNameAndInst[i][2] + break + end + end + if not M.LuaUnit.asFunction(result) then + result = _G[key] + end + return result +end + +function M.LuaUnit:setupSuite(listOfNameAndInst) + local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst) + if self.asFunction(setupSuite) then + self:updateStatus(self:protectedCall(nil, setupSuite, "setupSuite")) + end +end + +function M.LuaUnit:teardownSuite(listOfNameAndInst) + local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst) + if self.asFunction(teardownSuite) then + self:updateStatus(self:protectedCall(nil, teardownSuite, "teardownSuite")) + end +end + +function M.LuaUnit:setupClass(className, instance) + if type(instance) == "table" and self.asFunction(instance.setupClass) then + self:updateStatus(self:protectedCall(instance, instance.setupClass, className .. ".setupClass")) + end +end + +function M.LuaUnit:teardownClass(className, instance) + if type(instance) == "table" and self.asFunction(instance.teardownClass) then + self:updateStatus(self:protectedCall(instance, instance.teardownClass, className .. ".teardownClass")) + end +end + +function M.LuaUnit:internalRunSuiteByInstances(listOfNameAndInst) + --[[ Run an explicit list of tests. Each item of the list must be one of: + * { function name, function instance } + * { class name, class instance } + * { class.method name, class instance } + + This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances() + ]] + + local expandedList = self.expandClasses(listOfNameAndInst) + if self.shuffle then + randomizeTable(expandedList) + end + local filteredList, filteredOutList = self.applyPatternFilter(self.patternIncludeFilter, expandedList) + + self:startSuite(#filteredList, #filteredOutList) + self:setupSuite(listOfNameAndInst) + + for i, v in ipairs(filteredList) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction(nil, name, nil, instance) + else + -- expandClasses() should have already taken care of sanitizing the input + assert(type(instance) == "table") + local className, methodName = M.LuaUnit.splitClassMethod(name) + assert(className ~= nil) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction(className, methodName, instance, methodInstance) + end + if self.result.aborted then + break -- "--error" or "--failure" option triggered + end + end + + if self.lastClassName ~= nil then + self:endClass() + end + + self:teardownSuite(listOfNameAndInst) + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + self:unregisterSuite() + os.exit(-2) + end +end + +function M.LuaUnit:internalRunSuiteByNames(listOfName) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + namespace analysis. Convert the list into a list of (name, valid instances (table or function)) + and calls internalRunSuiteByInstances. + ]] + + local instanceName, instance + local listOfNameAndInst = {} + + for i, name in ipairs(listOfName) do + local className, methodName = M.LuaUnit.splitClassMethod(name) + if className then + instanceName = className + instance = _G[instanceName] + + if instance == nil then + self:unregisterSuite() + error("No such name in global space: " .. instanceName) + end + + if type(instance) ~= "table" then + self:unregisterSuite() + error("Instance of " .. instanceName .. " must be a table, not " .. type(instance)) + end + + local methodInstance = instance[methodName] + if methodInstance == nil then + self:unregisterSuite() + error("Could not find method in class " .. tostring(className) .. " for method " .. tostring(methodName)) + end + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] + end + + if instance == nil then + self:unregisterSuite() + error("No such name in global space: " .. instanceName) + end + + if type(instance) ~= "table" and type(instance) ~= "function" then + self:unregisterSuite() + error("Name must match a function or a table: " .. instanceName) + end + + table.insert(listOfNameAndInst, { name, instance }) + end + + self:internalRunSuiteByInstances(listOfNameAndInst) +end + +function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + local runner = M.LuaUnit.new() + return runner:runSuite(...) +end + +function M.LuaUnit:registerSuite() + -- register the current instance into our global array of instances + -- print('-> Register suite') + M.LuaUnit.instances[#M.LuaUnit.instances + 1] = self +end + +function M.unregisterCurrentSuite() + -- force unregister the last registered suite + table.remove(M.LuaUnit.instances, #M.LuaUnit.instances) +end + +function M.LuaUnit:unregisterSuite() + -- print('<- Unregister suite') + -- remove our current instqances from the global array of instances + local instanceIdx = nil + for i, instance in ipairs(M.LuaUnit.instances) do + if instance == self then + instanceIdx = i + break + end + end + + if instanceIdx ~= nil then + table.remove(M.LuaUnit.instances, instanceIdx) + -- print('Unregister done') + end +end + +function M.LuaUnit:initFromArguments(...) + --[[Parses all arguments from either command-line or direct call and set internal + flags of LuaUnit runner according to it. + + Return the list of names which were possibly passed on the command-line or as arguments + ]] + local args = { ... } + if type(args[1]) == "table" and args[1].__class__ == "LuaUnit" then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument self to make it a command-line argument list + table.remove(args, 1) + end + + if #args == 0 then + args = cmdline_argv + end + + local options = pcall_or_abort(M.LuaUnit.parseCmdLine, args) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + options.output = options.output or os.getenv("LUAUNIT_OUTPUT") + options.fname = options.fname or os.getenv("LUAUNIT_JUNIT_FNAME") + + if options.output then + if options.output:lower() == "junit" and options.fname == nil then + print("With junit output, a filename must be supplied with -n or --name") + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + return options.testNames +end + +function M.LuaUnit:runSuite(...) + local testNames = self:initFromArguments(...) + self:registerSuite() + self:internalRunSuiteByNames(testNames or M.LuaUnit.collectTests()) + self:unregisterSuite() + return self.result.notSuccessCount +end + +function M.LuaUnit:runSuiteByInstances(listOfNameAndInst, commandLineArguments) + --[[ + Run all test functions or tables provided as input. + + Input: a list of { name, instance } + instance can either be a function or a table containing test functions starting with the prefix "test" + + return the number of failures and errors, 0 meaning success + ]] + -- parse the command-line arguments + local testNames = self:initFromArguments(commandLineArguments) + self:registerSuite() + self:internalRunSuiteByInstances(listOfNameAndInst) + self:unregisterSuite() + return self.result.notSuccessCount +end + +-- class LuaUnit + +-- For compatbility with LuaUnit v2 +M.run = M.LuaUnit.run +M.Run = M.LuaUnit.run + +function M:setVerbosity(verbosity) + -- set the verbosity value (as integer) + M.LuaUnit.verbosity = verbosity +end +M.set_verbosity = M.setVerbosity +M.SetVerbosity = M.setVerbosity + +return M