{"id":"hardlyardi/runtest","name":"runtest","scope":"hardlyardi","platform":"roblox","description":"Mirrored from the Wally registry.","version":"0.1.0","latest":"0.1.0","versions":["0.0.1-test2","0.0.1-test3","0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"f462ebdb16de8d2ef548e26381879fccbbcd15390df4fe792010913279d313f6","likes":0,"downloads":0,"install":"forest install hardlyardi/runtest","url":"https://forest.dev/p/roblox/hardlyardi/runtest","files":"https://api.forest.dev/ai/package/roblox/hardlyardi/runtest/files","readme":"# Runtest\r\n\r\nRuntest is a testing framework designed for easily constructing robust testing environments in lune.\r\n\r\n`example.spec.luau`\r\n\r\n```luau\r\nlocal EPSILON = 0.001\r\n\r\nlocal runtest = require(\"@runtest/\")\r\nlocal task = require(\"@lune/task\")\r\n\r\nlocal spec = runtest.test.spec.init(...)\r\nlocal pprint = runtest.util.style.print.from_epsilon\r\n\r\nlocal value\r\n\r\nspec.test(\"foo\", function(interface)\r\n    local timeout = task.delay(5, function()\r\n        interface:fail(\"timeout exceeded\")\r\n        interface:early_end()\r\n    end)\r\n\r\n    assert(not value)\r\n    value = { 0.001 }\r\n\r\n    interface:expect_exactly_equal(value, value)\r\n    interface:expect_not_exactly_equal(value, {})\r\n    interface:expect_equal(value, value, EPSILON)\r\n    interface:expect_not_equal(value, {}, EPSILON)\r\n    interface:expect_truthy(true)\r\n    interface:expect_falsy(false)\r\n    task.cancel(timeout)\r\nend)\r\n\r\nspec.test(\"bar\", function(interface)\r\n    -- every test function in the spec runs in a completely unique environment! you can put in all kinds of statefulness\r\n    -- or boilerplate, and the tests will remain sound.\r\n    assert(not value)\r\n    value = { 0.001 }\r\n    interface:output(pprint(value))\r\n    interface:output_no_newline(\"\\n:3c\")\r\nend)\r\n\r\nspec.test(\"oh no\", function(interface)\r\n    -- Tests can even error or cause problems! They'll all still run the same way.\r\n    error(\">:3\")\r\nend)\r\n\r\nreturn spec.done()\r\n```\r\n\r\nTo make some of the magic happen, a \"runner\" is needed. Runtest's design scope is pretty much.. to run tests, so, apart\r\nfrom styling, you need to roll your own testing suite. Should look something like this:\r\n\r\n```luau\r\nlocal SPEC_DIRECTORY = \"./tests/_specs\"\r\nlocal MATCH_SPEC_FILE = \"%.spec%.luau$\"\r\nlocal RESULTS_DIRECTORY = \"./_SUITE\"\r\n\r\nlocal filesystem = require(\"@lune/fs\")\r\nlocal process = require(\"@lune/process\")\r\nlocal runtest = require(\"@runtest/\")\r\nlocal serdes = require(\"@lune/serde\")\r\n\r\ntype CompletedSpec = runtest.CompletedSpec\r\ntype FilesystemHierarchy<T = true> = runtest.FilesystemHierarchy<T>\r\n\r\nlocal run = runtest.run\r\nlocal style = runtest.util.style\r\nlocal filesystem_hierarchy = runtest.util.filesystem_hierarchy\r\n\r\nlocal color = style.color\r\n\r\nlocal blue = color.blue\r\nlocal green_bright = color.green_bright\r\nlocal red_bright = color.red_bright\r\n\r\nlocal function complete_spec(filename: string, name: string): CompletedSpec\r\n    print(blue(`Running Spec \"{name}\"`))\r\n\r\n    local spec = run.spec(filename, name)\r\n    local completed_spec = spec:test()\r\n\r\n    if completed_spec.result ~= \"PASS\" then\r\n        print(red_bright(`Ended with result \"{completed_spec.result}\". Log:`) .. completed_spec.log)\r\n    end\r\n\r\n    if completed_spec.amount_pass > 0 then\r\n        print(green_bright(`{completed_spec.amount_pass} Tests passed!`))\r\n    else\r\n        print(green_bright(`No tests passed.`))\r\n    end\r\n\r\n    if completed_spec.amount_fail > 0 then\r\n        print(red_bright(`{completed_spec.amount_fail} Tests failed!`))\r\n        for test_name, completed_test in completed_spec.tests do\r\n            if completed_test.result ~= \"FAIL\" then continue end\r\n            print(red_bright(`-  \"{test_name}\" failed:`) .. `{completed_test.log}`)\r\n        end\r\n    else\r\n        print(green_bright(`No tests failed.`))\r\n    end\r\n\r\n    if completed_spec.amount_error > 0 then\r\n        print(red_bright(`{completed_spec.amount_error} Tests errored!`))\r\n        for test_name, completed_test in completed_spec.tests do\r\n            if completed_test.result ~= \"ERROR\" then continue end\r\n            print(red_bright(`-  \"{test_name}\" errored:`) .. `{completed_test.log}`)\r\n        end\r\n    else\r\n        print(green_bright(`No tests errored.`))\r\n    end\r\n\r\n    return completed_spec\r\nend\r\n\r\nlocal function complete_specs(\r\n    spec_hierarchy: FilesystemHierarchy,\r\n    parent_name: string\r\n): FilesystemHierarchy<CompletedSpec>\r\n    local completed_specs = {} :: FilesystemHierarchy<CompletedSpec>\r\n    for child_name, child in spec_hierarchy do\r\n        local child_full_name = `{parent_name}/{child_name}`\r\n        if type(child) == \"table\" then\r\n            completed_specs[child_name] = complete_specs(child, child_full_name)\r\n            continue\r\n        end\r\n        completed_specs[child_name] = complete_spec(child_full_name, child_full_name)\r\n    end\r\n    return completed_specs\r\nend\r\n\r\nlocal spec_hierarchy = filesystem_hierarchy.new(SPEC_DIRECTORY)\r\nspec_hierarchy = filesystem_hierarchy.filter(spec_hierarchy, { MATCH_SPEC_FILE })\r\n\r\nlocal completed_specs = complete_specs(spec_hierarchy, SPEC_DIRECTORY)\r\n\r\nlocal any_didnt_pass = false\r\n\r\npcall(filesystem.removeDir, RESULTS_DIRECTORY)\r\nfilesystem.writeDir(RESULTS_DIRECTORY)\r\n\r\nfor filename, completed_spec in completed_specs do\r\n    local encoded_spec = serdes.encode(\"json\", completed_spec, true)\r\n\r\n    if completed_spec.result ~= \"PASS\" then any_didnt_pass = true end\r\n\r\n    filesystem.writeFile(`{RESULTS_DIRECTORY}/{filename}.testresult.json`, encoded_spec)\r\nend\r\n\r\nif any_didnt_pass then\r\n    print(red_bright(\"\\nSome specs did not succeed.\"))\r\n    process.exit(1)\r\nend\r\n\r\nprint(green_bright(\"\\nAll specs passed.\"))\r\nprocess.exit(0)\r\n```\r\n","readmeTruncated":false}