diff --git a/package-lock.json b/package-lock.json index 367aa8c..a4d4a09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18232,7 +18232,7 @@ }, "packages/lest": { "name": "@taservers/lest", - "version": "3.0.0", + "version": "3.1.0", "license": "MIT", "dependencies": { "lookpath": "^1.2.2", diff --git a/packages/lest/CHANGELOG.md b/packages/lest/CHANGELOG.md index d6de439..6b3a94c 100644 --- a/packages/lest/CHANGELOG.md +++ b/packages/lest/CHANGELOG.md @@ -14,6 +14,20 @@ The valid change types are: - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## [3.1.0] - [#113](https://github.com/TAServers/lest/pull/113) + +### Added + +- Added pretty diff rendering to `toEqual` matcher to show differences between deeply nested values + +### Changed + +- `toBe` matcher now generates its message more in line with Jest to make comparing values easier +- All values displayed in test failure messages are now properly serialised to mostly valid Lua, including the contents of tables +- Test failure messages no longer include the expected and received values in the `expect(...).matcherName(...)` signature + - This matches the behaviour of Jest and avoids duplicating information between the signature and failure message +- Test failure messages are no longer highlighted in red to give more control to matchers over how individual messages are formatted + ## [3.0.0] - [#102](https://github.com/TAServers/lest/issues/102) ### Added diff --git a/packages/lest/lest.config.lua b/packages/lest/lest.config.lua index 600619c..78cbf04 100644 --- a/packages/lest/lest.config.lua +++ b/packages/lest/lest.config.lua @@ -1,3 +1,5 @@ +package.path = "./src/lua/?.lua;./src/lua/?/index.lua;./?.lua" + return { testMatch = { "tests/lua/.+%.test%.lua", "src/lua/.+%.test%.lua" }, } diff --git a/packages/lest/package.json b/packages/lest/package.json index 884af2f..654d6ab 100644 --- a/packages/lest/package.json +++ b/packages/lest/package.json @@ -1,6 +1,6 @@ { "name": "@taservers/lest", - "version": "3.0.0", + "version": "3.1.0", "license": "MIT", "description": "Painless Lua testing.", "homepage": "https://taservers.github.io/lest/", @@ -16,10 +16,10 @@ "lest": "./dist/bin/lest.js" }, "scripts": { - "build:lua": "luabundler bundle src/lua/lest.lua -p \"./?.lua\" -o dist/lua/lest.lua", + "build:lua": "luabundler bundle src/lua/lest.lua -p \"./src/lua/?.lua\" -p \"./src/lua/?/index.lua\" -p \"./?.lua\" -o dist/lua/lest.lua", "build:ts": "tsc -p tsconfig.build.json", "build": "npm run build:lua && npm run build:ts", - "test:lua": "lua src/lua/lest.lua", + "test:lua": "npm run build:lua && lua dist/lua/lest.lua", "test:ts": "jest", "test": "npm run test:lua && npm run test:ts", "clean": "rimraf dist", diff --git a/packages/lest/src/lua/asserts/matchers.lua b/packages/lest/src/lua/asserts/matchers.lua index 11cb90c..b42dea0 100644 --- a/packages/lest/src/lua/asserts/matchers.lua +++ b/packages/lest/src/lua/asserts/matchers.lua @@ -1,21 +1,24 @@ --- Asserts the matcher passed. ---@param result lest.MatcherResult local function assertPass(result) - assert(result.pass, debug.traceback("Expected matcher to pass", 2)) + if not result.pass then + error("Expected matcher to pass", 2) + end end --- Asserts the matcher failed. ---@param result lest.MatcherResult local function assertFail(result) - assert(not result.pass, debug.traceback("Expected matcher to fail", 2)) + if result.pass then + error("Expected matcher to fail", 2) + end end --- Asserts the matcher returned the given message. ---@param result lest.MatcherResult local function assertMessage(result, message) - assert( - result.message == message, - debug.traceback( + if result.message ~= message then + error( string.format( "Expected message: %s\nReceived message: %s", message, @@ -23,7 +26,7 @@ local function assertMessage(result, message) ), 2 ) - ) + end end return { diff --git a/packages/lest/src/lua/asserts/type.lua b/packages/lest/src/lua/asserts/type.lua index 64179e6..8936c7b 100644 --- a/packages/lest/src/lua/asserts/type.lua +++ b/packages/lest/src/lua/asserts/type.lua @@ -1,5 +1,5 @@ -local TypeError = require("src.lua.errors.type") -local prettyValue = require("src.lua.utils.prettyValue") +local TypeError = require("errors.type") +local serialiseValue = require("utils.serialise-value") --- Asserts the object is of the specified type --- @@ -9,7 +9,7 @@ local prettyValue = require("src.lua.utils.prettyValue") ---@param label? string ---@param level? number return function(object, typeStringOrMeta, label, level) - label = label or prettyValue(object) + label = label or serialiseValue(object) level = (level or 1) + 1 if @@ -34,7 +34,7 @@ return function(object, typeStringOrMeta, label, level) string.format( "Expected %s to be an instance of %s", label, - prettyValue(typeStringOrMeta) + serialiseValue(typeStringOrMeta) ) ), level diff --git a/packages/lest/src/lua/errors/testresult.lua b/packages/lest/src/lua/errors/testresult.lua index 92274c5..6e54846 100644 --- a/packages/lest/src/lua/errors/testresult.lua +++ b/packages/lest/src/lua/errors/testresult.lua @@ -1,5 +1,4 @@ -local COLOURS = require("src.lua.utils.consoleColours") -local registerError = require("src.lua.errors.register") +local registerError = require("errors.register") return registerError("TestResultError", function(message, signature) return { message = message, @@ -7,10 +6,6 @@ return registerError("TestResultError", function(message, signature) } end, { __tostring = function(self) - return string.format( - "%s\n\n%s", - self.signature, - COLOURS.FAIL(self.message) - ) + return string.format("%s\n\n%s", self.signature, self.message) end, }) diff --git a/packages/lest/src/lua/expect.lua b/packages/lest/src/lua/expect.lua index 294aeb6..c2f53a4 100644 --- a/packages/lest/src/lua/expect.lua +++ b/packages/lest/src/lua/expect.lua @@ -1,30 +1,22 @@ -local COLOURS = require("src.lua.utils.consoleColours") +local COLOURS = require("utils.consoleColours") -local matchers = require("src.lua.matchers") -local prettyValue = require("src.lua.utils.prettyValue") -local TestResultError = require("src.lua.errors.testresult") +local matchers = require("matchers") +local TestResultError = require("errors.testresult") --- Builds a signature for the expect call ---@param name string -- Name of the matcher that was used ----@param args any -- Arguments passed to the matcher ----@param received any ---@param inverted boolean -- True if the condition was inverted ---@return string -local function buildSignature(name, args, received, inverted) - local stringArgs = {} - for i, arg in ipairs(args) do - stringArgs[i] = prettyValue(arg) - end - +local function buildSignature(name, inverted) return string.format( "%s%s%s%s.%s%s%s%s", COLOURS.DIMMED("expect("), - COLOURS.RECEIVED(prettyValue(received)), + COLOURS.RECEIVED("received"), COLOURS.DIMMED(")"), inverted and ".never" or "", COLOURS.HIGHLIGHT(name), COLOURS.DIMMED("("), - COLOURS.EXPECTED(table.concat(stringArgs, ", ")), + COLOURS.EXPECTED("expected"), COLOURS.DIMMED(")") ) end @@ -51,7 +43,7 @@ local function bindMatcher(name, matcher, received, inverted) error( TestResultError( tostring(result.message), - buildSignature(name, { ... }, received, inverted) + buildSignature(name, inverted) ) ) end diff --git a/packages/lest/src/lua/matchers/equality.lua b/packages/lest/src/lua/matchers/equality.lua index cadbd22..4e74a83 100644 --- a/packages/lest/src/lua/matchers/equality.lua +++ b/packages/lest/src/lua/matchers/equality.lua @@ -1,16 +1,12 @@ -local prettyValue = require("src.lua.utils.prettyValue") -local deepEqual = require("src.lua.utils.deepEqual") +local serialiseValue = require("utils.serialise-value") +local deepEqual = require("utils.deepEqual") +local renderDiff = require("utils.render-diff") ---@type lest.Matcher local function toBe(ctx, received, expected) return { pass = received == expected, - message = string.format( - "Expected %s to%sbe %s", - prettyValue(received), - ctx.inverted and " not " or " ", - prettyValue(expected) - ), + message = renderDiff(expected, received, false, ctx.inverted), } end @@ -20,7 +16,7 @@ local function toBeDefined(ctx, received) pass = received ~= nil, message = string.format( "Expected %s to be %sdefined", - prettyValue(received), + serialiseValue(received), ctx.inverted and "un" or "" ), } @@ -32,7 +28,7 @@ local function toBeUndefined(ctx, received) pass = received == nil, message = string.format( "Expected %s to be %sdefined", - prettyValue(received), + serialiseValue(received), ctx.inverted and "" or "un" ), } @@ -41,12 +37,7 @@ end local function toEqual(ctx, received, expected) return { pass = deepEqual(received, expected), - message = string.format( - "Expected %s to%sdeeply equal %s", - prettyValue(received), - ctx.inverted and " not " or " ", - prettyValue(expected) - ), + message = renderDiff(expected, received, true, ctx.inverted), } end @@ -56,7 +47,7 @@ local function toBeTruthy(ctx, received) pass = not not received, message = string.format( "Expected %s to%sbe truthy", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " " ), } @@ -68,7 +59,7 @@ local function toBeFalsy(ctx, received) pass = not received, message = string.format( "Expected %s to%sbe falsy", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " " ), } @@ -82,9 +73,9 @@ local function toBeInstanceOf(ctx, received, metatable) pass = getmetatable(received) == metatable, message = string.format( "Expected %s to%sbe an instance of %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(metatable) + serialiseValue(metatable) ), } end diff --git a/packages/lest/src/lua/matchers/equality.test.lua b/packages/lest/src/lua/matchers/equality.test.lua index cc28aed..5cb7c86 100644 --- a/packages/lest/src/lua/matchers/equality.test.lua +++ b/packages/lest/src/lua/matchers/equality.test.lua @@ -1,18 +1,7 @@ -local equality = require("src.lua.matchers.equality") -local prettyValue = require("src.lua.utils.prettyValue") - ---- Asserts that a matcher passed ----@param result lest.MatcherResult Result of the matcher -local function assertPass(result) - assert(result.pass, "test failed when it should have passed!") -end - ---- Asserts that a matcher failed ----@param result lest.MatcherResult Result of the matcher ----@param expectedMsg string Expected message of the matcher -local function assertFail(result, expectedMsg) - assert(result.message == expectedMsg, "test has an incorrect fail message!") -end +local equality = require("matchers.equality") +local serialiseValue = require("utils.serialise-value") +local assertMatcher = require("asserts.matchers") +local renderDiff = require("utils.render-diff") local CONTEXT = { inverted = false, @@ -27,30 +16,56 @@ describe("equality matchers", function() it("should pass when the arguments are equal", function() local result = equality.toBe(CONTEXT, 2, 2) - assertPass(result) + assertMatcher.passed(result) end) it("should fail when the arguments aren't equal", function() - local result = equality.toBe(CONTEXT, 2, 42) + local expected = { 1, 2, 3 } + local received = { 1, 2, 3 } - assertFail(result, "Expected 2 to be 42") + local result = equality.toBe(CONTEXT, received, expected) + + assertMatcher.failed(result) + assertMatcher.hasMessage( + result, + renderDiff(expected, received, false, false) + ) end) it( "should pass when inverted and the arguments aren't equal", function() - local result = equality.toBe(INVERTED_CONTEXT, 2, 42) + local expected = { 1, 2, 3 } + local received = { 1, 2, 3 } + + local result = + equality.toBe(INVERTED_CONTEXT, received, expected) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end ) it("should fail when inverted and the arguments are equal", function() - local result = equality.toBe(INVERTED_CONTEXT, 2, 2) + local expectedAndReceived = 2 + + local result = equality.toBe( + INVERTED_CONTEXT, + expectedAndReceived, + expectedAndReceived + ) result.pass = not result.pass - assertFail(result, "Expected 2 to not be 2") + assertMatcher.failed(result) + assertMatcher.hasMessage( + result, + renderDiff( + expectedAndReceived, + expectedAndReceived, + false, + true + ) + ) end) end) @@ -58,27 +73,29 @@ describe("equality matchers", function() it("should pass when defined", function() local result = equality.toBeDefined(CONTEXT, 10) - assertPass(result) + assertMatcher.passed(result) end) it("should fail when undefined", function() local result = equality.toBeDefined(CONTEXT, nil) - assertFail(result, "Expected nil to be defined") + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected nil to be defined") end) it("should pass when inverted and undefined", function() local result = equality.toBeDefined(INVERTED_CONTEXT, nil) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end) it("should fail when inverted and defined", function() local result = equality.toBeDefined(INVERTED_CONTEXT, 10) result.pass = not result.pass - assertFail(result, "Expected 10 to be undefined") + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected 10 to be undefined") end) end) @@ -86,27 +103,29 @@ describe("equality matchers", function() it("should pass when undefined", function() local result = equality.toBeUndefined(CONTEXT, nil) - assertPass(result) + assertMatcher.passed(result) end) it("should fail when defined", function() local result = equality.toBeUndefined(CONTEXT, 10) - assertFail(result, "Expected 10 to be undefined") + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected 10 to be undefined") end) it("should pass when inverted and defined", function() local result = equality.toBeUndefined(INVERTED_CONTEXT, 10) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end) it("should fail when inverted and undefined", function() local result = equality.toBeUndefined(INVERTED_CONTEXT, nil) result.pass = not result.pass - assertFail(result, "Expected nil to be defined") + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected nil to be defined") end) end) @@ -114,13 +133,20 @@ describe("equality matchers", function() it("should pass on equality", function() local result = equality.toEqual(CONTEXT, 10, 10) - assertPass(result) + assertMatcher.passed(result) end) it("should fail on inequality", function() - local result = equality.toEqual(CONTEXT, 10, 15) + local expected = 10 + local received = 15 + + local result = equality.toEqual(CONTEXT, received, expected) - assertFail(result, "Expected 10 to deeply equal 15") + assertMatcher.failed(result) + assertMatcher.hasMessage( + result, + renderDiff(expected, received, true) + ) end) it("should pass on deep equality", function() @@ -146,39 +172,37 @@ describe("equality matchers", function() local result = equality.toEqual(CONTEXT, tableOne, tableTwo) - assertPass(result) + assertMatcher.passed(result) end) it("should fail on deep inequality", function() - local tableOne = { + local received = { hi = 10, ["turing"] = "alan", { 12, 14, 18, + "I'm not supposed to be here", }, } - local tableTwo = { + local expected = { hi = 10, ["turing"] = "alan", { 12, 14, 18, - "I'm not supposed to be here", }, } - local result = equality.toEqual(CONTEXT, tableOne, tableTwo) + local result = equality.toEqual(CONTEXT, received, expected) - assertFail( + assertMatcher.failed(result) + assertMatcher.hasMessage( result, - ("Expected %s to deeply equal %s"):format( - prettyValue(tableOne), - prettyValue(tableTwo) - ) + renderDiff(expected, received, true) ) end) @@ -186,13 +210,24 @@ describe("equality matchers", function() local result = equality.toEqual(INVERTED_CONTEXT, 5, 10) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end) it("should fail when inverted on equality", function() - local result = equality.toEqual(INVERTED_CONTEXT, 10, 10) + local expectedAndReceived = 10 - assertFail(result, "Expected 10 to not deeply equal 10") + local result = equality.toEqual( + INVERTED_CONTEXT, + expectedAndReceived, + expectedAndReceived + ) + result.pass = not result.pass + + assertMatcher.failed(result) + assertMatcher.hasMessage( + result, + renderDiff(expectedAndReceived, expectedAndReceived, true, true) + ) end) it("should pass when inverted on deep inequality", function() @@ -221,11 +256,11 @@ describe("equality matchers", function() equality.toEqual(INVERTED_CONTEXT, tableOne, tableTwo) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end) it("should fail when inverted on deep equality", function() - local tableOne = { + local expectedAndReceived = { hi = 10, ["turing"] = "alan", { @@ -235,95 +270,89 @@ describe("equality matchers", function() }, } - local tableTwo = { - hi = 10, - ["turing"] = "alan", - { - 12, - 14, - 18, - }, - } - - local result = - equality.toEqual(INVERTED_CONTEXT, tableOne, tableTwo) + local result = equality.toEqual( + INVERTED_CONTEXT, + expectedAndReceived, + expectedAndReceived + ) + result.pass = not result.pass - assertFail( + assertMatcher.failed(result) + assertMatcher.hasMessage( result, - ("Expected %s to not deeply equal %s"):format( - prettyValue(tableOne), - prettyValue(tableTwo) - ) + renderDiff(expectedAndReceived, expectedAndReceived, true, true) ) end) end) describe("toBeTruthy", function() it("should pass on truthy values", function() - assertPass(equality.toBeTruthy(CONTEXT, true)) + assertMatcher.passed(equality.toBeTruthy(CONTEXT, true)) end) it("should fail on falsy values", function() - assertFail( - equality.toBeTruthy(CONTEXT, false), - "Expected false to be truthy" - ) - assertFail( - equality.toBeTruthy(CONTEXT, nil), - "Expected nil to be truthy" - ) + local resultFalse = equality.toBeTruthy(CONTEXT, false) + local resultNil = equality.toBeTruthy(CONTEXT, nil) + + assertMatcher.failed(resultFalse) + assertMatcher.hasMessage(resultFalse, "Expected false to be truthy") + assertMatcher.failed(resultNil) + assertMatcher.hasMessage(resultNil, "Expected nil to be truthy") end) it("should fail when inverted on truthy values", function() local result = equality.toBeTruthy(INVERTED_CONTEXT, true) result.pass = not result.pass - assertFail(result, "Expected true to not be truthy") + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected true to not be truthy") end) it("should pass when inverted on falsy values", function() local result = equality.toBeTruthy(INVERTED_CONTEXT, false) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) result = equality.toBeTruthy(INVERTED_CONTEXT, nil) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end) end) describe("toBeFalsy", function() it("should fail on truthy values", function() - assertFail( - equality.toBeFalsy(CONTEXT, true), - "Expected true to be falsy" - ) + local result = equality.toBeFalsy(CONTEXT, true) + + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected true to be falsy") end) it("should pass on falsy values", function() - assertPass(equality.toBeFalsy(CONTEXT, false)) - assertPass(equality.toBeFalsy(CONTEXT, nil)) + assertMatcher.passed(equality.toBeFalsy(CONTEXT, false)) + assertMatcher.passed(equality.toBeFalsy(CONTEXT, nil)) end) it("should pass when inverted on truthy values", function() local result = equality.toBeFalsy(INVERTED_CONTEXT, true) result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end) it("should fail when inverted on falsy values", function() local result = equality.toBeFalsy(INVERTED_CONTEXT, false) result.pass = not result.pass - assertFail(result, "Expected false to not be falsy") + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected false to not be falsy") result = equality.toBeFalsy(INVERTED_CONTEXT, nil) result.pass = not result.pass - assertFail(result, "Expected nil to not be falsy") + assertMatcher.failed(result) + assertMatcher.hasMessage(result, "Expected nil to not be falsy") end) end) @@ -332,16 +361,23 @@ describe("equality matchers", function() local instance = setmetatable({}, TestClass) it("should pass on instances of a class", function() - assertPass(equality.toBeInstanceOf(CONTEXT, instance, TestClass)) + assertMatcher.passed( + equality.toBeInstanceOf(CONTEXT, instance, TestClass) + ) end) it("should fail on non-instances of a class", function() local nonInstance = {} - assertFail( - equality.toBeInstanceOf(CONTEXT, nonInstance, TestClass), + + local result = + equality.toBeInstanceOf(CONTEXT, nonInstance, TestClass) + + assertMatcher.failed(result) + assertMatcher.hasMessage( + result, ("Expected %s to be an instance of %s"):format( - prettyValue(nonInstance), - prettyValue(TestClass) + serialiseValue(nonInstance), + serialiseValue(TestClass) ) ) end) @@ -350,11 +386,13 @@ describe("equality matchers", function() local result = equality.toBeInstanceOf(INVERTED_CONTEXT, instance, TestClass) result.pass = not result.pass - assertFail( + + assertMatcher.failed(result) + assertMatcher.hasMessage( result, ("Expected %s to not be an instance of %s"):format( - prettyValue(instance), - prettyValue(TestClass) + serialiseValue(instance), + serialiseValue(TestClass) ) ) end) @@ -369,7 +407,7 @@ describe("equality matchers", function() result.pass = not result.pass - assertPass(result) + assertMatcher.passed(result) end) end) end) diff --git a/packages/lest/src/lua/matchers/mocks.lua b/packages/lest/src/lua/matchers/mocks.lua index a097875..59c79fa 100644 --- a/packages/lest/src/lua/matchers/mocks.lua +++ b/packages/lest/src/lua/matchers/mocks.lua @@ -1,11 +1,11 @@ -local prettyValue = require("src.lua.utils.prettyValue") -local deepEqual = require("src.lua.utils.deepEqual") -local assertType = require("src.lua.asserts.type") +local serialiseValue = require("utils.serialise-value") +local deepEqual = require("utils.deepEqual") +local assertType = require("asserts.type") local function prettyArgs(args) local argStrings = {} for i, arg in ipairs(args) do - argStrings[i] = prettyValue(arg) + argStrings[i] = serialiseValue(arg) end return table.concat(argStrings, ", ") @@ -16,7 +16,10 @@ end ---@return lest.Mock local function assertMockFn(value) if not lest.isMockFunction(value) then - error(string.format("%s is not a mock function", prettyValue(value)), 3) + error( + string.format("%s is not a mock function", serialiseValue(value)), + 3 + ) end return value diff --git a/packages/lest/src/lua/matchers/numbers.lua b/packages/lest/src/lua/matchers/numbers.lua index d1cd0e9..a8a4443 100644 --- a/packages/lest/src/lua/matchers/numbers.lua +++ b/packages/lest/src/lua/matchers/numbers.lua @@ -1,5 +1,5 @@ -local prettyValue = require("src.lua.utils.prettyValue") -local assertType = require("src.lua.asserts.type") +local serialiseValue = require("utils.serialise-value") +local assertType = require("asserts.type") ---@type lest.Matcher local function toBeCloseTo(ctx, received, expected, numDigits) @@ -14,9 +14,9 @@ local function toBeCloseTo(ctx, received, expected, numDigits) pass = passed, message = string.format( "Expected %s to%sbe close to %s (%d decimal places)", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(expected), + serialiseValue(expected), numDigits ), } @@ -47,9 +47,9 @@ local function toBeGreaterThan(ctx, received, expected) pass = success and comparison, message = string.format( "Expected %s to%sbe greater than %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(expected) + serialiseValue(expected) ), } end @@ -66,9 +66,9 @@ local function toBeGreaterThanOrEqual(ctx, received, expected) pass = success and comparison, message = string.format( "Expected %s to%sbe greater than or equal to %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(expected) + serialiseValue(expected) ), } end @@ -85,9 +85,9 @@ local function toBeLessThan(ctx, received, expected) pass = success and comparison, message = string.format( "Expected %s to%sbe less than %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(expected) + serialiseValue(expected) ), } end @@ -104,9 +104,9 @@ local function toBeLessThanOrEqual(ctx, received, expected) pass = success and comparison, message = string.format( "Expected %s to%sbe less than or equal to %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(expected) + serialiseValue(expected) ), } end @@ -117,7 +117,7 @@ local function toBeNaN(ctx, received) pass = type(received) == "number" and received ~= received, message = string.format( "Expected %s to%sbe NaN", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " " ), } @@ -129,7 +129,7 @@ local function toBeInfinity(ctx, received) pass = type(received) == "number" and math.abs(received) == math.huge, message = string.format( "Expected %s to%sbe infinity", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " " ), } diff --git a/packages/lest/src/lua/matchers/strings.lua b/packages/lest/src/lua/matchers/strings.lua index bd22c55..bac621b 100644 --- a/packages/lest/src/lua/matchers/strings.lua +++ b/packages/lest/src/lua/matchers/strings.lua @@ -1,5 +1,5 @@ -local prettyValue = require("src.lua.utils.prettyValue") -local assertType = require("src.lua.asserts.type") +local serialiseValue = require("utils.serialise-value") +local assertType = require("asserts.type") ---@type lest.Matcher local function toMatch(ctx, received, pattern) @@ -9,9 +9,9 @@ local function toMatch(ctx, received, pattern) pass = type(received) == "string" and received:match(pattern) ~= nil, message = string.format( "Expected %s to%smatch %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(pattern) + serialiseValue(pattern) ), } end diff --git a/packages/lest/src/lua/matchers/strings.test.lua b/packages/lest/src/lua/matchers/strings.test.lua index a20c9f1..ee98504 100644 --- a/packages/lest/src/lua/matchers/strings.test.lua +++ b/packages/lest/src/lua/matchers/strings.test.lua @@ -1,6 +1,6 @@ -local matchers = require("src.lua.matchers.strings") -local assertMatcher = require("src.lua.asserts.matchers") -local prettyValue = require("src.lua.utils.prettyValue") +local matchers = require("matchers.strings") +local assertMatcher = require("asserts.matchers") +local serialiseValue = require("utils.serialise-value") describe("string matchers", function() local CONTEXT = { @@ -63,8 +63,8 @@ describe("string matchers", function() assertMatcher.hasMessage( result, ("Expected %s to match %s"):format( - prettyValue(testString), - prettyValue(testPattern) + serialiseValue(testString), + serialiseValue(testPattern) ) ) end @@ -86,8 +86,8 @@ describe("string matchers", function() assertMatcher.hasMessage( result, ("Expected %s to match %s"):format( - prettyValue(testString), - prettyValue(testPattern) + serialiseValue(testString), + serialiseValue(testPattern) ) ) end @@ -107,8 +107,8 @@ describe("string matchers", function() assertMatcher.hasMessage( result, ("Expected %s to match %s"):format( - prettyValue(invalidObject), - prettyValue(testString) + serialiseValue(invalidObject), + serialiseValue(testString) ) ) end) @@ -127,8 +127,8 @@ describe("string matchers", function() assertMatcher.hasMessage( result, ("Expected %s to not match %s"):format( - prettyValue(testString), - prettyValue(testPattern) + serialiseValue(testString), + serialiseValue(testPattern) ) ) end) diff --git a/packages/lest/src/lua/matchers/tables.lua b/packages/lest/src/lua/matchers/tables.lua index 142026e..add38bf 100644 --- a/packages/lest/src/lua/matchers/tables.lua +++ b/packages/lest/src/lua/matchers/tables.lua @@ -1,6 +1,6 @@ -local prettyValue = require("src.lua.utils.prettyValue") -local deepEqual = require("src.lua.utils.deepEqual") -local assertType = require("src.lua.asserts.type") +local serialiseValue = require("utils.serialise-value") +local deepEqual = require("utils.deepEqual") +local assertType = require("asserts.type") ---@type lest.Matcher local function toHaveLength(ctx, received, length) @@ -14,7 +14,7 @@ local function toHaveLength(ctx, received, length) pass = success and comparison, message = string.format( "Expected %s to%shave a length of %d", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", length ), @@ -40,9 +40,9 @@ local function toContain(ctx, received, item) pass = pass, message = string.format( "Expected %s to%scontain %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(item) + serialiseValue(item) ), } end @@ -64,9 +64,9 @@ local function toContainEqual(ctx, received, item) pass = pass, message = string.format( "Expected %s to%scontain %s with deep equality", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(item) + serialiseValue(item) ), } end @@ -106,9 +106,9 @@ local function toMatchObject(ctx, received, object) pass = type(received) == "table" and matches(received, object), message = string.format( "Expected %s to%smatch %s", - prettyValue(received), + serialiseValue(received), ctx.inverted and " not " or " ", - prettyValue(object) + serialiseValue(object) ), } end diff --git a/packages/lest/src/lua/matchers/tables.test.lua b/packages/lest/src/lua/matchers/tables.test.lua index 594e6bd..02aa79c 100644 --- a/packages/lest/src/lua/matchers/tables.test.lua +++ b/packages/lest/src/lua/matchers/tables.test.lua @@ -1,6 +1,6 @@ -local matchers = require("src.lua.matchers.tables") -local assertMatcher = require("src.lua.asserts.matchers") -local prettyValue = require("src.lua.utils.prettyValue") +local matchers = require("matchers.tables") +local assertMatcher = require("asserts.matchers") +local serialiseValue = require("utils.serialise-value") describe("table matchers", function() local CONTEXT = { @@ -49,7 +49,7 @@ describe("table matchers", function() assertMatcher.hasMessage( resultArray, ("Expected %s to have a length of %d"):format( - prettyValue(testArray), + serialiseValue(testArray), 4 ) ) @@ -69,7 +69,7 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to have a length of 10"):format( - prettyValue(noLengthObject) + serialiseValue(noLengthObject) ) ) end) @@ -87,7 +87,7 @@ describe("table matchers", function() assertMatcher.hasMessage( resultArray, ("Expected %s to not have a length of 4"):format( - prettyValue(testArray) + serialiseValue(testArray) ) ) end) @@ -145,8 +145,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to contain %s"):format( - prettyValue(testArray), - prettyValue(testItem) + serialiseValue(testArray), + serialiseValue(testItem) ) ) end @@ -168,8 +168,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to contain %s"):format( - prettyValue(testString), - prettyValue(testItem) + serialiseValue(testString), + serialiseValue(testItem) ) ) end @@ -190,8 +190,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to contain %s"):format( - prettyValue(invalidObject), - prettyValue(10) + serialiseValue(invalidObject), + serialiseValue(10) ) ) end @@ -211,8 +211,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to not contain %s"):format( - prettyValue(testArray), - prettyValue(testItem) + serialiseValue(testArray), + serialiseValue(testItem) ) ) end) @@ -253,8 +253,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to contain %s with deep equality"):format( - prettyValue(testArray), - prettyValue(testItem) + serialiseValue(testArray), + serialiseValue(testItem) ) ) end @@ -273,7 +273,7 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to contain 45 with deep equality"):format( - prettyValue(invalidObject) + serialiseValue(invalidObject) ) ) end) @@ -292,8 +292,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to not contain %s with deep equality"):format( - prettyValue(testArray), - prettyValue(testItem) + serialiseValue(testArray), + serialiseValue(testItem) ) ) end) @@ -395,8 +395,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to match %s"):format( - prettyValue(testObject), - prettyValue(testMatchObject) + serialiseValue(testObject), + serialiseValue(testMatchObject) ) ) end @@ -442,8 +442,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to match %s"):format( - prettyValue(testObjectArray), - prettyValue(testMatchObjectArray) + serialiseValue(testObjectArray), + serialiseValue(testMatchObjectArray) ) ) end @@ -463,8 +463,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to match %s"):format( - prettyValue(invalidObject), - prettyValue(matchTable) + serialiseValue(invalidObject), + serialiseValue(matchTable) ) ) end) @@ -492,8 +492,8 @@ describe("table matchers", function() assertMatcher.hasMessage( result, ("Expected %s to not match %s"):format( - prettyValue(testObject), - prettyValue(testMatchObject) + serialiseValue(testObject), + serialiseValue(testMatchObject) ) ) end) diff --git a/packages/lest/src/lua/prettyprint.lua b/packages/lest/src/lua/prettyprint.lua index 2394ef1..d68f343 100644 --- a/packages/lest/src/lua/prettyprint.lua +++ b/packages/lest/src/lua/prettyprint.lua @@ -1,7 +1,6 @@ local COLOURS = require("src.lua.utils.consoleColours") local NodeType = require("src.lua.interface.testnodetype") local tablex = require("src.lua.utils.tablex") -local prettyValue = require("src.lua.utils.prettyValue") local codepage = require("src.lua.utils.codepage") local PASS_SYMBOL = COLOURS.PASS("√") diff --git a/packages/lest/src/lua/utils/is-lua-symbol.lua b/packages/lest/src/lua/utils/is-lua-symbol.lua new file mode 100644 index 0000000..d465b65 --- /dev/null +++ b/packages/lest/src/lua/utils/is-lua-symbol.lua @@ -0,0 +1,34 @@ +local KEYWORDS = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, +} + +--- Returns true if the value is a string containing a valid Lua symbol +---@param value any +---@return boolean +local function isLuaSymbol(value) + return type(value) == "string" + and not KEYWORDS[value] + and not not string.match(value, "^[_%a][_%a%d]*$") +end + +return isLuaSymbol diff --git a/packages/lest/src/lua/utils/prettyValue.lua b/packages/lest/src/lua/utils/prettyValue.lua deleted file mode 100644 index 8f503fb..0000000 --- a/packages/lest/src/lua/utils/prettyValue.lua +++ /dev/null @@ -1,22 +0,0 @@ ---- Formats a value to look prettier ----@param value any ----@return string -return function(value) - if type(value) == "string" then - return '"' .. value .. '"' - end - - if value == math.huge then - return "inf" - end - - if value == -math.huge then - return "-inf" - end - - if value ~= value then - return "NaN" - end - - return tostring(value) -end diff --git a/packages/lest/src/lua/utils/printTable.lua b/packages/lest/src/lua/utils/printTable.lua deleted file mode 100644 index 9ab1ae1..0000000 --- a/packages/lest/src/lua/utils/printTable.lua +++ /dev/null @@ -1,38 +0,0 @@ -local prettyValue = require("src.lua.utils.prettyValue") - -local function printTable(table, currDepth, maxDepth) - local indent = string.rep(" ", currDepth * 4) - - if currDepth > maxDepth then - print(indent .. "") - return - end - - for k, v in pairs(table) do - if type(v) == "table" then - print(string.format("%s[%s] = {", indent, prettyValue(k))) - printTable(v, currDepth + 1, maxDepth) - print(indent .. "},") - else - print( - string.format( - "%s[%s] = %s,", - indent, - prettyValue(k), - prettyValue(v) - ) - ) - end - end -end - ---- Prints a table to the console ----@param table table ----@param depth? integer -return function(table, depth) - depth = depth or 2 - - print("{") - printTable(table, 1, depth) - print("}") -end diff --git a/packages/lest/src/lua/utils/render-diff.lua b/packages/lest/src/lua/utils/render-diff.lua new file mode 100644 index 0000000..67dc51e --- /dev/null +++ b/packages/lest/src/lua/utils/render-diff.lua @@ -0,0 +1,196 @@ +local serialiseValue = require("utils.serialise-value") +local sortTableKeys = require("utils.sort-table-keys") +local isLuaSymbol = require("utils.is-lua-symbol") +local COLOURS = require("utils.consoleColours") + +local CIRCULAR_REFERENCE_TEXT = "" + +local COLOURS_BY_PREFIX = { + [" "] = function(text) + return text + end, + ["-"] = COLOURS.EXPECTED, + ["+"] = COLOURS.RECEIVED, +} + +--- Renders a field in the diff of a table +---@param key any +---@param value any +---@param isArray boolean +---@return string +local function renderTableField(key, value, isArray) + if isArray then + return value + end + + if isLuaSymbol(key) then + return string.format("%s = %s", key, value) + end + + return string.format("[%s] = %s", serialiseValue(key), value) +end + +--- Returns a sorted list of the union of keys from the expected and received tables +---@param expectedTable table +---@param receivedTable table +---@return any[] +local function getCombinedSortedKeys(expectedTable, receivedTable) + local keys = {} + local countedKeys = {} + + for key in pairs(expectedTable) do + table.insert(keys, key) + countedKeys[key] = true + end + for key in pairs(receivedTable) do + if not countedKeys[key] then + table.insert(keys, key) + end + end + + sortTableKeys(keys) + return keys +end + +--- Renders a difference between two tables +---@param expectedTable table +---@param receivedTable table +---@param indentation? string +---@param visitedTables? table +---@return table +local function renderTableDiff( + expectedTable, + receivedTable, + indentation, + visitedTables +) + indentation = indentation or " " + local rendered = "Table {\n" + local expected = 0 + local received = 0 + + local visitedTablesCopy = {} + for k, v in pairs(visitedTables or {}) do + visitedTablesCopy[k] = v + end + visitedTablesCopy[expectedTable] = true + visitedTablesCopy[receivedTable] = true + + local nextArrayKey = 1 + for _, key in ipairs(getCombinedSortedKeys(expectedTable, receivedTable)) do + local expectedValue = expectedTable[key] + local receivedValue = receivedTable[key] + + local isArray = key == nextArrayKey + if isArray then + nextArrayKey = nextArrayKey + 1 + end + + local renderCurrentField = function(prefix, value) + rendered = string.format( + "%s%s\n", + rendered, + COLOURS_BY_PREFIX[prefix]( + string.format( + "%s %s%s,", + prefix, + indentation, + renderTableField(key, value, isArray) + ) + ) + ) + + if prefix == "-" then + expected = expected + 1 + elseif prefix == "+" then + received = received + 1 + end + end + + if expectedValue == receivedValue then + renderCurrentField(" ", serialiseValue(expectedValue)) + elseif expectedValue == nil then + renderCurrentField("+", serialiseValue(receivedValue)) + elseif receivedValue == nil then + renderCurrentField("-", serialiseValue(expectedValue)) + elseif + visitedTablesCopy[expectedValue] or visitedTablesCopy[receivedValue] + then + renderCurrentField( + "-", + visitedTablesCopy[expectedValue] and CIRCULAR_REFERENCE_TEXT + or serialiseValue(expectedValue) + ) + renderCurrentField( + "+", + visitedTablesCopy[receivedValue] and CIRCULAR_REFERENCE_TEXT + or serialiseValue(receivedValue) + ) + elseif + type(expectedValue) == "table" + and type(receivedValue) == "table" + then + local valueDiff = renderTableDiff( + expectedValue, + receivedValue, + indentation .. " ", + visitedTablesCopy + ) + + renderCurrentField(" ", valueDiff.rendered) + expected = expected + valueDiff.expected + received = received + valueDiff.received + else + renderCurrentField("-", serialiseValue(expectedValue)) + renderCurrentField("+", serialiseValue(receivedValue)) + end + end + + return { + rendered = rendered .. indentation .. "}", + expected = expected, + received = received, + } +end + +--- Renders the difference between two values with ANSI colour highlighting +---@param expectedValue any +---@param receivedValue any +---@param deep boolean # Whether to render a deep equality diff +---@param inverted? boolean # Whether the assertion this diff is for was inverted (Default false) +---@return string +local function renderDiff(expectedValue, receivedValue, deep, inverted) + if inverted then + return string.format( + "Expected: not %s", + COLOURS.EXPECTED(serialiseValue(expectedValue)) + ) + end + + if + not deep + or type(expectedValue) ~= "table" + or type(receivedValue) ~= "table" + then + local serialisedExpected = serialiseValue(expectedValue) + local serialisedReceived = serialiseValue(receivedValue) + + return string.format( + "Expected: %s\nReceived: %s", + COLOURS.EXPECTED(serialisedExpected), + serialisedReceived == serialisedExpected + and "serialises to the same string" + or COLOURS.RECEIVED(serialisedReceived) + ) + end + + local diff = renderTableDiff(expectedValue, receivedValue) + return string.format( + "%s\n%s\n\n %s", + COLOURS.EXPECTED(string.format("- Expected - %d", diff.expected)), + COLOURS.RECEIVED(string.format("+ Received + %d", diff.received)), + diff.rendered + ) +end + +return renderDiff diff --git a/packages/lest/src/lua/utils/serialise-value.lua b/packages/lest/src/lua/utils/serialise-value.lua new file mode 100644 index 0000000..7eab6bc --- /dev/null +++ b/packages/lest/src/lua/utils/serialise-value.lua @@ -0,0 +1,141 @@ +local sortTableKeys = require("utils.sort-table-keys") +local isLuaSymbol = require("utils.is-lua-symbol") + +local PRINTABLE_REPLACEMENTS = { + ["\\"] = [[\\]], + ['"'] = [[\"]], + ["'"] = [[\']], +} + +local NON_PRINTABLE_REPLACEMENTS = { + ["\a"] = [[\a]], + ["\b"] = [[\b]], + ["\f"] = [[\f]], + ["\n"] = [[\n]], + ["\r"] = [[\r]], + ["\t"] = [[\t]], + ["\v"] = [[\v]], +} + +local function serialiseString(str) + return '"' + .. str:gsub("[\\\"']", PRINTABLE_REPLACEMENTS) + :gsub("[^\032-\126]", function(match) + return NON_PRINTABLE_REPLACEMENTS[match] + or string.format([[\%d]], string.byte(match, 1)) + end) + .. '"' +end + +--- Iterator function which returns elements in numeric then lexicographic order +---@param tbl table +---@return fun(): any, any +local function sortedPairs(tbl) + local keys = {} + for key in pairs(tbl) do + table.insert(keys, key) + end + + sortTableKeys(keys) + + local i = 1 + return function() + local key = keys[i] + if key ~= nil then + i = i + 1 + return key, tbl[key] + end + end +end + +local serialiseValue + +--- Serialises an inline truncated table +---@param tbl table +---@param visitedTables table +---@return string +local function serialiseTable(tbl, visitedTables) + local renderedFields = {} + local nextArrayIndex = 1 + + for key, value in sortedPairs(tbl) do + if type(key) == "number" and key == nextArrayIndex then + table.insert(renderedFields, serialiseValue(value, visitedTables)) + nextArrayIndex = nextArrayIndex + 1 + elseif isLuaSymbol(key) then + table.insert( + renderedFields, + string.format( + "%s = %s", + key, + serialiseValue(value, visitedTables) + ) + ) + else + table.insert( + renderedFields, + string.format( + "[%s] = %s", + serialiseValue(key, visitedTables), + serialiseValue(value, visitedTables) + ) + ) + end + end + + return string.format("{%s}", table.concat(renderedFields, ", ")) +end + +--- Serialises a value recursively +---@param value any +---@param visitedTables? table +---@return string +serialiseValue = function(value, visitedTables) + visitedTables = visitedTables or {} + + if type(value) == "string" then + return serialiseString(value) + end + + -- TODO: Replace with math.huge and update tests + if value == math.huge then + return "inf" + end + + if value == -math.huge then + return "-inf" + end + + if value ~= value then + return "NaN" + end + + if + type(value) == "table" + and string.match(tostring(value), "table: [%a%d]+") + then + if visitedTables[value] then + return "" + end + + local visitedTablesCopy = {} + for k, v in pairs(visitedTables) do + visitedTablesCopy[k] = v + end + visitedTablesCopy[value] = true + + return serialiseTable(value, visitedTablesCopy) + end + + if type(value) == "function" then + return "" + end + + if type(value) == "userdata" then + return "" + end + + return tostring(value) +end + +return serialiseValue diff --git a/packages/lest/src/lua/utils/sort-table-keys.lua b/packages/lest/src/lua/utils/sort-table-keys.lua new file mode 100644 index 0000000..cb62242 --- /dev/null +++ b/packages/lest/src/lua/utils/sort-table-keys.lua @@ -0,0 +1,18 @@ +--- Sorts an array of keys in-place first by value for numeric keys, and then lexicographically +--- +--- Useful for rendering tables with mixed syntax (e.g. `{1, 2, [111] = 3, a = 4}`) +---@param keys any[] +local function sortTableKeys(keys) + table.sort(keys, function(a, b) + local aIsNumber = type(a) == "number" + local bIsNumber = type(b) == "number" + + if aIsNumber then + return not bIsNumber or a < b + end + + return not bIsNumber and tostring(a) < tostring(b) + end) +end + +return sortTableKeys diff --git a/packages/lest/src/lua/utils/deepEqual.test.lua b/packages/lest/src/lua/utils/tests/deepEqual.test.lua similarity index 100% rename from packages/lest/src/lua/utils/deepEqual.test.lua rename to packages/lest/src/lua/utils/tests/deepEqual.test.lua diff --git a/packages/lest/src/lua/utils/tests/render-diff.test.lua b/packages/lest/src/lua/utils/tests/render-diff.test.lua new file mode 100644 index 0000000..edb4a1a --- /dev/null +++ b/packages/lest/src/lua/utils/tests/render-diff.test.lua @@ -0,0 +1,242 @@ +local renderDiff = require("utils.render-diff") +local COLOURS = require("utils.consoleColours") + +test.each({ + { + "primitive before and after", + 123, + "456", + string.format( + "Expected: %s\nReceived: %s", + COLOURS.EXPECTED("123"), + COLOURS.RECEIVED('"456"') + ), + }, + { + "primitive before, table after", + 123, + { 1, 2, 3 }, + string.format( + "Expected: %s\nReceived: %s", + COLOURS.EXPECTED("123"), + COLOURS.RECEIVED("{1, 2, 3}") + ), + }, + { + "table before, primitive after", + { 1, 2, 3 }, + 123, + string.format( + "Expected: %s\nReceived: %s", + COLOURS.EXPECTED("{1, 2, 3}"), + COLOURS.RECEIVED("123") + ), + }, + { + "same primitive before and after", + 1, + 1, + string.format( + "Expected: %s\nReceived: serialises to the same string", + COLOURS.EXPECTED("1") + ), + }, + { + "non-recursive equal tables", + { foo = 1 }, + { foo = 1 }, + string.format( + [[%s +%s + + Table { + foo = 1, + }]], + COLOURS.EXPECTED("- Expected - 0"), + COLOURS.RECEIVED("+ Received + 0") + ), + }, + { + "non-recursive tables with different value", + { foo = 1 }, + { foo = 2 }, + string.format( + [[%s +%s + + Table { +%s +%s + }]], + COLOURS.EXPECTED("- Expected - 1"), + COLOURS.RECEIVED("+ Received + 1"), + COLOURS.EXPECTED("- foo = 1,"), + COLOURS.RECEIVED("+ foo = 2,") + ), + }, + { + "non-recursive tables with different field", + { foo = 1 }, + { bar = 1 }, + string.format( + [[%s +%s + + Table { +%s +%s + }]], + COLOURS.EXPECTED("- Expected - 1"), + COLOURS.RECEIVED("+ Received + 1"), + COLOURS.RECEIVED("+ bar = 1,"), + COLOURS.EXPECTED("- foo = 1,") + ), + }, + { + "non-recursive tables with missing field", + { foo = 1 }, + {}, + string.format( + [[%s +%s + + Table { +%s + }]], + COLOURS.EXPECTED("- Expected - 1"), + COLOURS.RECEIVED("+ Received + 0"), + COLOURS.EXPECTED("- foo = 1,") + ), + }, + { + "recursive tables", + { { foo = 1 }, { bar = 2 }, [true] = { baz = 3 } }, + { { foo = 1 }, { bar = 3 }, [true] = { biz = 3 } }, + string.format( + [[%s +%s + + Table { + Table { + foo = 1, + }, + Table { +%s +%s + }, + [true] = Table { +%s +%s + }, + }]], + COLOURS.EXPECTED("- Expected - 2"), + COLOURS.RECEIVED("+ Received + 2"), + COLOURS.EXPECTED("- bar = 2,"), + COLOURS.RECEIVED("+ bar = 3,"), + COLOURS.EXPECTED("- baz = 3,"), + COLOURS.RECEIVED("+ biz = 3,") + ), + }, +})( + "correctly renders %s when deep is true", + function(_, expectedValue, receivedValue, expectedDiff) + local rendered = renderDiff(expectedValue, receivedValue, true, false) + + expect(rendered).toBe(expectedDiff) + end +) + +test("handles circular reference in expected table", function() + local expectedValue = {} + expectedValue.bar = expectedValue + local receivedValue = { bar = "5" } + + local rendered = renderDiff(expectedValue, receivedValue, true, false) + + expect(rendered).toBe( + string.format( + "%s\n%s\n\n Table {\n%s\n%s\n }", + COLOURS.EXPECTED("- Expected - 1"), + COLOURS.RECEIVED("+ Received + 1"), + COLOURS.EXPECTED("- bar = ,"), + COLOURS.RECEIVED('+ bar = "5",') + ) + ) +end) + +test("handles circular reference in received table", function() + local expectedValue = { bar = "5" } + local receivedValue = {} + receivedValue.bar = receivedValue + + local rendered = renderDiff(expectedValue, receivedValue, true, false) + + expect(rendered).toBe( + string.format( + "%s\n%s\n\n Table {\n%s\n%s\n }", + COLOURS.EXPECTED("- Expected - 1"), + COLOURS.RECEIVED("+ Received + 1"), + COLOURS.EXPECTED('- bar = "5",'), + COLOURS.RECEIVED("+ bar = ,") + ) + ) +end) + +test("handles circular reference in both tables", function() + local expectedValue = {} + expectedValue.bar = expectedValue + local receivedValue = {} + receivedValue.bar = receivedValue + + local rendered = renderDiff(expectedValue, receivedValue, true, false) + + expect(rendered).toBe( + string.format( + "%s\n%s\n\n Table {\n%s\n%s\n }", + COLOURS.EXPECTED("- Expected - 1"), + COLOURS.RECEIVED("+ Received + 1"), + COLOURS.EXPECTED("- bar = ,"), + COLOURS.RECEIVED("+ bar = ,") + ) + ) +end) + +test("handles non-circular internal reference", function() + local expectedValue = { a = {}, b = { foo = "bar" } } + expectedValue.a.b = expectedValue.b + local receivedValue = { a = {}, b = { foo = "bar" } } + receivedValue.a.b = receivedValue.b + + local rendered = renderDiff(expectedValue, receivedValue, true, false) + + expect(rendered).toBe( + string.format( + [[%s +%s + + Table { + a = Table { + b = Table { + foo = "bar", + }, + }, + b = Table { + foo = "bar", + }, + }]], + COLOURS.EXPECTED("- Expected - 0"), + COLOURS.RECEIVED("+ Received + 0") + ) + ) +end) + +test( + "serialises the expected value and returns inverted message when inverted", + function() + local expectedValue = { 1, 2, 3 } + + local rendered = renderDiff(expectedValue, expectedValue, true, true) + + expect(rendered).toBe("Expected: not " .. COLOURS.EXPECTED("{1, 2, 3}")) + end +) diff --git a/packages/lest/src/lua/utils/tests/serialise-value.test.lua b/packages/lest/src/lua/utils/tests/serialise-value.test.lua new file mode 100644 index 0000000..ee72fae --- /dev/null +++ b/packages/lest/src/lua/utils/tests/serialise-value.test.lua @@ -0,0 +1,62 @@ +local serialiseValue = require("utils.serialise-value") + +test.each({ + { "string", "foo", [["foo"]] }, + { + "string with special characters", + "\a \b \f \n \r \t \v \\ \" ' \0 \00 \000 \1 \026", + [["\a \b \f \n \r \t \v \\ \" \' \0 \0 \0 \1 \26"]], + }, + { "number", 123, [[123]] }, + { "boolean", true, [[true]] }, + { "positive infinity", math.huge, [[inf]] }, + { "negative infinity", -math.huge, [[-inf]] }, + { "NaN", 0 / 0, [[NaN]] }, + { + "table with tostring metamethod", + setmetatable({}, { + __tostring = function() + return "Blah" + end, + }), + [[Blah]], + }, + { "array-like table", { 1, "foo", 3 }, [[{1, "foo", 3}]] }, + { + "object-like table", + { foo = true, ["b-ar"] = false }, + [[{["b-ar"] = false, foo = true}]], + }, + { + "mixed table", + { 1, foo = 2, [true] = 3 }, + [[{1, foo = 2, [true] = 3}]], + }, + { + "nested table", + { foo = { 1, 2, 3 } }, + [[{foo = {1, 2, 3}}]], + }, +})("serialises %s", function(_, value, expected) + local serialised = serialiseValue(value) + + expect(serialised).toBe(expected) +end) + +test("serialises table with circular reference", function() + local tbl = { foo = "123" } + tbl.bar = tbl + + local serialised = serialiseValue(tbl) + + expect(serialised).toBe('{bar = , foo = "123"}') +end) + +test("serialises table with non-circular internal reference", function() + local tbl = { a = {}, b = { foo = "bar" } } + tbl.a.b = tbl.b + + local serialised = serialiseValue(tbl) + + expect(serialised).toBe('{a = {b = {foo = "bar"}}, b = {foo = "bar"}}') +end) diff --git a/packages/lest/src/lua/utils/timeout.test.lua b/packages/lest/src/lua/utils/tests/timeout.test.lua similarity index 100% rename from packages/lest/src/lua/utils/timeout.test.lua rename to packages/lest/src/lua/utils/tests/timeout.test.lua