diff --git a/indra/alscript/CMakeLists.txt b/indra/alscript/CMakeLists.txt index a7ae6d5182..e6706a1eca 100644 --- a/indra/alscript/CMakeLists.txt +++ b/indra/alscript/CMakeLists.txt @@ -237,168 +237,83 @@ if(AL_BUILD_TESTS) templates_define "AL_SCRIPT_TEMPLATES_DIR=\"${INDRA_SOURCE_DIR}/newview/app_settings/script_templates\"" ) - al_add_test( - alluauservice - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} ${templates_define} + # What every binary of alscript's tests sets up once, as the viewer has it + # before it checks a script, and the state guard's checks on what the + # script engines keep for the process. + add_library(alscript_test_checks OBJECT tests/alscript_test_checks.cpp) + target_link_libraries( + alscript_test_checks + PRIVATE al::flags al_test_support alscript llcommon ll::tailslide ) - # Completion and signature help over a fragment, against the whole - # script, at the places a scripter asks, over every SLua script to hand; - # the places found by Luau's own lexer. - al_add_test( - alluaufragment + target_compile_definitions(alscript_test_checks PRIVATE ${definitions_defines}) + set_target_properties(alscript_test_checks PROPERTIES FOLDER "Tests/alscript") + + # One Catch2 binary for alscript's tests, each test file run in a process + # of its own. The SLua checks, and LSL written again as SLua, run again + # with Luau's new type solver, which a scripter may choose + # (ALScriptLuauSolver): the same tests, told by the environment. + al_add_test_binary( + alscript_tests PROJECT alscript - LIBRARIES alscript llcommon ll::slua + SOURCES + tests/aldiskcache_test.cpp + tests/aldiskincludes_test.cpp + tests/alincludeidentity_test.cpp + tests/alincludesearch_test.cpp + tests/allinelabel_test.cpp + tests/allslconsts_test.cpp + tests/allslcosts_test.cpp + tests/allsleffects_test.cpp + tests/allsloptimizer_test.cpp + tests/allslservice_test.cpp + tests/allsltoslua_test.cpp + tests/alluauconfig_test.cpp + tests/alluaufragment_test.cpp + tests/alluaunavigation_test.cpp + tests/alluauservice_test.cpp + tests/almasterburst_test.cpp + tests/almasterindex_test.cpp + tests/almasterlinks_test.cpp + tests/almastermatch_test.cpp + tests/almasterplan_test.cpp + tests/almasterqueue_test.cpp + tests/almasterwatch_test.cpp + tests/almessagemap_test.cpp + tests/almodulelook_test.cpp + tests/alnotecardformat_test.cpp + tests/alpreprocessor_test.cpp + tests/alpreprocessorc11_test.cpp + tests/alscriptenvelope_test.cpp + tests/alscriptfixes_test.cpp + tests/alscriptformatter_test.cpp + tests/alscriptjoblane_test.cpp + tests/alscriptlexicon_test.cpp + tests/alscriptlintpass_test.cpp + tests/alscriptmessages_test.cpp + tests/alscriptnameindex_test.cpp + tests/alscriptoutlinepairs_test.cpp + tests/alscriptsnapshot_test.cpp + tests/alscriptstack_test.cpp + tests/alscripttempfiles_test.cpp + tests/alscripttextcache_test.cpp + tests/alscriptweight_test.cpp + tests/alsourcemap_test.cpp + tests/aluploadheader_test.cpp + SUPPORT alscript_test_checks llcommon_test_checks + LIBRARIES alscript llcommon ll::slua ll::tailslide DEFINES ${definitions_defines} ${templates_define} "AL_ALSCRIPT_TEST_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/tests\"" - ) - # Where a name is declared and used, across a script and the modules it - # requires. - al_add_test( - alluaunavigation - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} - ) - al_add_test( - allslservice - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} ${templates_define} - ) - # Each fix made over a real check, applied, and the text checked again. - al_add_test( - alscriptfixes - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} - ) - # The studio's own lints, each over real checks, with its fix made. - al_add_test( - alscriptlintpass - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} - ) - # The SLua checks, and LSL written again as SLua, again with Luau's new - # type solver, which a scripter may choose (ALScriptLuauSolver): the same - # tests, told by the environment. - foreach( - test - alluauservice - alluaufragment - alluaunavigation - alscriptfixes - alscriptlintpass - allsltoslua - ) - add_test( - NAME INTEGRATION_TEST_RUNNER_${test}_newsolver - COMMAND $ - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - ) - set_tests_properties( - INTEGRATION_TEST_RUNNER_${test}_newsolver - PROPERTIES - ENVIRONMENT "AL_TEST_LUAU_SOLVER=new" - ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" - SKIP_RETURN_CODE 125 - ) - endforeach() - # What a script weighs: SLua by its bytecode, LSO by Tailslide's image, - # with the grid's builtins loaded. - al_add_test( - alscriptweight - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} - ) - al_add_test(aldiskincludes PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(aldiskcache PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alincludeidentity PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alincludesearch PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(allinelabel PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almodulelook PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alsourcemap PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptjoblane PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptnameindex PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptoutlinepairs PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptsnapshot PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscripttextcache PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptstack PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptenvelope PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(aluploadheader PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almessagemap PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alluauconfig PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptformatter PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptlexicon PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscriptmessages PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alnotecardformat PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(alscripttempfiles PROJECT alscript LIBRARIES alscript llcommon) - # Disk masters: the changes gathered over a burst, the links and their - # index, the index kept in a file, a master found from what a script says - # or its name, what a send does, the sends underway, and the files watched - # for saves made outside. - al_add_test(almasterburst PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almasterindex PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almasterlinks PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almastermatch PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almasterplan PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almasterqueue PROJECT alscript LIBRARIES alscript llcommon) - al_add_test(almasterwatch PROJECT alscript LIBRARIES alscript llcommon) - # The preprocessor against C11's 6.10, clause by clause. - al_add_test(alpreprocessorc11 PROJECT alscript LIBRARIES alscript llcommon) - # What LSL may change and the order it runs in, over scripts parsed with - # the grid's builtins loaded. - al_add_test( - allsleffects - PROJECT alscript - LIBRARIES alscript llcommon ll::tailslide - DEFINES ${definitions_defines} - ) - # What LSL forms cost on each target: snippet pairs compiled by - # Tailslide, against what the optimizer takes them to cost. - al_add_test( - allslcosts - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} - ) - # The const keyword over scripts parsed with the grid's builtins loaded. - al_add_test( - allslconsts - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} - ) - # The optimizer's golden files under tests/optimizer/, checked with the - # grid's builtins loaded. - al_add_test( - allsloptimizer - PROJECT alscript - LIBRARIES alscript llcommon - DEFINES ${definitions_defines} "AL_ALSCRIPT_TEST_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/tests\"" - ) - # LSL written again as SLua, each result checked as SLua against the - # grid's definitions; and Tailslide's own test scripts under - # tests/tailslide/, for their breadth. The comments a toggle makes, as - # Luau's own lexer reads them. - al_add_test( - allsltoslua - PROJECT alscript - LIBRARIES alscript llcommon ll::slua - DEFINES ${definitions_defines} "AL_ALSCRIPT_TEST_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/tests\"" - ) - # The golden files under tests/preprocessor/: every test_*.lsl beside - # its expected output. - al_add_test( - alpreprocessor - PROJECT alscript - LIBRARIES alscript llcommon ll::slua - DEFINES ${definitions_defines} "AL_ALSCRIPT_TEST_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/tests\"" + RERUN newsolver + RERUN_SOURCES + tests/alluaufragment_test.cpp + tests/alluaunavigation_test.cpp + tests/alluauservice_test.cpp + tests/allsltoslua_test.cpp + tests/alscriptfixes_test.cpp + tests/alscriptlintpass_test.cpp + RERUN_ENVIRONMENT AL_TEST_LUAU_SOLVER=new ) # The preprocessor, the formatter, the optimizer and one analysis of each # language over big scripts, with the grid's definitions loaded. diff --git a/indra/alscript/lsl/allslservice.cpp b/indra/alscript/lsl/allslservice.cpp index 95a75403d5..2d14b5b0c7 100644 --- a/indra/alscript/lsl/allslservice.cpp +++ b/indra/alscript/lsl/allslservice.cpp @@ -1359,6 +1359,9 @@ bool ALLSLService::loadBuiltins(const std::string& path, std::string& error) added += '\n'; } } + // This service has them, whatever the table already held: a file + // all of whose names an earlier one loaded is loaded all the same. + mImpl->builtins = true; error.clear(); if (added.empty()) { @@ -1392,7 +1395,6 @@ bool ALLSLService::loadBuiltins(const std::string& path, std::string& error) const int index = Luau::lslEventIndex(symbol->getName()); symbol->setEventIndex(index > 0 ? index : ++last); } - mImpl->builtins = true; return true; } // Read here and handed over as text: a path Tailslide opened itself diff --git a/indra/alscript/tests/aldiskcache_test.cpp b/indra/alscript/tests/aldiskcache_test.cpp index c23183e32f..b4eefea5db 100644 --- a/indra/alscript/tests/aldiskcache_test.cpp +++ b/indra/alscript/tests/aldiskcache_test.cpp @@ -29,7 +29,7 @@ #include "fsyspath.h" #include "llfile.h" -#include "../test/lltut.h" +#include #include #include @@ -69,20 +69,10 @@ namespace }; } -namespace tut +namespace { - struct aldiskcache_data - { - }; - - typedef test_group aldiskcache_group; - typedef aldiskcache_group::object aldiskcache_object; - aldiskcache_group aldiskcache_instance("aldiskcache"); - - template<> template<> - void aldiskcache_object::test<1>() + TEST_CASE("aldiskcache: what is blessed and admitted kept until stale or the settings move", "[aldiskcache]") { - set_test_name("the folders blessed as the preprocessor blesses them, kept a moment or until the settings move; what they admit kept with them"); Scratch s; const std::string part = s.write("includes/lib/part.lsl", "integer part;\n"); s.write("includes/.lslrc", "{\"include\": [\"lib\"]}"); @@ -91,51 +81,76 @@ namespace tut const std::vector own{ s.at("includes") }; ALDiskCache::Blessed& first = cache.blessed(own, true, s.at("project/a"), {}, 1, 100.0); - ensure_equals("the scripter's, and what its .lslrc lists", first.includes.folders().size(), size_t(2)); - ensure_equals("a file under them admitted", cache.admits(first, part).value_or(""), part); - ensure("what is not there, not", !cache.admits(first, s.at("includes/lib/none.lsl"))); + UNSCOPED_INFO("the scripter's, and what its .lslrc lists"); + REQUIRE(first.includes.folders().size() == size_t(2)); + UNSCOPED_INFO("a file under them admitted"); + CHECK(cache.admits(first, part).value_or("") == part); + UNSCOPED_INFO("what is not there, not"); + REQUIRE_FALSE(cache.admits(first, s.at("includes/lib/none.lsl"))); // Taken away: still admitted while what is kept is fresh, and the // folders as they were. fs::remove(s.under("includes/lib/part.lsl")); ALDiskCache::Blessed& again = cache.blessed(own, true, s.at("project/a"), {}, 1, 101.0); - ensure("the same kept", &again == &first && cache.admits(again, part).has_value()); + { + INFO("the same kept"); + CHECK(&again == &first); + REQUIRE(cache.admits(again, part).has_value()); + } std::string text; - ensure("but not read", !cache.read(part, text)); + UNSCOPED_INFO("but not read"); + CHECK_FALSE(cache.read(part, text)); ALDiskCache::Blessed& later = cache.blessed(own, true, s.at("project/a"), {}, 1, 100.0 + ALDiskCache::FRESH_SECONDS + 0.5); - ensure("a moment later, asked again", !cache.admits(later, part)); + UNSCOPED_INFO("a moment later, asked again"); + REQUIRE_FALSE(cache.admits(later, part)); s.write("includes/lib/part.lsl", "integer part;\n"); - ensure("kept as asked until then", !cache.admits(later, part)); + UNSCOPED_INFO("kept as asked until then"); + REQUIRE_FALSE(cache.admits(later, part)); ALDiskCache::Blessed& moved = cache.blessed(own, true, s.at("project/a"), {}, 2, 103.0); - ensure("the settings moved: asked again at once", cache.admits(moved, part).has_value()); + UNSCOPED_INFO("the settings moved: asked again at once"); + REQUIRE(cache.admits(moved, part).has_value()); ALDiskCache::Blessed& plain = cache.blessed(own, false, s.at("project/a"), {}, 2, 103.0); - ensure_equals("SLua's: no .lslrc read", plain.includes.folders().size(), size_t(1)); + UNSCOPED_INFO("SLua's: no .lslrc read"); + REQUIRE(plain.includes.folders().size() == size_t(1)); ALDiskCache::Blessed& aliased = cache.blessed(own, false, std::string(), { s.at("project") }, 2, 103.0); - ensure_equals("an alias's folder blessed outright", aliased.includes.folders().size(), size_t(2)); - ensure_equals("the file at the top of each that has it", cache.atTop(moved, ".lslrc").size(), size_t(1)); + UNSCOPED_INFO("an alias's folder blessed outright"); + REQUIRE(aliased.includes.folders().size() == size_t(2)); + UNSCOPED_INFO("the file at the top of each that has it"); + REQUIRE(cache.atTop(moved, ".lslrc").size() == size_t(1)); } - template<> template<> - void aldiskcache_object::test<2>() + TEST_CASE("aldiskcache: a text read again once its time or size changes; upwards kept a moment", "[aldiskcache]") { - set_test_name("a text read again only once its time or size changes; the configurations up the folders found once a moment"); Scratch s; const std::string file = s.write("lib/util.lsl", "integer one;\n"); ALDiskCache cache; std::string text; - ensure("read", cache.read(file, text) && text == "integer one;\n"); + REQUIRE(cache.read(file, text)); + CHECK(text == "integer one;\n"); s.write("lib/util.lsl", "integer three;\n"); - ensure("read again once it changed", cache.read(file, text) && text == "integer three;\n"); - ensure("nothing where nothing is", !cache.read(s.at("lib/none.lsl"), text)); + { + INFO("read again once it changed"); + CHECK(cache.read(file, text)); + CHECK(text == "integer three;\n"); + } + UNSCOPED_INFO("nothing where nothing is"); + CHECK_FALSE(cache.read(s.at("lib/none.lsl"), text)); s.write("a/.luaurc", "{}"); s.write("a/b/c/.luaurc", "{}"); const std::vector& up = cache.upwards(s.at("a/b/c"), ".luaurc", 1, 10.0); - ensure("nearest first, to the root", up.size() >= 2 && up[0] == s.at("a/b/c/.luaurc") && up[1] == s.at("a/.luaurc")); + { + INFO("nearest first, to the root"); + REQUIRE(up.size() >= 2); + CHECK(up[0] == s.at("a/b/c/.luaurc")); + CHECK(up[1] == s.at("a/.luaurc")); + } s.write("a/b/.luaurc", "{}"); - ensure_equals("kept a moment", cache.upwards(s.at("a/b/c"), ".luaurc", 1, 11.0).size(), up.size()); - ensure("then found", cache.upwards(s.at("a/b/c"), ".luaurc", 1, 10.0 + ALDiskCache::FRESH_SECONDS + 1.0)[1] == s.at("a/b/.luaurc")); + UNSCOPED_INFO("kept a moment"); + REQUIRE(cache.upwards(s.at("a/b/c"), ".luaurc", 1, 11.0).size() == up.size()); + UNSCOPED_INFO("then found"); + CHECK(cache.upwards(s.at("a/b/c"), ".luaurc", 1, 10.0 + ALDiskCache::FRESH_SECONDS + 1.0)[1] == s.at("a/b/.luaurc")); } } diff --git a/indra/alscript/tests/aldiskincludes_test.cpp b/indra/alscript/tests/aldiskincludes_test.cpp index c1749798f4..a37b6d2ea7 100644 --- a/indra/alscript/tests/aldiskincludes_test.cpp +++ b/indra/alscript/tests/aldiskincludes_test.cpp @@ -28,7 +28,7 @@ #include "fsyspath.h" -#include "../test/lltut.h" +#include #include #include @@ -79,48 +79,49 @@ namespace }; } -namespace tut +namespace { - struct aldiskincludes_data - { - }; - - typedef test_group aldiskincludes_group; - typedef aldiskincludes_group::object aldiskincludes_object; - aldiskincludes_group aldiskincludes_instance("aldiskincludes"); - - template<> template<> - void aldiskincludes_object::test<1>() + TEST_CASE("aldiskincludes: nothing is read but under a blessed folder however the path is put", "[aldiskincludes]") { - set_test_name("nothing on the disk is read but under a blessed folder, however the path is put"); Scratch s; const std::string lib = s.write("lib/util.lsl", "integer util() { return 1; }\n"); const std::string secret = s.write("private/secret.txt", "the author's own\n"); s.write("lib2/other.lsl", "x\n"); ALDiskIncludes none; - ensure("nothing blessed, nothing read", !none.blessed() && !none.admits(lib)); + { + INFO("nothing blessed, nothing read"); + CHECK_FALSE(none.blessed()); + REQUIRE_FALSE(none.admits(lib)); + } ALDiskIncludes blessed; blessed.bless(s.at("lib")); - ensure("blessed", blessed.blessed() && blessed.folders().size() == 1); - ensure_equals("a file under it is read, as it stands", blessed.admits(lib).value_or(""), lib); - ensure("a path from a root elsewhere is not", !blessed.admits(secret)); - ensure("nor one climbing out of the blessed folder", !blessed.admits(s.at("lib/../private/secret.txt"))); - ensure("nor one in a folder whose name only begins with it", !blessed.admits(s.at("lib2/other.lsl"))); - ensure("nor the folder itself", !blessed.admits(s.at("lib"))); - ensure("nor what is not there", !blessed.admits(s.at("lib/missing.lsl"))); + CHECK(blessed.blessed()); + REQUIRE(blessed.folders().size() == 1); + UNSCOPED_INFO("a file under it is read, as it stands"); + CHECK(blessed.admits(lib).value_or("") == lib); + UNSCOPED_INFO("a path from a root elsewhere is not"); + REQUIRE_FALSE(blessed.admits(secret)); + UNSCOPED_INFO("nor one climbing out of the blessed folder"); + REQUIRE_FALSE(blessed.admits(s.at("lib/../private/secret.txt"))); + UNSCOPED_INFO("nor one in a folder whose name only begins with it"); + REQUIRE_FALSE(blessed.admits(s.at("lib2/other.lsl"))); + UNSCOPED_INFO("nor the folder itself"); + REQUIRE_FALSE(blessed.admits(s.at("lib"))); + UNSCOPED_INFO("nor what is not there"); + REQUIRE_FALSE(blessed.admits(s.at("lib/missing.lsl"))); blessed.bless(s.at("nowhere")); blessed.bless(lib); - ensure_equals("what is not a folder blesses no folder", blessed.folders().size(), size_t(1)); + UNSCOPED_INFO("what is not a folder blesses no folder"); + REQUIRE(blessed.folders().size() == size_t(1)); blessed.bless(s.at("lib/../lib")); - ensure_equals("and one folder is blessed once however it is named", blessed.folders().size(), size_t(1)); + UNSCOPED_INFO("and one folder is blessed once however it is named"); + REQUIRE(blessed.folders().size() == size_t(1)); } - template<> template<> - void aldiskincludes_object::test<2>() + TEST_CASE("aldiskincludes: links followed before judging; only sensibly sized ordinary files read", "[aldiskincludes]") { - set_test_name("a link is followed before it is judged, and only an ordinary file of a sensible size is read"); Scratch s; const std::string secret = s.write("private/secret.txt", "the author's own\n"); s.write("lib/util.lsl", "x\n"); @@ -131,66 +132,94 @@ namespace tut fs::create_symlink(fsyspath(secret), s.root / "lib" / "innocent.lsl", ec); if (!ec) { - ensure("a link out of the folder is not read", !blessed.admits(s.at("lib/innocent.lsl"))); + UNSCOPED_INFO("a link out of the folder is not read"); + REQUIRE_FALSE(blessed.admits(s.at("lib/innocent.lsl"))); } fs::create_directory_symlink(s.root / "private", s.root / "lib" / "door", ec); if (!ec) { - ensure("nor a file through a linked folder", !blessed.admits(s.at("lib/door/secret.txt"))); + UNSCOPED_INFO("nor a file through a linked folder"); + REQUIRE_FALSE(blessed.admits(s.at("lib/door/secret.txt"))); } const std::string big = s.write("lib/big.lsl", std::string(static_cast(ALDiskIncludes::MAX_BYTES) + 1, 'x')); - ensure("a file past the limit is not read", !blessed.admits(big)); + UNSCOPED_INFO("a file past the limit is not read"); + REQUIRE_FALSE(blessed.admits(big)); std::string text; - ensure("nor read by the reader", !ALDiskIncludes::readOrdinary(big, text) && text.empty()); - ensure("a folder is not read", !ALDiskIncludes::readOrdinary(s.at("lib"), text)); - ensure("an ordinary one is", ALDiskIncludes::readOrdinary(s.at("lib/util.lsl"), text) && text == "x\n"); - ensure("and held at its own size, not the limit's", text.capacity() < 1024); + { + INFO("nor read by the reader"); + REQUIRE_FALSE(ALDiskIncludes::readOrdinary(big, text)); + REQUIRE(text.empty()); + } + UNSCOPED_INFO("a folder is not read"); + CHECK_FALSE(ALDiskIncludes::readOrdinary(s.at("lib"), text)); + { + INFO("an ordinary one is"); + CHECK(ALDiskIncludes::readOrdinary(s.at("lib/util.lsl"), text)); + CHECK(text == "x\n"); + } + UNSCOPED_INFO("and held at its own size, not the limit's"); + CHECK(text.capacity() < 1024); const std::string whole(100000, 'y'); const std::string mid = s.write("lib/mid.lsl", whole); - ensure("one of some size, whole", ALDiskIncludes::readOrdinary(mid, text) && text == whole); - ensure("held at about its size", text.capacity() < whole.size() + 1024); + { + INFO("one of some size, whole"); + CHECK(ALDiskIncludes::readOrdinary(mid, text)); + CHECK(text == whole); + } + UNSCOPED_INFO("held at about its size"); + REQUIRE(text.capacity() < whole.size() + 1024); const std::string at_limit = s.write("lib/limit.lsl", std::string(static_cast(ALDiskIncludes::MAX_BYTES), 'z')); - ensure("one at the limit, whole", ALDiskIncludes::readOrdinary(at_limit, text) && text.size() == ALDiskIncludes::MAX_BYTES); + { + INFO("one at the limit, whole"); + CHECK(ALDiskIncludes::readOrdinary(at_limit, text)); + REQUIRE(text.size() == ALDiskIncludes::MAX_BYTES); + } #if LL_DARWIN || LL_LINUX // What would be read for ever, or never answer: refused at once. ALDiskIncludes devices; devices.bless("/dev"); - ensure("a device is not an include", !devices.admits("/dev/zero")); - ensure("nor read", !ALDiskIncludes::readOrdinary("/dev/zero", text)); + UNSCOPED_INFO("a device is not an include"); + REQUIRE_FALSE(devices.admits("/dev/zero")); + UNSCOPED_INFO("nor read"); + CHECK_FALSE(ALDiskIncludes::readOrdinary("/dev/zero", text)); const std::string pipe = s.at("lib/pipe"); if (mkfifo(pipe.c_str(), 0600) == 0) { - ensure("a pipe is not an include", !blessed.admits(pipe)); - ensure("nor read, and the reading does not wait on it", !ALDiskIncludes::readOrdinary(pipe, text)); + UNSCOPED_INFO("a pipe is not an include"); + REQUIRE_FALSE(blessed.admits(pipe)); + UNSCOPED_INFO("nor read, and the reading does not wait on it"); + CHECK_FALSE(ALDiskIncludes::readOrdinary(pipe, text)); } #endif } - template<> template<> - void aldiskincludes_object::test<3>() + TEST_CASE("aldiskincludes: a .lslrc lists folders from beside it and the nearest up is found", "[aldiskincludes]") { - set_test_name("a .lslrc lists folders from beside it, and the nearest up from a folder is found"); Scratch s; s.write("project/.lslrc", "{\"include\": [\"../lib\", \"shared/\"]}"); s.write("project/src/deep/main.lsl", "x\n"); const std::vector listed = ALDiskIncludes::lslrcFolders(s.at("project")); - ensure_equals("both", listed.size(), size_t(2)); - ensure_equals("the one above, from beside the file", listed[0], s.at("lib")); - ensure_equals("the one below, without its slash", listed[1], s.at("project/shared")); + REQUIRE(listed.size() == size_t(2)); + UNSCOPED_INFO("the one above, from beside the file"); + CHECK(listed[0] == s.at("lib")); + UNSCOPED_INFO("the one below, without its slash"); + CHECK(listed[1] == s.at("project/shared")); std::string found_in; - ensure("the nearest up", ALDiskIncludes::nearestLslrcFolders(s.at("project/src/deep"), &found_in) == listed); - ensure_equals("and where it is", found_in, s.at("project")); - ensure("none where there is none", ALDiskIncludes::lslrcFolders(s.at("project/src")).empty()); + UNSCOPED_INFO("the nearest up"); + CHECK(ALDiskIncludes::nearestLslrcFolders(s.at("project/src/deep"), &found_in) == listed); + UNSCOPED_INFO("and where it is"); + CHECK(found_in == s.at("project")); + UNSCOPED_INFO("none where there is none"); + REQUIRE(ALDiskIncludes::lslrcFolders(s.at("project/src")).empty()); s.write("broken/.lslrc", "{not json"); - ensure("nor where it is not a configuration", ALDiskIncludes::lslrcFolders(s.at("broken")).empty()); + UNSCOPED_INFO("nor where it is not a configuration"); + REQUIRE(ALDiskIncludes::lslrcFolders(s.at("broken")).empty()); } - template<> template<> - void aldiskincludes_object::test<4>() + TEST_CASE("aldiskincludes: a blessed folder's files listed by path from it no deeper than asked", "[aldiskincludes]") { - set_test_name("a blessed folder's files are listed through its folders, by their paths from it, and no further than asked"); Scratch s; s.write("lib/util.luau", "return {}\n"); s.write("lib/net/http.luau", "return {}\n"); @@ -213,76 +242,103 @@ namespace tut return said; }; ALDiskIncludes blessed; - ensure("nothing of a folder not blessed", blessed.filesUnder(s.at("lib"), { ".luau" }, 3, 100, 100).empty()); + UNSCOPED_INFO("nothing of a folder not blessed"); + REQUIRE(blessed.filesUnder(s.at("lib"), { ".luau" }, 3, 100, 100).empty()); blessed.bless(s.at("lib")); const std::vector found = blessed.filesUnder(s.at("lib"), { ".luau", ".lua" }, 2, 100, 100); - ensure_equals("two folders down, no hidden one, only what is asked for", listed(found), std::string("net/http.luau util.luau")); + UNSCOPED_INFO("two folders down, no hidden one, only what is asked for"); + CHECK(listed(found) == std::string("net/http.luau util.luau")); for (const ALDiskIncludes::Listed& one : found) { - ensure_equals("each where it stands", one.file, s.at("lib/" + one.relative)); + UNSCOPED_INFO("each where it stands"); + REQUIRE(one.file == s.at("lib/" + one.relative)); } - ensure_equals("deeper where asked", listed(blessed.filesUnder(s.at("lib"), { ".luau" }, 4, 100, 100)), + UNSCOPED_INFO("deeper where asked"); + CHECK(listed(blessed.filesUnder(s.at("lib"), { ".luau" }, 4, 100, 100)) == std::string("net/deep/deeper/far.luau net/http.luau util.luau")); - ensure_equals("a folder under a blessed one, from itself", listed(blessed.filesUnder(s.at("lib/net"), { ".luau" }, 1, 100, 100)), + UNSCOPED_INFO("a folder under a blessed one, from itself"); + CHECK(listed(blessed.filesUnder(s.at("lib/net"), { ".luau" }, 1, 100, 100)) == std::string("http.luau")); - ensure_equals("no more than asked for", blessed.filesUnder(s.at("lib"), { ".luau" }, 4, 100, 1).size(), size_t(1)); - ensure("nothing outside", blessed.filesUnder(s.at("elsewhere"), { ".luau" }, 4, 100, 100).empty()); + UNSCOPED_INFO("no more than asked for"); + REQUIRE(blessed.filesUnder(s.at("lib"), { ".luau" }, 4, 100, 1).size() == size_t(1)); + UNSCOPED_INFO("nothing outside"); + REQUIRE(blessed.filesUnder(s.at("elsewhere"), { ".luau" }, 4, 100, 100).empty()); #if LL_DARWIN || LL_LINUX // A link to a folder outside is not followed down. std::error_code ec; fs::create_directory_symlink(s.under("elsewhere"), s.under("lib/linked"), ec); - ensure("linked", !ec); - ensure_equals("not through a link", listed(blessed.filesUnder(s.at("lib"), { ".luau" }, 4, 100, 100)), + UNSCOPED_INFO("linked"); + REQUIRE_FALSE(ec); + UNSCOPED_INFO("not through a link"); + CHECK(listed(blessed.filesUnder(s.at("lib"), { ".luau" }, 4, 100, 100)) == std::string("net/deep/deeper/far.luau net/http.luau util.luau")); #endif } - template<> template<> - void aldiskincludes_object::test<5>() + TEST_CASE("aldiskincludes: a configuration blesses only under its own folder or the scripter's", "[aldiskincludes]") { - set_test_name("a configuration blesses a folder under its own, or under one the scripter blessed, and no further"); Scratch s; s.write("project/shared/a.lsl", "a\n"); s.write("lib/b.lsl", "b\n"); s.write("secret/key.txt", "k\n"); ALDiskIncludes none; - ensure("under its own", none.mayFromConfig(s.at("project/shared"), s.at("project"))); - ensure("its own", none.mayFromConfig(s.at("project"), s.at("project"))); - ensure("not a sibling", !none.mayFromConfig(s.at("lib"), s.at("project"))); - ensure("not a root", !none.mayFromConfig(fsyspath(s.root.root_path()).string(), s.at("project"))); - ensure("not by going up", !none.mayFromConfig(s.at("project/../secret"), s.at("project"))); - ensure("not what is not there", !none.mayFromConfig(s.at("project/missing"), s.at("project"))); + UNSCOPED_INFO("under its own"); + CHECK(none.mayFromConfig(s.at("project/shared"), s.at("project"))); + UNSCOPED_INFO("its own"); + CHECK(none.mayFromConfig(s.at("project"), s.at("project"))); + UNSCOPED_INFO("not a sibling"); + CHECK_FALSE(none.mayFromConfig(s.at("lib"), s.at("project"))); + UNSCOPED_INFO("not a root"); + CHECK_FALSE(none.mayFromConfig(fsyspath(s.root.root_path()).string(), s.at("project"))); + UNSCOPED_INFO("not by going up"); + CHECK_FALSE(none.mayFromConfig(s.at("project/../secret"), s.at("project"))); + UNSCOPED_INFO("not what is not there"); + CHECK_FALSE(none.mayFromConfig(s.at("project/missing"), s.at("project"))); // The sibling, where the scripter's own folders hold it. ALDiskIncludes own; own.bless(fsyspath(s.root).string()); - ensure("under the scripter's", own.mayFromConfig(s.at("lib"), s.at("project"))); + UNSCOPED_INFO("under the scripter's"); + CHECK(own.mayFromConfig(s.at("lib"), s.at("project"))); ALDiskIncludes blessed; blessed.bless(s.at("lib")); - ensure("blessed from it", blessed.blessFromConfig(s.at("lib"), s.at("project"))); - ensure("and refused past it", !blessed.blessFromConfig(s.at("secret"), s.at("project")) && !blessed.admits(s.at("secret/key.txt"))); + UNSCOPED_INFO("blessed from it"); + CHECK(blessed.blessFromConfig(s.at("lib"), s.at("project"))); + { + INFO("and refused past it"); + CHECK_FALSE(blessed.blessFromConfig(s.at("secret"), s.at("project"))); + REQUIRE_FALSE(blessed.admits(s.at("secret/key.txt"))); + } #if LL_DARWIN || LL_LINUX // A link inside that leads outside is where it leads. std::error_code ec; fs::create_directory_symlink(s.under("secret"), s.under("project/escape"), ec); - ensure("linked: " + ec.message(), !ec); - ensure("not through a link out", !none.mayFromConfig(s.at("project/escape"), s.at("project"))); + UNSCOPED_INFO("linked: " << ec.message()); + REQUIRE_FALSE(ec); + UNSCOPED_INFO("not through a link out"); + CHECK_FALSE(none.mayFromConfig(s.at("project/escape"), s.at("project"))); #endif } - template<> template<> - void aldiskincludes_object::test<6>() + TEST_CASE("aldiskincludes: a require with no extension takes its own files before a folder's init", "[aldiskincludes]") { - set_test_name("a require of a name with no extension finds a folder's init.luau after the name's own files"); typedef std::vector names; - ensure("SLua's require", ALDiskIncludes::namesFor("lib", true, true) == + UNSCOPED_INFO("SLua's require"); + CHECK(ALDiskIncludes::namesFor("lib", true, true) == names{ "lib", "lib.luau", "lib.lua", "lib/init.luau", "lib/init.lua" }); const names folder = ALDiskIncludes::namesFor("./shared/lib/", true, true); - ensure("a path's last folder, however it ends", - folder.size() == 5 && folder[3] == "./shared/lib/init.luau" && folder[4] == "./shared/lib/init.lua"); - ensure("a name with its extension is that file", ALDiskIncludes::namesFor("lib.Luau", true, true) == + { + INFO("a path's last folder, however it ends"); + REQUIRE(folder.size() == 5); + CHECK(folder[3] == "./shared/lib/init.luau"); + CHECK(folder[4] == "./shared/lib/init.lua"); + } + UNSCOPED_INFO("a name with its extension is that file"); + CHECK(ALDiskIncludes::namesFor("lib.Luau", true, true) == names{ "lib.Luau", "lib.Luau.luau", "lib.Luau.lua" }); - ensure("an #include in SLua is a file", ALDiskIncludes::namesFor("lib", true, false) == names{ "lib", "lib.luau", "lib.lua" }); - ensure("an LSL include", ALDiskIncludes::namesFor("lib", false, false) == names{ "lib", "lib.lsl" }); + UNSCOPED_INFO("an #include in SLua is a file"); + CHECK(ALDiskIncludes::namesFor("lib", true, false) == names{ "lib", "lib.luau", "lib.lua" }); + UNSCOPED_INFO("an LSL include"); + CHECK(ALDiskIncludes::namesFor("lib", false, false) == names{ "lib", "lib.lsl" }); // As a folder is looked in: the first of the names it admits. Scratch s; @@ -299,19 +355,21 @@ namespace tut } return std::string(); }; - ensure_equals("the folder's module", first("lib"), init); + UNSCOPED_INFO("the folder's module"); + CHECK(first("lib") == init); const std::string own = s.write("modules/lib.luau", "return 1\n"); - ensure_equals("a file of the name before the folder", first("lib"), own); - ensure_equals("init by its own name too", first("lib/init"), init); + UNSCOPED_INFO("a file of the name before the folder"); + CHECK(first("lib") == own); + UNSCOPED_INFO("init by its own name too"); + CHECK(first("lib/init") == init); } - template<> template<> - void aldiskincludes_object::test<7>() + TEST_CASE("aldiskincludes: atTop finds each blessed folder's own configuration in blessing order", "[aldiskincludes]") { - set_test_name("the configuration at the top of each blessed folder that has one, in their order, read as any file under it is"); Scratch s; ALDiskIncludes none; - ensure("nothing blessed, nothing found", none.atTop(".luaurc").empty()); + UNSCOPED_INFO("nothing blessed, nothing found"); + REQUIRE(none.atTop(".luaurc").empty()); s.write("first/util.luau", "return 1\n"); const std::string second = s.write("second/.luaurc", "{\"aliases\": {\"lib\": \"./lib\"}}\n"); @@ -322,27 +380,30 @@ namespace tut blessed.bless(s.at("third")); blessed.bless(s.at("second")); typedef std::vector files; - ensure("each folder's own, in the order blessed, not one further down", blessed.atTop(".luaurc") == files{ third, second }); + UNSCOPED_INFO("each folder's own, in the order blessed, not one further down"); + CHECK(blessed.atTop(".luaurc") == files{ third, second }); // A folder so named is no file. fs::create_directories(s.under("first/.luaurc")); - ensure("a folder so named passed over", blessed.atTop(".luaurc") == files{ third, second }); + UNSCOPED_INFO("a folder so named passed over"); + CHECK(blessed.atTop(".luaurc") == files{ third, second }); #if LL_DARWIN || LL_LINUX // A link out of the blessed folders is where it leads, and not read. s.write("elsewhere/.luaurc", "{}\n"); std::error_code ec; fs::remove(s.under("first/.luaurc"), ec); fs::create_symlink(s.under("elsewhere/.luaurc"), s.under("first/.luaurc"), ec); - ensure("linked: " + ec.message(), !ec); - ensure("not through a link out", blessed.atTop(".luaurc") == files{ third, second }); + UNSCOPED_INFO("linked: " << ec.message()); + REQUIRE_FALSE(ec); + UNSCOPED_INFO("not through a link out"); + CHECK(blessed.atTop(".luaurc") == files{ third, second }); #endif - ensure("nor a name no folder has", blessed.atTop(".lslrc").empty()); + UNSCOPED_INFO("nor a name no folder has"); + REQUIRE(blessed.atTop(".lslrc").empty()); } - template<> template<> - void aldiskincludes_object::test<8>() + TEST_CASE("aldiskincludes: scriptsUnder lists each script of a language once within the limits", "[aldiskincludes]") { - set_test_name("the scripts of a language under folders: each folder blessed for the look, each script once, no more than asked, nothing outside"); Scratch s; s.write("lib/util.luau", "return {}\n"); s.write("lib/old.lua", "return {}\n"); @@ -361,33 +422,42 @@ namespace tut } return said; }; - ensure_equals("SLua's, both folders, through their folders", - names(ALDiskIncludes::scriptsUnder({ s.at("lib"), s.at("tools") }, true, 4, 100)), + UNSCOPED_INFO("SLua's, both folders, through their folders"); + CHECK(names(ALDiskIncludes::scriptsUnder({ s.at("lib"), s.at("tools") }, true, 4, 100)) == std::string("lib/net/http.luau lib/old.lua lib/util.luau tools/t.luau")); - ensure_equals("LSL's and its includes'", names(ALDiskIncludes::scriptsUnder({ s.at("lib") }, false, 4, 100)), + UNSCOPED_INFO("LSL's and its includes'"); + CHECK(names(ALDiskIncludes::scriptsUnder({ s.at("lib") }, false, 4, 100)) == std::string("lib/door.lsl lib/door.lslh")); - ensure_equals("a folder given twice, or under another, each once", - ALDiskIncludes::scriptsUnder({ s.at("lib"), s.at("lib/net"), s.at("lib") }, true, 4, 100).size(), size_t(3)); - ensure_equals("no more than asked", ALDiskIncludes::scriptsUnder({ s.at("lib"), s.at("tools") }, true, 4, 2).size(), size_t(2)); - ensure_equals("not deeper than asked", names(ALDiskIncludes::scriptsUnder({ s.at("lib") }, true, 0, 100)), + UNSCOPED_INFO("a folder given twice, or under another, each once"); + REQUIRE(ALDiskIncludes::scriptsUnder({ s.at("lib"), s.at("lib/net"), s.at("lib") }, true, 4, 100).size() == size_t(3)); + UNSCOPED_INFO("no more than asked"); + REQUIRE(ALDiskIncludes::scriptsUnder({ s.at("lib"), s.at("tools") }, true, 4, 2).size() == size_t(2)); + UNSCOPED_INFO("not deeper than asked"); + CHECK(names(ALDiskIncludes::scriptsUnder({ s.at("lib") }, true, 0, 100)) == std::string("lib/old.lua lib/util.luau")); - ensure("nothing of a folder not there", ALDiskIncludes::scriptsUnder({ s.at("nowhere") }, true, 4, 100).empty()); + UNSCOPED_INFO("nothing of a folder not there"); + REQUIRE(ALDiskIncludes::scriptsUnder({ s.at("nowhere") }, true, 4, 100).empty()); } - template<> template<> - void aldiskincludes_object::test<9>() + TEST_CASE("aldiskincludes: extensions in any case; a file blessed alone; paths judged lexically", "[aldiskincludes]") { - set_test_name("a script's extensions, in any case; a file blessed alone, nothing beside it; a path as written under a folder or not, nothing on the disk asked"); typedef std::vector names; const names& lua = ALDiskIncludes::scriptExtensions(true); - ensure("SLua's, the modern first", lua == names{ ".luau", ".lua" }); - ensure("LSL's and its includes'", ALDiskIncludes::scriptExtensions(false) == names{ ".lsl", ".lslh", ".lsli" }); - ensure_equals("in any case", ALDiskIncludes::extensionOf("Util.LUAU", lua), size_t(5)); - ensure_equals("the old one", ALDiskIncludes::extensionOf("old.Lua", lua), size_t(4)); - ensure_equals("an include's", ALDiskIncludes::extensionOf("defs.LSLH", ALDiskIncludes::scriptExtensions(false)), size_t(5)); - ensure_equals("past a name of its own", ALDiskIncludes::extensionOf(".luau", lua), size_t(0)); - ensure_equals("none", ALDiskIncludes::extensionOf("notes.txt", lua), size_t(0)); - ensure("a require of one in any case is that file", ALDiskIncludes::namesFor("Util.LUAU", true, true) == + UNSCOPED_INFO("SLua's, the modern first"); + CHECK(lua == names{ ".luau", ".lua" }); + UNSCOPED_INFO("LSL's and its includes'"); + CHECK(ALDiskIncludes::scriptExtensions(false) == names{ ".lsl", ".lslh", ".lsli" }); + UNSCOPED_INFO("in any case"); + CHECK(ALDiskIncludes::extensionOf("Util.LUAU", lua) == size_t(5)); + UNSCOPED_INFO("the old one"); + CHECK(ALDiskIncludes::extensionOf("old.Lua", lua) == size_t(4)); + UNSCOPED_INFO("an include's"); + CHECK(ALDiskIncludes::extensionOf("defs.LSLH", ALDiskIncludes::scriptExtensions(false)) == size_t(5)); + UNSCOPED_INFO("past a name of its own"); + CHECK(ALDiskIncludes::extensionOf(".luau", lua) == size_t(0)); + CHECK(ALDiskIncludes::extensionOf("notes.txt", lua) == size_t(0)); + UNSCOPED_INFO("a require of one in any case is that file"); + CHECK(ALDiskIncludes::namesFor("Util.LUAU", true, true) == names{ "Util.LUAU", "Util.LUAU.luau", "Util.LUAU.lua" }); Scratch s; @@ -397,36 +467,65 @@ namespace tut ALDiskIncludes blessed; blessed.bless(s.at("lib")); const std::vector listed = blessed.filesUnder(s.at("lib"), lua, 1, 100, 100); - ensure("listed in any case", listed.size() == 1 && listed[0].file == shout); + { + INFO("listed in any case"); + REQUIRE(listed.size() == 1); + CHECK(listed[0].file == shout); + } blessed.bless(module); - ensure_equals("a file blessed alone", blessed.admits(module).value_or(""), module); - ensure("nothing beside it", !blessed.admits(beside)); - ensure_equals("and no folder", blessed.folders().size(), size_t(1)); + UNSCOPED_INFO("a file blessed alone"); + CHECK(blessed.admits(module).value_or("") == module); + UNSCOPED_INFO("nothing beside it"); + REQUIRE_FALSE(blessed.admits(beside)); + UNSCOPED_INFO("and no folder"); + REQUIRE(blessed.folders().size() == size_t(1)); const std::vector folders{ s.at("lib") }; std::string upper = s.at("lib/net/x.luau"); std::transform(upper.begin(), upper.end(), upper.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); - ensure("under, as written", ALDiskIncludes::lexicallyUnder(s.at("lib/net/x.luau"), folders)); - ensure("in any case", ALDiskIncludes::lexicallyUnder(upper, folders)); - ensure("whatever is there", ALDiskIncludes::lexicallyUnder(s.at("lib/missing/x.luau"), folders)); - ensure("not climbing out", !ALDiskIncludes::lexicallyUnder(s.at("lib/../vendor/json.luau"), folders)); - ensure("not a folder whose name it only begins with", !ALDiskIncludes::lexicallyUnder(s.at("lib2/x.luau"), folders)); - ensure("not another machine's share", !ALDiskIncludes::lexicallyUnder("//alincludes.invalid/share/x", folders) && - !ALDiskIncludes::lexicallyUnder("\\\\alincludes.invalid\\share\\x", folders)); - ensure("nor a device's path to one under it", !ALDiskIncludes::lexicallyUnder("\\\\?\\" + s.at("lib/x.luau"), folders)); - ensure("nor under nothing", !ALDiskIncludes::lexicallyUnder(s.at("lib/x.luau"), {}) && - !ALDiskIncludes::lexicallyUnder(std::string(), folders)); + UNSCOPED_INFO("under, as written"); + CHECK(ALDiskIncludes::lexicallyUnder(s.at("lib/net/x.luau"), folders)); + UNSCOPED_INFO("in any case"); + CHECK(ALDiskIncludes::lexicallyUnder(upper, folders)); + UNSCOPED_INFO("whatever is there"); + CHECK(ALDiskIncludes::lexicallyUnder(s.at("lib/missing/x.luau"), folders)); + UNSCOPED_INFO("not climbing out"); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder(s.at("lib/../vendor/json.luau"), folders)); + UNSCOPED_INFO("not a folder whose name it only begins with"); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder(s.at("lib2/x.luau"), folders)); + { + INFO("not another machine's share"); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder("//alincludes.invalid/share/x", folders)); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder("\\\\alincludes.invalid\\share\\x", folders)); + } + UNSCOPED_INFO("nor a device's path to one under it"); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder("\\\\?\\" + s.at("lib/x.luau"), folders)); + { + INFO("nor under nothing"); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder(s.at("lib/x.luau"), {})); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder(std::string(), folders)); + } // What a configuration lists of another machine's share: not // blessed, and not asked after. - ensure("a configuration's share", !blessed.mayFromConfig("//alincludes.invalid/share", s.at("lib")) && - !blessed.blessFromConfig("\\\\alincludes.invalid\\share", s.at("lib"))); + { + INFO("a configuration's share"); + CHECK_FALSE(blessed.mayFromConfig("//alincludes.invalid/share", s.at("lib"))); + CHECK_FALSE(blessed.blessFromConfig("\\\\alincludes.invalid\\share", s.at("lib"))); + } // Nor by the NT namespace's names, which Windows passes on as they // are: a share's, and even a blessed folder's own. - ensure("the NT namespace's share", !blessed.mayFromConfig("\\??\\UNC\\alincludes.invalid\\share", s.at("lib")) && - !blessed.blessFromConfig("/?\?/UNC/alincludes.invalid/share", s.at("lib"))); + { + INFO("the NT namespace's share"); + CHECK_FALSE(blessed.mayFromConfig("\\??\\UNC\\alincludes.invalid\\share", s.at("lib"))); + CHECK_FALSE(blessed.blessFromConfig("/?\?/UNC/alincludes.invalid/share", s.at("lib"))); + } fs::create_directories(s.under("lib/net")); - ensure("its name for a folder under the configuration's", !blessed.mayFromConfig("\\??\\" + s.at("lib/net"), s.at("lib")) && - !ALDiskIncludes::lexicallyUnder("\\??\\" + s.at("lib/net"), folders)); - ensure("which, as itself, it may name", blessed.mayFromConfig(s.at("lib/net"), s.at("lib"))); + { + INFO("its name for a folder under the configuration's"); + CHECK_FALSE(blessed.mayFromConfig("\\??\\" + s.at("lib/net"), s.at("lib"))); + CHECK_FALSE(ALDiskIncludes::lexicallyUnder("\\??\\" + s.at("lib/net"), folders)); + } + UNSCOPED_INFO("which, as itself, it may name"); + CHECK(blessed.mayFromConfig(s.at("lib/net"), s.at("lib"))); } } diff --git a/indra/alscript/tests/alincludeidentity_test.cpp b/indra/alscript/tests/alincludeidentity_test.cpp index 72edc93253..10dbc833ed 100644 --- a/indra/alscript/tests/alincludeidentity_test.cpp +++ b/indra/alscript/tests/alincludeidentity_test.cpp @@ -25,34 +25,49 @@ #include "../preprocessor/alincludeidentity.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { - struct alincludeidentity_data + TEST_CASE("alincludeidentity: object and inventory items and files read back but nothing else does", "[alincludeidentity]") { - }; - - typedef test_group alincludeidentity_group; - typedef alincludeidentity_group::object alincludeidentity_object; - alincludeidentity_group alincludeidentity_instance("alincludeidentity"); - - template<> template<> - void alincludeidentity_object::test<1>() - { - set_test_name("an item in an object, one in the inventory, a file: made and read back; anything else not"); const LLUUID prim = LLUUID::generateNewID(), item = LLUUID::generateNewID(); LLUUID object, found; std::string file; const std::string in_object = ALIncludeIdentity::ofItem(prim, item); - ensure("an object's item", ALIncludeIdentity::itemOf(in_object, object, found) && object == prim && found == item); + { + INFO("an object's item"); + REQUIRE(ALIncludeIdentity::itemOf(in_object, object, found)); + CHECK(object == prim); + CHECK(found == item); + } const std::string held = ALIncludeIdentity::ofItem(LLUUID::null, item); - ensure("an inventory item", held.rfind("inventory:", 0) == 0 && ALIncludeIdentity::itemOf(held, object, found) && object.isNull() && found == item); - ensure("both in the world", ALIncludeIdentity::inWorld(in_object) && ALIncludeIdentity::inWorld(held)); - ensure("a file", ALIncludeIdentity::fileOf(ALIncludeIdentity::ofFile("/lib/a.lsl"), file) && file == "/lib/a.lsl" && - !ALIncludeIdentity::inWorld("disk:/lib/a.lsl")); - ensure("an object's with no item", !ALIncludeIdentity::itemOf("object:" + prim.asString(), object, found)); - ensure("nor a file's item, nor an item's file", !ALIncludeIdentity::itemOf("disk:/a", object, found) && !ALIncludeIdentity::fileOf(held, file)); - ensure("an empty file", !ALIncludeIdentity::fileOf("disk:", file)); + { + INFO("an inventory item"); + CHECK(held.rfind("inventory:", 0) == 0); + CHECK(ALIncludeIdentity::itemOf(held, object, found)); + CHECK(object.isNull()); + CHECK(found == item); + } + { + INFO("both in the world"); + CHECK(ALIncludeIdentity::inWorld(in_object)); + CHECK(ALIncludeIdentity::inWorld(held)); + } + { + INFO("a file"); + REQUIRE(ALIncludeIdentity::fileOf(ALIncludeIdentity::ofFile("/lib/a.lsl"), file)); + CHECK(file == "/lib/a.lsl"); + CHECK_FALSE(ALIncludeIdentity::inWorld("disk:/lib/a.lsl")); + } + UNSCOPED_INFO("an object's with no item"); + CHECK_FALSE(ALIncludeIdentity::itemOf("object:" + prim.asString(), object, found)); + { + INFO("nor a file's item, nor an item's file"); + CHECK_FALSE(ALIncludeIdentity::itemOf("disk:/a", object, found)); + CHECK_FALSE(ALIncludeIdentity::fileOf(held, file)); + } + UNSCOPED_INFO("an empty file"); + CHECK_FALSE(ALIncludeIdentity::fileOf("disk:", file)); } } diff --git a/indra/alscript/tests/alincludesearch_test.cpp b/indra/alscript/tests/alincludesearch_test.cpp index cc5875d713..d41eb3b09a 100644 --- a/indra/alscript/tests/alincludesearch_test.cpp +++ b/indra/alscript/tests/alincludesearch_test.cpp @@ -30,7 +30,7 @@ #include "fsyspath.h" #include "llfile.h" -#include "../test/lltut.h" +#include #include #include @@ -234,76 +234,99 @@ namespace tut } }; - typedef test_group alincludesearch_group; - typedef alincludesearch_group::object alincludesearch_object; - alincludesearch_group alincludesearch_instance("alincludesearch"); - - template<> template<> - void alincludesearch_object::test<1>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: the world in the settings' order; a failed text skipped until retried", "[alincludesearch]") { - set_test_name("the world in the order the settings say, only where let in; a text not in hand wanted, one held taken, one that failed not asked again unless retried"); const ALIncludeWorld::Item held = item("lib.lsl", true); const ALIncludeWorld::Item theirs = item("lib.lsl", false); world.inventory["lib.lsl"] = { held }; world.object = { theirs }; ALPreprocessor::Include found; ALIncludeSearch::wanted_t wanted; - ensure_equals("the world not let in: nothing", int(search.resolve(ask("lib.lsl"), found, asking(), where(false, false), &wanted, false)), + UNSCOPED_INFO("the world not let in: nothing"); + CHECK(int(search.resolve(ask("lib.lsl"), found, asking(), where(false, false), &wanted, false)) == int(ALPreprocessor::Found::No)); - ensure_equals("let in: the inventory's first, not in hand", int(search.resolve(ask("lib.lsl"), found, asking(), where(true, false), &wanted, false)), + UNSCOPED_INFO("let in: the inventory's first, not in hand"); + CHECK(int(search.resolve(ask("lib.lsl"), found, asking(), where(true, false), &wanted, false)) == int(ALPreprocessor::Found::Pending)); - ensure("named, and wanted", found.path == held.path && wanted.contains(held.path)); + { + INFO("named, and wanted"); + CHECK(found.path == held.path); + CHECK(wanted.contains(held.path)); + } texts.put(held.path, held.assetId, "integer lib;\n"); - ensure("in hand: taken", search.resolve(ask("lib.lsl"), found, asking(), where(true, false), nullptr, false) == ALPreprocessor::Found::Yes && - found.text == "integer lib;\n" && found.assetId == held.assetId.asString()); + { + INFO("in hand: taken"); + CHECK(search.resolve(ask("lib.lsl"), found, asking(), where(true, false), nullptr, false) == ALPreprocessor::Found::Yes); + CHECK(found.text == "integer lib;\n"); + CHECK(found.assetId == held.assetId.asString()); + } ALIncludeSearch::Where object_first = where(true, false); object_first.order = { "object", "inventory" }; texts.failed(theirs.path); - ensure("the object's, failed before: passed over for the next", search.resolve(ask("lib.lsl"), found, asking(), object_first, nullptr, false) == - ALPreprocessor::Found::Yes && found.path == held.path); + { + INFO("the object's, failed before: passed over for the next"); + CHECK(search.resolve(ask("lib.lsl"), found, asking(), object_first, nullptr, false) == + ALPreprocessor::Found::Yes); + CHECK(found.path == held.path); + } wanted.clear(); - ensure("retried: wanted again", search.resolve(ask("lib.lsl"), found, asking(), object_first, &wanted, true) == ALPreprocessor::Found::Pending && - found.path == theirs.path && wanted.contains(theirs.path)); + { + INFO("retried: wanted again"); + CHECK(search.resolve(ask("lib.lsl"), found, asking(), object_first, &wanted, true) == ALPreprocessor::Found::Pending); + CHECK(found.path == theirs.path); + CHECK(wanted.contains(theirs.path)); + } world.inventory.clear(); world.object.clear(); world.objectSaid = false; - ensure("nowhere, and the object not said: it may be there", search.resolve(ask("lib.lsl"), found, asking(), where(true, false), nullptr, false) == + UNSCOPED_INFO("nowhere, and the object not said: it may be there"); + CHECK(search.resolve(ask("lib.lsl"), found, asking(), where(true, false), nullptr, false) == ALPreprocessor::Found::Pending); } - template<> template<> - void alincludesearch_object::test<2>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: the disk: the scripter's folders and .lslrc lists and nowhere else", "[alincludesearch]") { - set_test_name("the disk: under the scripter's folders and what their .lslrc lists, a name with its extension, beside a file asking; never elsewhere"); Scratch s; const std::string part = s.write("includes/lib/part.lsl", "integer part;\n"); s.write("includes/.lslrc", "{\"include\": [\"lib\"]}"); s.write("elsewhere/secret.lsl", "integer secret;\n"); const std::string own = s.at("includes"); ALPreprocessor::Include found; - ensure("the disk off: nothing", search.resolve(ask("part"), found, asking(), where(false, false, { own }), nullptr, false) == ALPreprocessor::Found::No); - ensure("on: through the .lslrc, with its extension", - search.resolve(ask("part"), found, asking(), where(false, true, { own }), nullptr, false) == ALPreprocessor::Found::Yes && - found.path == ALIncludeIdentity::ofFile(part) && found.name == "part.lsl" && found.text == "integer part;\n"); + UNSCOPED_INFO("the disk off: nothing"); + CHECK(search.resolve(ask("part"), found, asking(), where(false, false, { own }), nullptr, false) == ALPreprocessor::Found::No); + { + INFO("on: through the .lslrc, with its extension"); + CHECK(search.resolve(ask("part"), found, asking(), where(false, true, { own }), nullptr, false) == ALPreprocessor::Found::Yes); + CHECK(found.path == ALIncludeIdentity::ofFile(part)); + CHECK(found.name == "part.lsl"); + CHECK(found.text == "integer part;\n"); + } std::string text; - ensure("admitted: its text may be asked", search.heldText(found.path, text) && text == "integer part;\n"); - ensure("not a path out of the folders", search.resolve(ask(s.at("elsewhere/secret.lsl")), found, asking(), where(false, true, { own }), nullptr, + { + INFO("admitted: its text may be asked"); + REQUIRE(search.heldText(found.path, text)); + CHECK(text == "integer part;\n"); + } + UNSCOPED_INFO("not a path out of the folders"); + CHECK(search.resolve(ask(s.at("elsewhere/secret.lsl")), found, asking(), where(false, true, { own }), nullptr, false) == ALPreprocessor::Found::No); - ensure("nor its text", !search.heldText(ALIncludeIdentity::ofFile(s.at("elsewhere/secret.lsl")), text)); + UNSCOPED_INFO("nor its text"); + CHECK_FALSE(search.heldText(ALIncludeIdentity::ofFile(s.at("elsewhere/secret.lsl")), text)); // Beside a file asking, where the file is in a blessed folder. const std::string beside = s.write("includes/lib/near.lsl", "integer near;\n"); - ensure("beside the asking file", search.resolve(ask("near.lsl", false, ALIncludeIdentity::ofFile(part)), found, asking(), where(false, true, { own }), - nullptr, false) == ALPreprocessor::Found::Yes && - found.path == ALIncludeIdentity::ofFile(beside)); + { + INFO("beside the asking file"); + CHECK(search.resolve(ask("near.lsl", false, ALIncludeIdentity::ofFile(part)), found, asking(), where(false, true, { own }), + nullptr, false) == ALPreprocessor::Found::Yes); + CHECK(found.path == ALIncludeIdentity::ofFile(beside)); + } } - template<> template<> - void alincludesearch_object::test<3>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: SLua: a .luaurc alias on disk found and its folder blessed; mode read", "[alincludesearch]") { - set_test_name("SLua: an alias through the .luaurc chain over a file on disk, its folder blessed only as far as a configuration may reach; the chain's mode read"); Scratch s; const std::string script = s.write("project/main.luau", "local net = require(\"@lib/net\")\n"); s.write("project/.luaurc", "{\"aliases\": {\"lib\": \"./modules\"}, \"languageMode\": \"strict\"}"); @@ -313,50 +336,78 @@ namespace tut const ALIncludeSearch::Where disk = where(false, true, { s.at("project") }); ALPreprocessor::Include found; std::vector aliases; - ensure("through the alias", search.resolve(ask("@lib/net", true), found, from_disk, disk, nullptr, false, &aliases) == ALPreprocessor::Found::Yes && - found.path == ALIncludeIdentity::ofFile(net)); - ensure("its folder blessed for the run", aliases.size() == 1 && aliases[0].find("modules") != std::string::npos); - ensure("an alias nobody says: nothing", search.resolve(ask("@other/x", true), found, from_disk, disk, nullptr, false) == ALPreprocessor::Found::No); + { + INFO("through the alias"); + CHECK(search.resolve(ask("@lib/net", true), found, from_disk, disk, nullptr, false, &aliases) == ALPreprocessor::Found::Yes); + CHECK(found.path == ALIncludeIdentity::ofFile(net)); + } + { + INFO("its folder blessed for the run"); + REQUIRE(aliases.size() == 1); + REQUIRE(aliases[0].find("modules") != std::string::npos); + } + UNSCOPED_INFO("an alias nobody says: nothing"); + CHECK(search.resolve(ask("@other/x", true), found, from_disk, disk, nullptr, false) == ALPreprocessor::Found::No); std::vector chain; - ensure("the chain over the file", search.configsFor(from_disk.self, from_disk, disk, nullptr, false, chain) == ALPreprocessor::Found::Yes && - !chain.empty() && chain[0].path == ALIncludeIdentity::ofFile(s.at("project/.luaurc"))); + { + INFO("the chain over the file"); + CHECK(search.configsFor(from_disk.self, from_disk, disk, nullptr, false, chain) == ALPreprocessor::Found::Yes); + REQUIRE_FALSE(chain.empty()); + CHECK(chain[0].path == ALIncludeIdentity::ofFile(s.at("project/.luaurc"))); + } ALLuauConfig config; - ensure("its mode", search.configOf(from_disk, disk, config) && config.mode == "strict"); + { + INFO("its mode"); + CHECK(search.configOf(from_disk, disk, config)); + CHECK(config.mode == "strict"); + } const std::vector> folders = search.moduleFolders(from_disk, disk); - ensure("the module folders: the scripter's, and the alias's under its name", - folders.size() == 2 && folders[0].first.empty() && folders[1].first == "@lib/"); + { + INFO("the module folders: the scripter's, and the alias's under its name"); + REQUIRE(folders.size() == 2); + REQUIRE(folders[0].first.empty()); + CHECK(folders[1].first == "@lib/"); + } // A script in the world: the world's configurations, Pending while // the object has not said; above them the one at the top of the // scripter's folders. world.configsPending = true; - ensure("the object not said", search.configsFor(asking(true).self, asking(true), where(true, true, { s.at("project") }), nullptr, false, chain) == + UNSCOPED_INFO("the object not said"); + CHECK(search.configsFor(asking(true).self, asking(true), where(true, true, { s.at("project") }), nullptr, false, chain) == ALPreprocessor::Found::Pending); world.configsPending = false; - ensure("the top of the scripter's folder", search.configsFor(asking(true).self, asking(true), where(true, true, { s.at("project") }), nullptr, false, - chain) == ALPreprocessor::Found::Yes && - chain.size() == 1); + { + INFO("the top of the scripter's folder"); + CHECK(search.configsFor(asking(true).self, asking(true), where(true, true, { s.at("project") }), nullptr, false, + chain) == ALPreprocessor::Found::Yes); + REQUIRE(chain.size() == 1); + } } - template<> template<> - void alincludesearch_object::test<4>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: every file a text includes and theirs in turn each once", "[alincludesearch]") { - set_test_name("every file a text includes, and theirs in turn, each once"); Scratch s; const std::string a = s.write("inc/a.lsl", "#include \"b.lsl\"\ninteger a;\n"); const std::string b = s.write("inc/b.lsl", "#include \"a.lsl\"\ninteger b;\n"); const std::vector found = search.includedBy("#include \"a.lsl\"\n#include \ndefault { state_entry() { } }\n", asking(), where(false, true, { s.at("inc") })); - ensure_equals("both, once", found.size(), size_t(2)); - ensure("in the order met", found[0].path == ALIncludeIdentity::ofFile(a) && found[1].path == ALIncludeIdentity::ofFile(b)); - ensure("names of an include", ALIncludeSearch::itemNameOf("./lib/util.lsl") == "util.lsl" && - ALIncludeSearch::foldersOf("../lib/util.lsl") == std::vector({ "..", "lib" })); + UNSCOPED_INFO("both, once"); + REQUIRE(found.size() == size_t(2)); + { + INFO("in the order met"); + CHECK(found[0].path == ALIncludeIdentity::ofFile(a)); + CHECK(found[1].path == ALIncludeIdentity::ofFile(b)); + } + { + INFO("names of an include"); + CHECK(ALIncludeSearch::itemNameOf("./lib/util.lsl") == "util.lsl"); + CHECK(ALIncludeSearch::foldersOf("../lib/util.lsl") == std::vector({ "..", "lib" })); + } } - template<> template<> - void alincludesearch_object::test<5>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: explainMissing: the object's silence first then the world and the disk", "[alincludesearch]") { - set_test_name("a name not found said as why: the object's silence first, one in the world that is not taken from, the disk not looked in"); const auto missing = [](const char* key, const char* name, const std::string& file = std::string()) { ALScriptProblem p; p.key = key; @@ -372,23 +423,27 @@ namespace tut facts.noFolders = false; facts.inWorld = [](const std::string& name) { return name == "lib.lsl"; }; ALIncludeSearch::explainMissing(problems, facts); - ensure_equals("the silence said first", problems.front().key, std::string("PreprocObjectUnanswered")); - ensure_equals("one in the world, not taken from", problems[1].key, std::string("PreprocIncludeInWorld")); - ensure_equals("one nowhere, the disk looked in: as it was", problems[2].key, std::string("PreprocModuleNotFound")); + UNSCOPED_INFO("the silence said first"); + CHECK(problems.front().key == std::string("PreprocObjectUnanswered")); + UNSCOPED_INFO("one in the world, not taken from"); + CHECK(problems[1].key == std::string("PreprocIncludeInWorld")); + UNSCOPED_INFO("one nowhere, the disk looked in: as it was"); + CHECK(problems[2].key == std::string("PreprocModuleNotFound")); problems = { missing("PreprocModuleNotFound", "util"), missing("PreprocIncludeNotFound", "x.lsl", "disk:/lib/a.lsl") }; facts = ALIncludeSearch::Missing(); facts.disk = true; facts.noFolders = true; ALIncludeSearch::explainMissing(problems, facts); - ensure_equals("no folder of the scripter's: the disk not looked in", problems[0].key, std::string("PreprocModuleNotOnDisk")); - ensure_equals("but one asked from a file on disk is not second-guessed", problems[1].key, std::string("PreprocIncludeNotFound")); - ensure("said with its name", problems[0].message.find("'util'") != std::string::npos); + UNSCOPED_INFO("no folder of the scripter's: the disk not looked in"); + CHECK(problems[0].key == std::string("PreprocModuleNotOnDisk")); + UNSCOPED_INFO("but one asked from a file on disk is not second-guessed"); + CHECK(problems[1].key == std::string("PreprocIncludeNotFound")); + UNSCOPED_INFO("said with its name"); + REQUIRE(problems[0].message.find("'util'") != std::string::npos); } - template<> template<> - void alincludesearch_object::test<6>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: leftOut: the names once in order and the problems and the disk's route", "[alincludesearch]") { - set_test_name("what a run left out: the names not found each once in order, the problems that said so, and whether a folder on disk would have let one in"); const auto said = [](const char* key, const char* name) { ALScriptProblem p; p.key = key; @@ -403,15 +458,27 @@ namespace tut said("PreprocSomethingElse", "c.lsl"), said("PreprocModuleNotFound", "util"), said("PreprocIncludeNotFound", "b.lsl") }; ALIncludeSearch::LeftOut left = ALIncludeSearch::leftOut(problems); - ensure("each once, in order", left.names == std::vector{ "b.lsl", "util" }); - ensure_equals("every problem that said so", left.problems, 3); - ensure("found nowhere: nothing a folder would change", !left.diskRoute); + UNSCOPED_INFO("each once, in order"); + CHECK(left.names == std::vector{ "b.lsl", "util" }); + UNSCOPED_INFO("every problem that said so"); + CHECK(left.problems == 3); + UNSCOPED_INFO("found nowhere: nothing a folder would change"); + CHECK_FALSE(left.diskRoute); left = ALIncludeSearch::leftOut({ said("PreprocIncludeInWorld", "a.lsl") }); - ensure("one in the world, which is not taken from: the disk's route", left.diskRoute && left.names.size() == 1); + { + INFO("one in the world, which is not taken from: the disk's route"); + CHECK(left.diskRoute); + REQUIRE(left.names.size() == 1); + } left = ALIncludeSearch::leftOut({ said("PreprocModuleNotOnDisk", "m") }); - ensure("the disk not looked in: the disk's route", left.diskRoute && left.names == std::vector{ "m" }); - ensure("nothing said: nothing left out", ALIncludeSearch::leftOut({}).names.empty()); + { + INFO("the disk not looked in: the disk's route"); + CHECK(left.diskRoute); + CHECK(left.names == std::vector{ "m" }); + } + UNSCOPED_INFO("nothing said: nothing left out"); + REQUIRE(ALIncludeSearch::leftOut({}).names.empty()); } // The require parity suite (LA13): what a SLua require finds on disk, @@ -449,10 +516,8 @@ namespace tut }; } // anonymous namespace - template<> template<> - void alincludesearch_object::test<7>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: the require parity suite: Luau's and the plugin's cases as LAD9 picks", "[alincludesearch]") { - set_test_name("the require parity suite: each case from Luau's tests, the plugin's and ours, as LAD9 picks, beside what the search found before"); Scratch s; // Luau's without_config, in lw/. s.write("proj/lw/dependency.luau", "return 'dependency'\n"); @@ -587,46 +652,63 @@ namespace tut { under = under_tree(file); } - ensure_equals(std::string(one.what) + ": as LAD9 has it", under, std::string(one.chosen)); + INFO(one.what); + REQUIRE(under == std::string(one.chosen)); if (*one.name != '@') { // A name the search before took: asked of it. const std::string before = searched(one.name, own, disk); - ensure_equals(std::string(one.what) + ": as before", before.empty() ? before : under_tree(before), std::string(one.before)); + REQUIRE((before.empty() ? before : under_tree(before)) == std::string(one.before)); } else if (!*one.rule) { // An alias's, where all agree: found so before. - ensure_equals(std::string(one.what) + ": as before", under, std::string(one.before)); + REQUIRE(under == std::string(one.before)); } } } - template<> template<> - void alincludesearch_object::test<8>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: navigatorPath: ./ for no prefix; @self; @sl- reserved; no alias climbs", "[alincludesearch]") { - set_test_name("a require's path in the navigator's terms: no prefix is ./; @self from beside the file; @sl-* reserved; an alias may not climb"); const auto path = [](const std::string& name, const std::string& stem = "main") { return ALRequireNavigation::navigatorPath(name, stem); }; - ensure_equals("no prefix", path("util").path, std::string("./util")); - ensure_equals("a folder in it", path("lib/util").path, std::string("./lib/util")); - ensure_equals("./ as it is", path("./util").path, std::string("./util")); - ensure_equals("../ as it is", path("../util").path, std::string("../util")); - ensure_equals("a separator of Windows's", path("lib\\util").path, std::string("./lib/util")); - ensure_equals("an alias as it is", path("@lib/util").path, std::string("@lib/util")); - ensure_equals("@self from a file: the folder of its name", path("@self/child").path, std::string("./main/child")); - ensure_equals("@self from a folder's init: beside it", path("@self/child", "init").path, std::string("./child")); - ensure_equals("@self alone", path("@self").path, std::string("./main")); - ensure_equals("@SELF in any case", path("@SELF/child", "init").path, std::string("./child")); - ensure("@sl-* reserved", !path("@sl-std/util").error.empty() && !path("@SL-std").error.empty()); - ensure("an alias may not climb", !path("@lib/../secret").error.empty() && !path("@lib/a/../../b").error.empty()); - ensure("@self may, as Luau's own", path("@self/../x").error.empty()); - ensure("a name with .. in it is no climb", path("@lib/a..b").error.empty()); + UNSCOPED_INFO("no prefix"); + CHECK(path("util").path == std::string("./util")); + UNSCOPED_INFO("a folder in it"); + CHECK(path("lib/util").path == std::string("./lib/util")); + UNSCOPED_INFO("./ as it is"); + CHECK(path("./util").path == std::string("./util")); + UNSCOPED_INFO("../ as it is"); + CHECK(path("../util").path == std::string("../util")); + UNSCOPED_INFO("a separator of Windows's"); + CHECK(path("lib\\util").path == std::string("./lib/util")); + UNSCOPED_INFO("an alias as it is"); + CHECK(path("@lib/util").path == std::string("@lib/util")); + UNSCOPED_INFO("@self from a file: the folder of its name"); + CHECK(path("@self/child").path == std::string("./main/child")); + UNSCOPED_INFO("@self from a folder's init: beside it"); + CHECK(path("@self/child", "init").path == std::string("./child")); + UNSCOPED_INFO("@self alone"); + CHECK(path("@self").path == std::string("./main")); + UNSCOPED_INFO("@SELF in any case"); + CHECK(path("@SELF/child", "init").path == std::string("./child")); + { + INFO("@sl-* reserved"); + REQUIRE_FALSE(path("@sl-std/util").error.empty()); + REQUIRE_FALSE(path("@SL-std").error.empty()); + } + { + INFO("an alias may not climb"); + REQUIRE_FALSE(path("@lib/../secret").error.empty()); + REQUIRE_FALSE(path("@lib/a/../../b").error.empty()); + } + UNSCOPED_INFO("@self may, as Luau's own"); + REQUIRE(path("@self/../x").error.empty()); + UNSCOPED_INFO("a name with .. in it is no climb"); + REQUIRE(path("@lib/a..b").error.empty()); } - template<> template<> - void alincludesearch_object::test<9>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: a require walked through the world; Pending where it has not said", "[alincludesearch]") { - set_test_name("a require walked through the world: an inventory folder's items and folders, its .luaurc notecard, an object's contents; Pending where the world has not said; nothing of it with world includes off but the scripter's folders' own .luaurc"); // The inventory: a project folder in a root, with a library folder, // a module's folder with its init, and a configuration. const auto add = [this](const std::string& folder, const std::string& name) { @@ -664,35 +746,50 @@ namespace tut const ALPreprocessor::Found said = search.resolve(alincludesearch_data::ask(name, true, own.self), include, own, in, wanted, false, &aliases); return said == ALPreprocessor::Found::Yes ? include.path : said == ALPreprocessor::Found::Pending ? std::string("pending") : std::string(); }; - ensure_equals("beside it", found("./util", on), util.path); - ensure_equals("with no prefix", found("util", on), util.path); - ensure_equals("up a folder", found("../shared", on), shared.path); - ensure_equals("a folder's init", found("./mod", on), init.path); - ensure_equals("in a folder", found("./lib/net", on), net.path); - ensure_equals("through the folder's .luaurc", found("@lib/net", on), net.path); - ensure_equals("one there is not", found("./nothere", on), std::string()); - ensure_equals("a world .luaurc names nothing on disk", found("@far/x", on), std::string()); - ensure_equals("past the world's top, the scripter's folders' own .luaurc", found("@top/x", on), ALIncludeIdentity::ofFile(x)); + UNSCOPED_INFO("beside it"); + CHECK(found("./util", on) == util.path); + UNSCOPED_INFO("with no prefix"); + CHECK(found("util", on) == util.path); + UNSCOPED_INFO("up a folder"); + CHECK(found("../shared", on) == shared.path); + UNSCOPED_INFO("a folder's init"); + CHECK(found("./mod", on) == init.path); + UNSCOPED_INFO("in a folder"); + CHECK(found("./lib/net", on) == net.path); + UNSCOPED_INFO("through the folder's .luaurc"); + CHECK(found("@lib/net", on) == net.path); + UNSCOPED_INFO("one there is not"); + CHECK(found("./nothere", on) == std::string()); + UNSCOPED_INFO("a world .luaurc names nothing on disk"); + CHECK(found("@far/x", on) == std::string()); + UNSCOPED_INFO("past the world's top, the scripter's folders' own .luaurc"); + CHECK(found("@top/x", on) == ALIncludeIdentity::ofFile(x)); // What the world has not said yet: Pending, and what is on its way // wanted. ALIncludeSearch::wanted_t wanted; world.folders["lib"].known = false; - ensure_equals("a folder not known: Pending", found("./lib/net", on, &wanted), std::string("pending")); + UNSCOPED_INFO("a folder not known: Pending"); + CHECK(found("./lib/net", on, &wanted) == std::string("pending")); world.folders["lib"].known = true; ALScriptTextCache fresh; ALIncludeSearch unread(fresh, world); ALPreprocessor::Include include; std::vector aliases; - ensure("the .luaurc not in hand: Pending", unread.resolve(ask("@lib/net", true, own.self), include, own, on, &wanted, false, &aliases) == + UNSCOPED_INFO("the .luaurc not in hand: Pending"); + CHECK(unread.resolve(ask("@lib/net", true, own.self), include, own, on, &wanted, false, &aliases) == ALPreprocessor::Found::Pending); - ensure("and wanted", wanted.contains(config.path)); + UNSCOPED_INFO("and wanted"); + CHECK(wanted.contains(config.path)); // With world includes off, nothing of the world; the scripter's // folders' own .luaurc still. - ensure_equals("off: not beside it", found("./util", off), std::string()); - ensure_equals("off: no .luaurc of the world's", found("@lib/net", off), std::string()); - ensure_equals("off: the scripter's folders' own", found("@top/x", off), ALIncludeIdentity::ofFile(x)); + UNSCOPED_INFO("off: not beside it"); + CHECK(found("./util", off) == std::string()); + UNSCOPED_INFO("off: no .luaurc of the world's"); + CHECK(found("@lib/net", off) == std::string()); + UNSCOPED_INFO("off: the scripter's folders' own"); + CHECK(found("@top/x", off) == ALIncludeIdentity::ofFile(x)); // An object's contents, one folder. world.folders["contents"].parent = std::string(); @@ -701,17 +798,19 @@ namespace tut texts.put(helper.path, helper.assetId, "return 'helper'\n"); const ALIncludeSearch::Asking in_object{ script.path, true }; ALPreprocessor::Include got; - ensure("an object's: beside it", search.resolve(ask("./helper", true, script.path), got, in_object, on, nullptr, false, &aliases) == - ALPreprocessor::Found::Yes && - got.path == helper.path); - ensure("an object has no folder above", search.resolve(ask("../helper", true, script.path), got, in_object, on, nullptr, false, &aliases) == + { + INFO("an object's: beside it"); + CHECK(search.resolve(ask("./helper", true, script.path), got, in_object, on, nullptr, false, &aliases) == + ALPreprocessor::Found::Yes); + CHECK(got.path == helper.path); + } + UNSCOPED_INFO("an object has no folder above"); + CHECK(search.resolve(ask("../helper", true, script.path), got, in_object, on, nullptr, false, &aliases) == ALPreprocessor::Found::No); } - template<> template<> - void alincludesearch_object::test<10>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: studio aliases reach the disk from an object; a project's .luaurc wins", "[alincludesearch]") { - set_test_name("studio aliases: a script in an object reaches a library on disk through one, world includes off; a project's own .luaurc wins; naming blesses only for a require"); Scratch s; const std::string util = s.write("library/util.luau", "return 'util'\n"); const std::string helper = s.write("library/helper.lsl", "integer helper;\n"); @@ -729,32 +828,37 @@ namespace tut return search.resolve(alincludesearch_data::ask(name, true, who.self), include, who, in, nullptr, false, &aliases) == ALPreprocessor::Found::Yes ? include.path : std::string(); }; - ensure_equals("an object's script, through the studio's alias", found("@lib/util", in_object, disk), ALIncludeIdentity::ofFile(util)); - ensure_equals("in any case", found("@LIB/util", in_object, disk), ALIncludeIdentity::ofFile(util)); - ensure_equals("one nobody named", found("@other/util", in_object, disk), std::string()); + UNSCOPED_INFO("an object's script, through the studio's alias"); + CHECK(found("@lib/util", in_object, disk) == ALIncludeIdentity::ofFile(util)); + UNSCOPED_INFO("in any case"); + CHECK(found("@LIB/util", in_object, disk) == ALIncludeIdentity::ofFile(util)); + UNSCOPED_INFO("one nobody named"); + CHECK(found("@other/util", in_object, disk) == std::string()); ALIncludeSearch::Where unnamed = disk; unnamed.aliases.clear(); - ensure_equals("unnamed: not read, the folder blessed by nothing", found("@lib/util", in_object, unnamed), std::string()); + UNSCOPED_INFO("unnamed: not read, the folder blessed by nothing"); + CHECK(found("@lib/util", in_object, unnamed) == std::string()); ALIncludeSearch::Where off = disk; off.disk = false; - ensure_equals("the disk off: nothing", found("@lib/util", in_object, off), std::string()); + UNSCOPED_INFO("the disk off: nothing"); + CHECK(found("@lib/util", in_object, off) == std::string()); // A project's own `.luaurc` names the alias first. const ALIncludeSearch::Asking from_disk{ ALIncludeIdentity::ofFile(script), true }; - ensure_equals("the project's own alias", found("@shared/util", from_disk, disk), ALIncludeIdentity::ofFile(own)); - ensure_equals("where it names none, the studio's", found("@shared/util", in_object, disk), ALIncludeIdentity::ofFile(util)); + UNSCOPED_INFO("the project's own alias"); + CHECK(found("@shared/util", from_disk, disk) == ALIncludeIdentity::ofFile(own)); + UNSCOPED_INFO("where it names none, the studio's"); + CHECK(found("@shared/util", in_object, disk) == ALIncludeIdentity::ofFile(util)); // Naming blesses the folder for a require, and adds nothing to an // include's search. ALPreprocessor::Include include; - ensure("an LSL include does not search an alias's folder", - search.resolve(ask("helper.lsl"), include, asking(), disk, nullptr, false) == ALPreprocessor::Found::No); + UNSCOPED_INFO("an LSL include does not search an alias's folder"); + CHECK(search.resolve(ask("helper.lsl"), include, asking(), disk, nullptr, false) == ALPreprocessor::Found::No); } - template<> template<> - void alincludesearch_object::test<11>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: a require only the old search found: where and each way to say it now", "[alincludesearch]") { - set_test_name("a require the search found before, and finds nothing now: where it found it, and each way to say it now that finds that very file"); Scratch s; const std::string script = s.write("proj/main.luau", "return require('util')\n"); s.write("proj/.luaurc", "{\"aliases\": {\"shared\": \"../inc\"}}"); @@ -768,7 +872,8 @@ namespace tut const auto missed = [&](const std::string& name, const ALIncludeSearch::Asking& who) { ALPreprocessor::Include include; std::vector aliases; - ensure(name + ": found nothing now", search.resolve(alincludesearch_data::ask(name, true, who.self), include, who, disk, nullptr, false, &aliases) == + CAPTURE(name); + REQUIRE(search.resolve(alincludesearch_data::ask(name, true, who.self), include, who, disk, nullptr, false, &aliases) == ALPreprocessor::Found::No); return include; }; @@ -782,32 +887,47 @@ namespace tut }; ALPreprocessor::Include include = missed("util2", from_disk); - ensure_equals("found under the file's own folder", include.searched, under); - ensure_equals("written from beside it", said(include), std::string("./lib/util2")); + UNSCOPED_INFO("found under the file's own folder"); + CHECK(include.searched == under); + UNSCOPED_INFO("written from beside it"); + CHECK(said(include) == std::string("./lib/util2")); include = missed("util", from_disk); - ensure_equals("found in another include folder", include.searched, util); - ensure_equals("through the .luaurc's alias that reaches it", said(include), std::string("@shared/util")); + UNSCOPED_INFO("found in another include folder"); + CHECK(include.searched == util); + UNSCOPED_INFO("through the .luaurc's alias that reaches it"); + CHECK(said(include) == std::string("@shared/util")); include = missed("util", in_object); - ensure_equals("from an object: through the alias of the .luaurc at the top of an include folder", said(include), std::string("@shared/util")); + UNSCOPED_INFO("from an object: through the alias of the .luaurc at the top of an include folder"); + CHECK(said(include) == std::string("@shared/util")); include = missed("solo", in_object); - ensure_equals("from an object, where none reaches it", include.searched, solo); - ensure_equals("through a studio alias of the folder that holds it, named after it", said(include), std::string("@other/solo +@other")); - ensure_equals("the folder named", include.moves[0].folder, s.at("other")); + UNSCOPED_INFO("from an object, where none reaches it"); + CHECK(include.searched == solo); + UNSCOPED_INFO("through a studio alias of the folder that holds it, named after it"); + REQUIRE(said(include) == std::string("@other/solo +@other")); + UNSCOPED_INFO("the folder named"); + CHECK(include.moves[0].folder == s.at("other")); include = missed("deep/net", in_object); - ensure("a folder in it", include.searched == deep && said(include) == "@other/deep/net +@other"); + { + INFO("a folder in it"); + CHECK(include.searched == deep); + CHECK(said(include) == "@other/deep/net +@other"); + } include = missed("nowhere", from_disk); - ensure("nowhere before either", include.searched.empty() && include.moves.empty()); + { + INFO("nowhere before either"); + REQUIRE(include.searched.empty()); + REQUIRE(include.moves.empty()); + } include = missed("@missing/util", from_disk); - ensure("an alias's is no searched name", include.searched.empty()); + UNSCOPED_INFO("an alias's is no searched name"); + REQUIRE(include.searched.empty()); } - template<> template<> - void alincludesearch_object::test<12>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: a file beside a folder's init of its name is taken and the init said", "[alincludesearch]") { - set_test_name("a file and a folder's init of one name: the file taken, the init passed over said; either alone, nothing to say"); Scratch s; const std::string script = s.write("proj/main.luau", "return require('./both')\n"); const std::string file = s.write("proj/both.luau", "return 'file'\n"); @@ -817,24 +937,29 @@ namespace tut const ALIncludeSearch::Asking own{ ALIncludeIdentity::ofFile(script), true }; ALPreprocessor::Include found; std::vector aliases; - ensure("found", search.resolve(ask("./both", true, own.self), found, own, disk, nullptr, false, &aliases) == ALPreprocessor::Found::Yes); - ensure_equals("the file", found.path, ALIncludeIdentity::ofFile(file)); - ensure_equals("the init passed over, by its path from the folder both are in", found.passedOver, std::string("both/init.luau")); - ensure_equals("as the file taken is, by its name", found.name, std::string("both.luau")); + CHECK(search.resolve(ask("./both", true, own.self), found, own, disk, nullptr, false, &aliases) == ALPreprocessor::Found::Yes); + CHECK(found.path == ALIncludeIdentity::ofFile(file)); + UNSCOPED_INFO("the init passed over, by its path from the folder both are in"); + CHECK(found.passedOver == std::string("both/init.luau")); + UNSCOPED_INFO("as the file taken is, by its name"); + CHECK(found.name == std::string("both.luau")); ALPreprocessor::Include alone; - ensure("a folder's init alone", search.resolve(ask("./alone", true, own.self), alone, own, disk, nullptr, false, &aliases) == - ALPreprocessor::Found::Yes && - alone.passedOver.empty()); + { + INFO("a folder's init alone"); + CHECK(search.resolve(ask("./alone", true, own.self), alone, own, disk, nullptr, false, &aliases) == + ALPreprocessor::Found::Yes); + REQUIRE(alone.passedOver.empty()); + } ALPreprocessor::Include named; - ensure("named with its extension: the file alone, nothing passed over", - search.resolve(ask("./both.luau", true, own.self), named, own, disk, nullptr, false, &aliases) == ALPreprocessor::Found::Yes && - named.passedOver.empty()); + { + INFO("named with its extension: the file alone, nothing passed over"); + CHECK(search.resolve(ask("./both.luau", true, own.self), named, own, disk, nullptr, false, &aliases) == ALPreprocessor::Found::Yes); + REQUIRE(named.passedOver.empty()); + } } - template<> template<> - void alincludesearch_object::test<13>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: why a require found nothing: Luau's navigator's words or studio rules", "[alincludesearch]") { - set_test_name("why a require found nothing, in Luau's navigator's words or the studio's rules'; said as a module not found, and as the disk not looked in where it was not"); Scratch s; const std::string script = s.write("proj/main.luau", "return require('./nowhere')\n"); s.write("proj/.luaurc", "{\"aliases\": {\"gone\": \"./gone\", \"c1\": \"@c2\", \"c2\": \"@c1\", \"lib\": \"./lib\"}}"); @@ -846,12 +971,16 @@ namespace tut search.resolve(alincludesearch_data::ask(name, true, own.self), found, own, disk, nullptr, false, &aliases); return found.why; }; - ensure_equals("nothing so named", why("./nowhere"), std::string("could not resolve child component \"nowhere\"")); - ensure_equals("an alias nobody names", why("@missing/x"), std::string("@missing is not a valid alias")); - ensure_equals("aliases in a circle", why("@c1"), std::string("detected alias cycle (@c1 -> @c2 -> @c1)")); - ensure_equals("an alias whose folder is not there", why("@gone/x"), std::string("the alias stands for './gone', which is not there")); - ensure("reserved", why("@sl-std/x").find("reserved") != std::string::npos); - ensure("climbing", why("@lib/../x").find("may not climb") != std::string::npos); + UNSCOPED_INFO("nothing so named"); + CHECK(why("./nowhere") == std::string("could not resolve child component \"nowhere\"")); + UNSCOPED_INFO("an alias nobody names"); + CHECK(why("@missing/x") == std::string("@missing is not a valid alias")); + UNSCOPED_INFO("aliases in a circle"); + CHECK(why("@c1") == std::string("detected alias cycle (@c1 -> @c2 -> @c1)")); + UNSCOPED_INFO("an alias whose folder is not there"); + CHECK(why("@gone/x") == std::string("the alias stands for './gone', which is not there")); + REQUIRE(why("@sl-std/x").find("reserved") != std::string::npos); + REQUIRE(why("@lib/../x").find("may not climb") != std::string::npos); // Said as a module not found, for what explains it. ALScriptProblem said; @@ -861,14 +990,17 @@ namespace tut ALIncludeSearch::Missing facts; facts.disk = false; ALIncludeSearch::explainMissing(problems, facts); - ensure("the disk not looked in", problems[0].key == "PreprocModuleNotOnDisk" && problems[0].args == std::vector{ "./nowhere" }); - ensure("left out", ALIncludeSearch::leftOut({ said }).names == std::vector{ "./nowhere" }); + { + INFO("the disk not looked in"); + CHECK(problems[0].key == "PreprocModuleNotOnDisk"); + CHECK(problems[0].args == std::vector{ "./nowhere" }); + } + UNSCOPED_INFO("left out"); + CHECK(ALIncludeSearch::leftOut({ said }).names == std::vector{ "./nowhere" }); } - template<> template<> - void alincludesearch_object::test<14>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: suggestions as a require is typed: aliases first; each module resolves", "[alincludesearch]") { - set_test_name("paths suggested as a require is typed, by Luau's own suggester over the places: aliases, ./ and ../ first, then a folder's modules and folders; every module offered resolves; an include's by its search"); Scratch s; const std::string script = s.write("proj/main.luau", "return require('./util')\n"); s.write("proj/util.luau", "return 1\n"); @@ -892,15 +1024,24 @@ namespace tut } return out; }; - ensure_equals("first: the aliases in reach, none reserved, then ./ and ../", labels(""), std::string("@shared/ @tools/ .// ..//")); - ensure_equals("the file's own folder: modules and folders, none hidden, nothing else", labels("./"), std::string("../ lib/ main util")); - ensure_equals("a name part typed: the same, for the editor to narrow", labels("./li"), labels("./")); - ensure_equals("in a folder: its own, and up", labels("./lib/"), std::string("../ http net sub/")); - ensure_equals("a name part typed there", labels("./lib/ne"), labels("./lib/")); - ensure_equals("through a .luaurc's alias, which may not climb: no way up", labels("@shared/"), std::string("http net sub/")); - ensure_equals("nor further in", labels("@shared/sub/"), std::string("deep")); - ensure_equals("through the studio's", labels("@tools/"), std::string("t")); - ensure_equals("a reserved alias walks nowhere", labels("@sl-std/"), std::string()); + UNSCOPED_INFO("first: the aliases in reach, none reserved, then ./ and ../"); + CHECK(labels("") == std::string("@shared/ @tools/ .// ..//")); + UNSCOPED_INFO("the file's own folder: modules and folders, none hidden, nothing else"); + CHECK(labels("./") == std::string("../ lib/ main util")); + UNSCOPED_INFO("a name part typed: the same, for the editor to narrow"); + CHECK(labels("./li") == labels("./")); + UNSCOPED_INFO("in a folder: its own, and up"); + CHECK(labels("./lib/") == std::string("../ http net sub/")); + UNSCOPED_INFO("a name part typed there"); + CHECK(labels("./lib/ne") == labels("./lib/")); + UNSCOPED_INFO("through a .luaurc's alias, which may not climb: no way up"); + CHECK(labels("@shared/") == std::string("http net sub/")); + UNSCOPED_INFO("nor further in"); + CHECK(labels("@shared/sub/") == std::string("deep")); + UNSCOPED_INFO("through the studio's"); + CHECK(labels("@tools/") == std::string("t")); + UNSCOPED_INFO("a reserved alias walks nowhere"); + CHECK(labels("@sl-std/") == std::string()); // Every module offered is what a require of it finds. for (const std::string& typed : { std::string("./"), std::string("./lib/"), std::string("./lib/sub/"), std::string("@shared/"), std::string("@tools/") }) @@ -913,11 +1054,13 @@ namespace tut } ALPreprocessor::Include found; std::vector aliases; - ensure("resolves: " + one.path, search.resolve(ask(one.path, true, own.self), found, own, disk, nullptr, false, &aliases) == + CAPTURE(one.path); + REQUIRE(search.resolve(ask(one.path, true, own.self), found, own, disk, nullptr, false, &aliases) == ALPreprocessor::Found::Yes); } } - ensure_equals("each one's whole path", search.suggest(own.self, "./lib/", true, own, disk)[1].path, std::string("./lib/http")); + UNSCOPED_INFO("each one's whole path"); + CHECK(search.suggest(own.self, "./lib/", true, own, disk)[1].path == std::string("./lib/http")); // An include's: the file's own folder, by its search, names whole -- // an include's own `.lslh` and `.lsli` as well as `.lsl`. @@ -930,20 +1073,23 @@ namespace tut } return said; }; - ensure_equals("an LSL include's", included(""), std::string("defs.lslh inc.lsl lib/")); - ensure_equals("in a folder under it", included("lib/"), std::string("lib/sub/")); + UNSCOPED_INFO("an LSL include's"); + CHECK(included("") == std::string("defs.lslh inc.lsl lib/")); + UNSCOPED_INFO("in a folder under it"); + CHECK(included("lib/") == std::string("lib/sub/")); // Only under the folders the search looks in, however it is typed: // not up out of them, nor from a root elsewhere, nor another // machine's share. - ensure_equals("not up out of them", included("../"), std::string()); - ensure_equals("nor from a root elsewhere", included(fsyspath(s.root).string() + "/"), std::string()); - ensure_equals("nor a share", included("\\\\alincludesearch.invalid\\share\\"), std::string()); + UNSCOPED_INFO("not up out of them"); + CHECK(included("../") == std::string()); + UNSCOPED_INFO("nor from a root elsewhere"); + CHECK(included(fsyspath(s.root).string() + "/") == std::string()); + UNSCOPED_INFO("nor a share"); + CHECK(included("\\\\alincludesearch.invalid\\share\\") == std::string()); } - template<> template<> - void alincludesearch_object::test<15>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: a .config.luau is read as the .luaurc it returns; beside one neither", "[alincludesearch]") { - set_test_name("a .config.luau on disk is read as the .luaurc it says: its aliases walked and suggested, its mode the analyzer's, in the chain as Luau reads one; a folder with both reads as neither; one that fails is passed over; at the top of the scripter's folder for a script in the world"); Scratch s; const std::string script = s.write("proj/src/main.luau", "return require('@lib/net')\n"); const std::string net = s.write("proj/modules/net.luau", "return 1\n"); @@ -988,24 +1134,41 @@ namespace tut }; ALLuauConfig config; - ensure_equals("through its alias", resolved("@lib/net"), ALIncludeIdentity::ofFile(net)); - ensure_equals("in the chain by its own name", chain(), std::string(".config.luau")); - ensure("its mode the analyzer's", search.configOf(own, disk(), config) && config.mode == "strict"); - ensure_equals("its alias suggested", suggested(), std::string("@lib ./ ../")); + UNSCOPED_INFO("through its alias"); + CHECK(resolved("@lib/net") == ALIncludeIdentity::ofFile(net)); + UNSCOPED_INFO("in the chain by its own name"); + CHECK(chain() == std::string(".config.luau")); + { + INFO("its mode the analyzer's"); + REQUIRE(search.configOf(own, disk(), config)); + CHECK(config.mode == "strict"); + } + UNSCOPED_INFO("its alias suggested"); + CHECK(suggested() == std::string("@lib ./ ../")); // A nearer .luaurc over it, as Luau reads a chain. s.write("proj/src/.luaurc", "{\"languageMode\": \"nonstrict\"}"); - ensure_equals("both, the nearer first", chain(), std::string(".luaurc .config.luau")); - ensure("the nearer's mode", search.configOf(own, disk(), config) && config.mode == "nonstrict"); - ensure_equals("the further one's alias still", resolved("@lib/net"), ALIncludeIdentity::ofFile(net)); + UNSCOPED_INFO("both, the nearer first"); + CHECK(chain() == std::string(".luaurc .config.luau")); + { + INFO("the nearer's mode"); + CHECK(search.configOf(own, disk(), config)); + CHECK(config.mode == "nonstrict"); + } + UNSCOPED_INFO("the further one's alias still"); + CHECK(resolved("@lib/net") == ALIncludeIdentity::ofFile(net)); // Both in one folder: neither, as Luau reads it. const std::string rc = s.write("proj/.luaurc", "{\"aliases\": {\"lib\": \"./modules\"}}"); std::string why; - ensure("both: not found", resolved("@lib/net", &why).empty()); - ensure_equals("in Luau's words", why, std::string("could not resolve alias \"lib\" (ambiguous configuration file)")); - ensure_equals("passed over in the chain", chain(), std::string(".luaurc")); - ensure_equals("nor suggested", suggested(), std::string("./ ../")); + UNSCOPED_INFO("both: not found"); + REQUIRE(resolved("@lib/net", &why).empty()); + UNSCOPED_INFO("in Luau's words"); + CHECK(why == std::string("could not resolve alias \"lib\" (ambiguous configuration file)")); + UNSCOPED_INFO("passed over in the chain"); + CHECK(chain() == std::string(".luaurc")); + UNSCOPED_INFO("nor suggested"); + CHECK(suggested() == std::string("./ ../")); std::error_code ec; fs::remove(fsyspath(rc), ec); @@ -1014,10 +1177,17 @@ namespace tut // why; passed over in the chain the analyzer reads, as such a // .luaurc is. s.write("proj/.config.luau", "error('broken')\n"); - ensure("broken: its alias gone", resolved("@lib/net", &why).empty()); - ensure("said why, naming it: " + why, why.find("the configuration '") == 0 && why.find(".config.luau' could not be read: ") != std::string::npos); - ensure_equals("nor suggested past it", suggested(), std::string("./ ../")); - ensure_equals("not in the chain", chain(), std::string(".luaurc")); + UNSCOPED_INFO("broken: its alias gone"); + REQUIRE(resolved("@lib/net", &why).empty()); + { + INFO("said why, naming it: " << why); + CHECK(why.find("the configuration '") == 0); + REQUIRE(why.find(".config.luau' could not be read: ") != std::string::npos); + } + UNSCOPED_INFO("nor suggested past it"); + CHECK(suggested() == std::string("./ ../")); + UNSCOPED_INFO("not in the chain"); + CHECK(chain() == std::string(".luaurc")); // A script in the world, world includes off: the one at the top of // the scripter's include folder governs it. @@ -1026,18 +1196,22 @@ namespace tut const ALIncludeSearch::Where off = where(false, true, { s.at("inc") }); ALPreprocessor::Include found; std::vector aliases; - ensure("a world script, through the top's alias", - search.resolve(ask("@top/x", true, asking(true).self), found, asking(true), off, nullptr, false, &aliases) == ALPreprocessor::Found::Yes && - found.path == ALIncludeIdentity::ofFile(x)); + { + INFO("a world script, through the top's alias"); + CHECK(search.resolve(ask("@top/x", true, asking(true).self), found, asking(true), off, nullptr, false, &aliases) == ALPreprocessor::Found::Yes); + CHECK(found.path == ALIncludeIdentity::ofFile(x)); + } std::vector top; - ensure("and its chain", search.configsFor(asking(true).self, asking(true), off, nullptr, false, top) == ALPreprocessor::Found::Yes && - top.size() == 1 && top[0].text.find("\"top\": \"./libs\"") != std::string::npos); + { + INFO("and its chain"); + CHECK(search.configsFor(asking(true).self, asking(true), off, nullptr, false, top) == ALPreprocessor::Found::Yes); + REQUIRE(top.size() == 1); + REQUIRE(top[0].text.find("\"top\": \"./libs\"") != std::string::npos); + } } - template<> template<> - void alincludesearch_object::test<16>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: a studio alias to an inventory folder is walked only with the world in", "[alincludesearch]") { - set_test_name("a studio alias naming a folder of the world, an inventory folder by its id: walked as the world is while the world is let in, and offered then; nothing of it otherwise, and nothing of it blessed on disk"); // An inventory folder of modules, by the id the world knows it by. const auto add = [this](const std::string& folder, const std::string& name) { const ALIncludeWorld::Item one{ ALIncludeIdentity::ofItem(LLUUID::null, LLUUID::generateNewID()), name, LLUUID::generateNewID() }; @@ -1065,9 +1239,12 @@ namespace tut ? include.path : std::string(); }; - ensure_equals("through the alias, the world let in", found("@inv/util", on), util.path); - ensure_equals("a folder under it", found("@inv/net/http", on), http.path); - ensure_equals("the world not let in: nothing", found("@inv/util", off), std::string()); + UNSCOPED_INFO("through the alias, the world let in"); + CHECK(found("@inv/util", on) == util.path); + UNSCOPED_INFO("a folder under it"); + CHECK(found("@inv/net/http", on) == http.path); + UNSCOPED_INFO("the world not let in: nothing"); + CHECK(found("@inv/util", off) == std::string()); const auto offered = [&](const ALIncludeSearch::Where& in) { std::string out; for (const ALRequireNavigation::Suggestion& one : search.suggest(in_object.self, "", true, in_object, in)) @@ -1076,22 +1253,24 @@ namespace tut } return out; }; - ensure("offered while the world is let in: " + offered(on), offered(on).find("@inv") != std::string::npos); - ensure("not otherwise: " + offered(off), offered(off).find("@inv") == std::string::npos); + UNSCOPED_INFO("offered while the world is let in: " << offered(on)); + REQUIRE(offered(on).find("@inv") != std::string::npos); + UNSCOPED_INFO("not otherwise: " << offered(off)); + REQUIRE(offered(off).find("@inv") == std::string::npos); // Beside a folder on disk, which is read only while the disk is. Scratch s; const std::string disk_util = s.write("library/util.luau", "return 'disk'\n"); ALIncludeSearch::Where both = where(true, true); both.aliases = { { "inv", "libfolder" }, { "lib", s.at("library") } }; - ensure_equals("each its own", found("@lib/util", both), ALIncludeIdentity::ofFile(disk_util)); - ensure_equals("and the world's", found("@inv/util", both), util.path); + UNSCOPED_INFO("each its own"); + CHECK(found("@lib/util", both) == ALIncludeIdentity::ofFile(disk_util)); + UNSCOPED_INFO("and the world's"); + CHECK(found("@inv/util", both) == util.path); } - template<> template<> - void alincludesearch_object::test<17>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: written paths reach only folders let in and never a share or device", "[alincludesearch]") { - set_test_name("a path a script or a configuration wrote is asked of the disk only where it is, as written, under a folder a require reads or the configuration's own: never another machine's share, a device, or another drive by a part of a name"); Scratch s; const std::string script = s.write("proj/main.luau", "return 1\n"); const std::string util = s.write("proj/lib/util.luau", "return 'util'\n"); @@ -1121,8 +1300,10 @@ namespace tut // Opened from the disk, in no include folder: what its configuration // names from a root under its own folder is all it reaches. const ALIncludeSearch::Where loose = where(false, true); - ensure_equals("from a root, under the configuration's own folder", found("@own/util", loose), ALIncludeIdentity::ofFile(util)); - ensure_equals("not a require's own path from a root, which no folder lets in", found(util, loose), std::string()); + UNSCOPED_INFO("from a root, under the configuration's own folder"); + CHECK(found("@own/util", loose) == ALIncludeIdentity::ofFile(util)); + UNSCOPED_INFO("not a require's own path from a root, which no folder lets in"); + CHECK(found(util, loose) == std::string()); const ALIncludeSearch::Where disk = where(false, true, { s.at("proj") }); std::string why; @@ -1131,22 +1312,52 @@ namespace tut const auto outside = [&s](const std::string& named) { return "the configuration in '" + s.at("proj") + "' names '" + named + "', which is outside it and outside the include folders"; }; - ensure_equals("under an include folder", found(util, disk), ALIncludeIdentity::ofFile(util)); - ensure("an alias naming another machine's share: not asked", - found("@share/x", disk, &why).empty() && why == outside("//alincludesearch.invalid/share")); - ensure("nor by the NT namespace's name for one", found("@nt/x", disk, &why).empty() && why == outside("/?\?/UNC/alincludesearch.invalid/share")); - ensure("nor its name for a local folder", found("@ntlocal/util", disk, &why).empty() && why == outside("/?\?/" + lib)); - ensure("a folder that is there, out of every folder it may name: not said to be missing", - found("@abroad/e", disk, &why).empty() && why == outside(abroad)); - ensure("a require naming one", found("//alincludesearch.invalid/share/x", disk, &why).empty() && - why == "could not find '//alincludesearch.invalid/share/x'"); - ensure("in Windows's separators", found("\\\\alincludesearch.invalid\\share\\x", disk).empty() && - found("\\??\\UNC\\alincludesearch.invalid\\share\\x", disk).empty()); - ensure("a device's path to a file under the folder", found("\\\\?\\" + util, disk).empty() && found("\\\\.\\" + util, disk).empty() && - found("\\??\\" + util, disk).empty()); - ensure("another drive's folder by a part of a name", found("./D:/x", disk, &why).empty() && why == "could not resolve child component \"D:\""); + UNSCOPED_INFO("under an include folder"); + CHECK(found(util, disk) == ALIncludeIdentity::ofFile(util)); + { + INFO("an alias naming another machine's share: not asked"); + REQUIRE(found("@share/x", disk, &why).empty()); + CHECK(why == outside("//alincludesearch.invalid/share")); + } + { + INFO("nor by the NT namespace's name for one"); + REQUIRE(found("@nt/x", disk, &why).empty()); + CHECK(why == outside("/?\?/UNC/alincludesearch.invalid/share")); + } + { + INFO("nor its name for a local folder"); + REQUIRE(found("@ntlocal/util", disk, &why).empty()); + CHECK(why == outside("/?\?/" + lib)); + } + { + INFO("a folder that is there, out of every folder it may name: not said to be missing"); + REQUIRE(found("@abroad/e", disk, &why).empty()); + CHECK(why == outside(abroad)); + } + { + INFO("a require naming one"); + REQUIRE(found("//alincludesearch.invalid/share/x", disk, &why).empty()); + CHECK(why == "could not find '//alincludesearch.invalid/share/x'"); + } + { + INFO("in Windows's separators"); + REQUIRE(found("\\\\alincludesearch.invalid\\share\\x", disk).empty()); + REQUIRE(found("\\??\\UNC\\alincludesearch.invalid\\share\\x", disk).empty()); + } + { + INFO("a device's path to a file under the folder"); + REQUIRE(found("\\\\?\\" + util, disk).empty()); + REQUIRE(found("\\\\.\\" + util, disk).empty()); + REQUIRE(found("\\??\\" + util, disk).empty()); + } + { + INFO("another drive's folder by a part of a name"); + REQUIRE(found("./D:/x", disk, &why).empty()); + CHECK(why == "could not resolve child component \"D:\""); + } ALPreprocessor::Include include; - ensure("nor an LSL include naming a share", search.resolve(ask("//alincludesearch.invalid/share/x.lsl"), include, asking(), disk, nullptr, false) == + UNSCOPED_INFO("nor an LSL include naming a share"); + CHECK(search.resolve(ask("//alincludesearch.invalid/share/x.lsl"), include, asking(), disk, nullptr, false) == ALPreprocessor::Found::No); // Nor are those modules' folders, for what a script may name; nor // an alias Second Life keeps. @@ -1155,7 +1366,8 @@ namespace tut { prefixes += prefix + " "; } - ensure_equals("the module folders: the scripter's, and the alias's that may be read", prefixes, std::string(" @own/ ")); + UNSCOPED_INFO("the module folders: the scripter's, and the alias's that may be read"); + CHECK(prefixes == std::string(" @own/ ")); // A configuration's folder reached through a link names its own // folder as it stands, and is let in so. @@ -1167,16 +1379,17 @@ namespace tut const ALIncludeSearch::Asking linked{ through, true }; ALPreprocessor::Include reached; std::vector aliases; - ensure("through a link", search.resolve(ask("@own/util", true, through), reached, linked, loose, nullptr, false, &aliases) == - ALPreprocessor::Found::Yes && - reached.path == ALIncludeIdentity::ofFile(util)); + { + INFO("through a link"); + CHECK(search.resolve(ask("@own/util", true, through), reached, linked, loose, nullptr, false, &aliases) == + ALPreprocessor::Found::Yes); + CHECK(reached.path == ALIncludeIdentity::ofFile(util)); + } } } - template<> template<> - void alincludesearch_object::test<18>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: why a require that reached a file found nothing or stopped at a config", "[alincludesearch]") { - set_test_name("why a require found nothing where the walk reached a file: no folder lets it in, or its configuration may not bless what an alias reached; a configuration that cannot be read ends a walk for an alias past it; a world .luaurc naming the disk"); Scratch s; const std::string loose = s.write("loose/main.luau", "return 1\n"); s.write("loose/sibling.luau", "return 1\n"); @@ -1191,9 +1404,11 @@ namespace tut return said == ALPreprocessor::Found::Yes ? std::string("found") : include.why; }; const ALIncludeSearch::Where disk = where(false, true, { s.at("inc") }); - ensure_equals("a file beside one opened from the disk, in no include folder", why("./sibling", ALIncludeIdentity::ofFile(loose), disk), + UNSCOPED_INFO("a file beside one opened from the disk, in no include folder"); + CHECK(why("./sibling", ALIncludeIdentity::ofFile(loose), disk) == "'" + s.at("loose/sibling.luau") + "' is there, but no include folder or alias lets a require read it"); - ensure_equals("an alias reaching out of its configuration's folder", why("@out/x", ALIncludeIdentity::ofFile(loose), disk), + UNSCOPED_INFO("an alias reaching out of its configuration's folder"); + CHECK(why("@out/x", ALIncludeIdentity::ofFile(loose), disk) == "the configuration in '" + s.at("loose") + "' names '" + s.at("outside") + "', which is outside it and outside the include folders"); @@ -1207,8 +1422,10 @@ namespace tut const std::string inner = ALIncludeIdentity::ofFile(s.write("proj/src/main.luau", "return 1\n")); const ALIncludeSearch::Where both = where(false, true, { s.at("inc"), s.at("proj") }); const std::string said = why("@lib/x", inner, both); - ensure("not reached through it: " + said, said.find("the configuration '" + s.at("proj/src/.luaurc") + "' could not be read: ") == 0); - ensure_equals("a path walks as ever", why("./near", inner, both), std::string("found")); + UNSCOPED_INFO("not reached through it: " << said); + CHECK(said.find("the configuration '" + s.at("proj/src/.luaurc") + "' could not be read: ") == 0); + UNSCOPED_INFO("a path walks as ever"); + CHECK(why("./near", inner, both) == std::string("found")); // A world .luaurc's alias naming the disk: why it is not walked. const ALIncludeWorld::Item main{ ALIncludeIdentity::ofItem(LLUUID::null, LLUUID::generateNewID()), "main", LLUUID::generateNewID() }; @@ -1217,14 +1434,13 @@ namespace tut world.folders["wproj"].items = { main, config }; world.itemFolders[main.path] = "wproj"; texts.put(config.path, config.assetId, "{\"aliases\": {\"far\": \"/somewhere/on/disk\"}}"); - ensure_equals("a world configuration naming the disk", why("@far/x", main.path, where(true, true, { s.at("inc") })), + UNSCOPED_INFO("a world configuration naming the disk"); + CHECK(why("@far/x", main.path, where(true, true, { s.at("inc") })) == std::string("the alias stands for '/somewhere/on/disk', a path on disk, which a configuration in the world may not name")); } - template<> template<> - void alincludesearch_object::test<19>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: suggestions in an object with the world off: the top .luaurc's aliases", "[alincludesearch]") { - set_test_name("paths suggested from a script in no place -- in an object, world includes off -- offer the aliases of the .luaurc at the top of the scripter's include folders, as a require from it walks them; a module and a folder of one name, as @self lays one out, both"); Scratch s; s.write("inc/.luaurc", "{\"aliases\": {\"top\": \"./libs\"}}"); s.write("inc/libs/x.luau", "return 1\n"); @@ -1240,19 +1456,21 @@ namespace tut } return out; }; - ensure_equals("the top's alias, then ./ and ../", labels(""), std::string("@top/ .// ..//")); - ensure_equals("a module and its folder, each", labels("@top/"), std::string("selfish selfish/ x")); - ensure_equals("into the folder", labels("@top/selfish/"), std::string("child")); + UNSCOPED_INFO("the top's alias, then ./ and ../"); + CHECK(labels("") == std::string("@top/ .// ..//")); + UNSCOPED_INFO("a module and its folder, each"); + CHECK(labels("@top/") == std::string("selfish selfish/ x")); + UNSCOPED_INFO("into the folder"); + CHECK(labels("@top/selfish/") == std::string("child")); ALPreprocessor::Include found; std::vector aliases; - ensure("and walked so", search.resolve(ask("@top/selfish/child", true, in_object.self), found, in_object, off, nullptr, false, &aliases) == + UNSCOPED_INFO("and walked so"); + CHECK(search.resolve(ask("@top/selfish/child", true, in_object.self), found, in_object, off, nullptr, false, &aliases) == ALPreprocessor::Found::Yes); } - template<> template<> - void alincludesearch_object::test<20>() + TEST_CASE_METHOD(alincludesearch_data, "alincludesearch: an alias to a module lets in its file alone; one to @sl- walks nowhere", "[alincludesearch]") { - set_test_name("an alias of a .luaurc on disk naming a module lets in its file alone; one naming an alias Second Life keeps walks nowhere; a studio alias offered for an include folder is named past every alias in reach"); Scratch s; const std::string script = s.write("loose/main.luau", "return 1\n"); const std::string json = s.write("loose/vendor/json.luau", "return 'json'\n"); @@ -1274,11 +1492,22 @@ namespace tut return yes ? include.path : std::string(); }; std::string why; - ensure_equals("an alias naming a module", found("@json"), ALIncludeIdentity::ofFile(json)); - ensure("blessed for the run, alone", run.size() == 1 && run[0] == s.at("loose/vendor/json.luau")); - ensure_equals("its file alone, the rest of the run: nothing beside it", found("./vendor/other"), std::string()); - ensure_equals("an alias naming a folder", found("@lib/util"), ALIncludeIdentity::ofFile(util)); - ensure("one naming an alias Second Life keeps: no configuration's", found("@x/util", &why).empty() && why == "@sl-std is not a valid alias"); + UNSCOPED_INFO("an alias naming a module"); + CHECK(found("@json") == ALIncludeIdentity::ofFile(json)); + { + INFO("blessed for the run, alone"); + REQUIRE(run.size() == 1); + CHECK(run[0] == s.at("loose/vendor/json.luau")); + } + UNSCOPED_INFO("its file alone, the rest of the run: nothing beside it"); + CHECK(found("./vendor/other") == std::string()); + UNSCOPED_INFO("an alias naming a folder"); + CHECK(found("@lib/util") == ALIncludeIdentity::ofFile(util)); + { + INFO("one naming an alias Second Life keeps: no configuration's"); + REQUIRE(found("@x/util", &why).empty()); + CHECK(why == "@sl-std is not a valid alias"); + } // A studio alias named after an include folder, where an alias of // the .luaurc at its top -- in reach of a script in an object -- has @@ -1289,9 +1518,16 @@ namespace tut const ALIncludeSearch::Where mine = where(false, true, { s.at("other") }); ALPreprocessor::Include include; std::vector aliases; - ensure("found nothing now", search.resolve(ask("solo", true, asking(true).self), include, asking(true), mine, nullptr, false, &aliases) == + UNSCOPED_INFO("found nothing now"); + CHECK(search.resolve(ask("solo", true, asking(true).self), include, asking(true), mine, nullptr, false, &aliases) == ALPreprocessor::Found::No); - ensure_equals("found before", include.searched, solo); - ensure("named past the .luaurc's own", include.moves.size() == 1 && include.moves[0].require == "@other2/solo" && include.moves[0].alias == "other2"); + UNSCOPED_INFO("found before"); + CHECK(include.searched == solo); + { + INFO("named past the .luaurc's own"); + REQUIRE(include.moves.size() == 1); + CHECK(include.moves[0].require == "@other2/solo"); + CHECK(include.moves[0].alias == "other2"); + } } } diff --git a/indra/alscript/tests/allinelabel_test.cpp b/indra/alscript/tests/allinelabel_test.cpp index 0332e0fefa..23f8bba655 100644 --- a/indra/alscript/tests/allinelabel_test.cpp +++ b/indra/alscript/tests/allinelabel_test.cpp @@ -27,7 +27,7 @@ #include "../preprocessor/allinelabel.h" #include "../preprocessor/alpreprocessor.h" -#include "../test/lltut.h" +#include #include @@ -48,7 +48,7 @@ namespace } } -namespace tut +namespace { struct allinelabel_data { @@ -84,157 +84,211 @@ namespace tut } }; - typedef test_group allinelabel_group; - typedef allinelabel_group::object allinelabel_object; - allinelabel_group allinelabel_instance("allinelabel"); - - template<> template<> - void allinelabel_object::test<1>() + TEST_CASE_METHOD(allinelabel_data, "allinelabel: a POSIX path from a folder goes down with `/` and up with `..`; case counts", "[allinelabel]") { - set_test_name("a path from a folder on a POSIX root: down with `/`, up with `..`, none to the folder itself or above it, and case tells parts apart"); - ensure_equals("beside", ALLineLabel::relative("/home/me/proj", "/home/me/proj/util.luau"), std::string("util.luau")); - ensure_equals("down", ALLineLabel::relative("/home/me/proj", "/home/me/proj/lib/util.luau"), std::string("lib/util.luau")); - ensure_equals("up and across", ALLineLabel::relative("/home/me/proj", "/home/me/shared/util.luau"), std::string("../shared/util.luau")); - ensure_equals("up to the root", ALLineLabel::relative("/home/me/proj", "/opt/lib/util.lsl"), std::string("../../../opt/lib/util.lsl")); - ensure_equals("a trailing slash and `.` passed over, `..` taken back", ALLineLabel::relative("/home/me/proj/./", "/home/me/x/../proj/lib//util.luau"), + UNSCOPED_INFO("beside"); + CHECK(ALLineLabel::relative("/home/me/proj", "/home/me/proj/util.luau") == std::string("util.luau")); + UNSCOPED_INFO("down"); + CHECK(ALLineLabel::relative("/home/me/proj", "/home/me/proj/lib/util.luau") == std::string("lib/util.luau")); + UNSCOPED_INFO("up and across"); + CHECK(ALLineLabel::relative("/home/me/proj", "/home/me/shared/util.luau") == std::string("../shared/util.luau")); + UNSCOPED_INFO("up to the root"); + CHECK(ALLineLabel::relative("/home/me/proj", "/opt/lib/util.lsl") == std::string("../../../opt/lib/util.lsl")); + UNSCOPED_INFO("a trailing slash and `.` passed over, `..` taken back"); + CHECK(ALLineLabel::relative("/home/me/proj/./", "/home/me/x/../proj/lib//util.luau") == std::string("lib/util.luau")); - ensure_equals("a name it only begins with is no folder of it", ALLineLabel::relative("/home/me/proj", "/home/me/project/util.luau"), + UNSCOPED_INFO("a name it only begins with is no folder of it"); + CHECK(ALLineLabel::relative("/home/me/proj", "/home/me/project/util.luau") == std::string("../project/util.luau")); - ensure_equals("another case another folder", ALLineLabel::relative("/home/me/proj", "/home/me/Proj/util.luau"), std::string("../Proj/util.luau")); - ensure_equals("a backslash no separator", ALLineLabel::relative("/home/me", "/home/me/a\\b.lsl"), std::string("a\\b.lsl")); - ensure("the folder itself", ALLineLabel::relative("/home/me/proj", "/home/me/proj/").empty()); - ensure("a folder it is in", ALLineLabel::relative("/home/me/proj", "/home/me").empty()); - ensure("not from a root", ALLineLabel::relative("proj", "proj/util.luau").empty() && ALLineLabel::relative("/home/me", "util.luau").empty()); - ensure("nothing", ALLineLabel::relative("", "").empty()); + UNSCOPED_INFO("another case another folder"); + CHECK(ALLineLabel::relative("/home/me/proj", "/home/me/Proj/util.luau") == std::string("../Proj/util.luau")); + UNSCOPED_INFO("a backslash no separator"); + CHECK(ALLineLabel::relative("/home/me", "/home/me/a\\b.lsl") == std::string("a\\b.lsl")); + UNSCOPED_INFO("the folder itself"); + REQUIRE(ALLineLabel::relative("/home/me/proj", "/home/me/proj/").empty()); + UNSCOPED_INFO("a folder it is in"); + REQUIRE(ALLineLabel::relative("/home/me/proj", "/home/me").empty()); + { + INFO("not from a root"); + REQUIRE(ALLineLabel::relative("proj", "proj/util.luau").empty()); + REQUIRE(ALLineLabel::relative("/home/me", "util.luau").empty()); + } + REQUIRE(ALLineLabel::relative("", "").empty()); } - template<> template<> - void allinelabel_object::test<2>() + TEST_CASE_METHOD(allinelabel_data, "allinelabel: a Windows path from a folder takes either separator and case and long forms", "[allinelabel]") { - set_test_name("a path from a folder as Windows writes it: either separator, any case, the long forms read as the paths they stand for; none to another drive or share"); - ensure_equals("down", ALLineLabel::relative("C:\\Users\\me\\proj", "C:\\Users\\me\\proj\\lib\\util.luau"), std::string("lib/util.luau")); - ensure_equals("up and across", ALLineLabel::relative("C:\\Users\\me\\proj", "C:\\Users\\me\\shared\\util.luau"), std::string("../shared/util.luau")); - ensure_equals("any case, the file's parts kept as written", ALLineLabel::relative("c:\\users\\ME\\Proj", "C:\\Users\\me\\proj\\Lib\\Util.luau"), + UNSCOPED_INFO("down"); + CHECK(ALLineLabel::relative("C:\\Users\\me\\proj", "C:\\Users\\me\\proj\\lib\\util.luau") == std::string("lib/util.luau")); + UNSCOPED_INFO("up and across"); + CHECK(ALLineLabel::relative("C:\\Users\\me\\proj", "C:\\Users\\me\\shared\\util.luau") == std::string("../shared/util.luau")); + UNSCOPED_INFO("any case, the file's parts kept as written"); + CHECK(ALLineLabel::relative("c:\\users\\ME\\Proj", "C:\\Users\\me\\proj\\Lib\\Util.luau") == std::string("Lib/Util.luau")); - ensure_equals("either separator", ALLineLabel::relative("C:/Users/me/proj/", "C:\\Users\\me\\proj/lib\\util.lsl"), std::string("lib/util.lsl")); - ensure_equals("a long path", ALLineLabel::relative("C:\\Users\\me\\proj", "\\\\?\\C:\\Users\\me\\proj\\lib\\util.luau"), std::string("lib/util.luau")); - ensure_equals("the NT namespace's", ALLineLabel::relative("\\??\\C:\\proj", "C:\\proj\\util.luau"), std::string("util.luau")); - ensure_equals("a share", ALLineLabel::relative("\\\\host\\share\\proj", "\\\\HOST\\Share\\proj\\lib\\util.lsl"), std::string("lib/util.lsl")); - ensure_equals("a share's long path", ALLineLabel::relative("\\\\?\\UNC\\host\\share\\proj", "//host/share/shared/util.lsl"), + UNSCOPED_INFO("either separator"); + CHECK(ALLineLabel::relative("C:/Users/me/proj/", "C:\\Users\\me\\proj/lib\\util.lsl") == std::string("lib/util.lsl")); + UNSCOPED_INFO("a long path"); + CHECK(ALLineLabel::relative("C:\\Users\\me\\proj", "\\\\?\\C:\\Users\\me\\proj\\lib\\util.luau") == std::string("lib/util.luau")); + UNSCOPED_INFO("the NT namespace's"); + CHECK(ALLineLabel::relative("\\??\\C:\\proj", "C:\\proj\\util.luau") == std::string("util.luau")); + UNSCOPED_INFO("a share"); + CHECK(ALLineLabel::relative("\\\\host\\share\\proj", "\\\\HOST\\Share\\proj\\lib\\util.lsl") == std::string("lib/util.lsl")); + UNSCOPED_INFO("a share's long path"); + CHECK(ALLineLabel::relative("\\\\?\\UNC\\host\\share\\proj", "//host/share/shared/util.lsl") == std::string("../shared/util.lsl")); - ensure("another drive", ALLineLabel::relative("C:\\proj", "D:\\proj\\util.luau").empty()); - ensure("another share", ALLineLabel::relative("\\\\host\\one\\proj", "\\\\host\\two\\proj\\util.luau").empty()); - ensure("a drive and a share", ALLineLabel::relative("C:\\proj", "\\\\host\\share\\proj\\util.luau").empty()); - ensure("a share with no share", ALLineLabel::relative("\\\\host", "\\\\host\\share\\util.luau").empty()); - ensure("a device", ALLineLabel::relative("\\\\.\\pipe", "\\\\.\\pipe\\util.luau").empty()); - ensure("Windows's and a POSIX root", ALLineLabel::relative("/proj", "C:\\proj\\util.luau").empty() && - ALLineLabel::relative("C:\\proj", "/proj/util.luau").empty()); - ensure("no further up than the root", ALLineLabel::relative("C:\\proj", "C:\\..\\..\\proj\\util.luau") == "util.luau"); + UNSCOPED_INFO("another drive"); + REQUIRE(ALLineLabel::relative("C:\\proj", "D:\\proj\\util.luau").empty()); + UNSCOPED_INFO("another share"); + REQUIRE(ALLineLabel::relative("\\\\host\\one\\proj", "\\\\host\\two\\proj\\util.luau").empty()); + UNSCOPED_INFO("a drive and a share"); + REQUIRE(ALLineLabel::relative("C:\\proj", "\\\\host\\share\\proj\\util.luau").empty()); + UNSCOPED_INFO("a share with no share"); + REQUIRE(ALLineLabel::relative("\\\\host", "\\\\host\\share\\util.luau").empty()); + UNSCOPED_INFO("a device"); + REQUIRE(ALLineLabel::relative("\\\\.\\pipe", "\\\\.\\pipe\\util.luau").empty()); + { + INFO("Windows's and a POSIX root"); + REQUIRE(ALLineLabel::relative("/proj", "C:\\proj\\util.luau").empty()); + REQUIRE(ALLineLabel::relative("C:\\proj", "/proj/util.luau").empty()); + } + UNSCOPED_INFO("no further up than the root"); + CHECK(ALLineLabel::relative("C:\\proj", "C:\\..\\..\\proj\\util.luau") == "util.luau"); } - template<> template<> - void allinelabel_object::test<3>() + TEST_CASE_METHOD(allinelabel_data, "allinelabel: quotable makes a label safe for a reader with no escapes", "[allinelabel]") { - set_test_name("a label quotable: a quote, a backslash and a line break made what a reader with no escapes reads whole"); - ensure_equals("each", ALLineLabel::quotable("a\"b\\c\nd\re\tf\x7fg"), std::string("a'b/c_d_e_f_g")); - ensure_equals("the rest as it was", ALLineLabel::quotable("lib/my util (2).luau \xc3\xa9"), std::string("lib/my util (2).luau \xc3\xa9")); + CHECK(ALLineLabel::quotable("a\"b\\c\nd\re\tf\x7fg") == std::string("a'b/c_d_e_f_g")); + UNSCOPED_INFO("the rest as it was"); + CHECK(ALLineLabel::quotable("lib/my util (2).luau \xc3\xa9") == std::string("lib/my util (2).luau \xc3\xa9")); } - template<> template<> - void allinelabel_object::test<4>() + TEST_CASE_METHOD(allinelabel_data, "allinelabel: a script on disk labels files from its folder and the world's by name", "[allinelabel]") { - set_test_name("a script on disk: itself by its base name, every file on disk from its folder however reached, the world's by name, nothing from a root"); const ALLineLabel label("disk:/home/me/proj/main.luau", { "/home/me/includes", "/home/me/proj/lib", "/home/me/shared" }); - ensure_equals("itself, by its own name on disk", label.of(fileOf("Main script", ""), true), std::string("main.luau")); - ensure_equals("beside it", label.of(fileOf("util.luau", "disk:/home/me/proj/util.luau"), false), std::string("util.luau")); - ensure_equals("under it", label.of(fileOf("util.luau", "disk:/home/me/proj/lib/util.luau"), false), std::string("lib/util.luau")); - ensure_equals("an alias's, from its folder all the same", label.of(fileOf("util.luau", "disk:/home/me/shared/util.luau"), false), + UNSCOPED_INFO("itself, by its own name on disk"); + CHECK(label.of(fileOf("Main script", ""), true) == std::string("main.luau")); + UNSCOPED_INFO("beside it"); + CHECK(label.of(fileOf("util.luau", "disk:/home/me/proj/util.luau"), false) == std::string("util.luau")); + UNSCOPED_INFO("under it"); + CHECK(label.of(fileOf("util.luau", "disk:/home/me/proj/lib/util.luau"), false) == std::string("lib/util.luau")); + UNSCOPED_INFO("an alias's, from its folder all the same"); + CHECK(label.of(fileOf("util.luau", "disk:/home/me/shared/util.luau"), false) == std::string("../shared/util.luau")); - ensure_equals("an include folder's", label.of(fileOf("util.lsl", "disk:/home/me/includes/net/util.lsl"), false), + UNSCOPED_INFO("an include folder's"); + CHECK(label.of(fileOf("util.lsl", "disk:/home/me/includes/net/util.lsl"), false) == std::string("../includes/net/util.lsl")); - ensure_equals("one under no blessed folder", label.of(fileOf("x.luau", "disk:/opt/x.luau"), false), std::string("../../../opt/x.luau")); - ensure_equals("one from the world, by its name", label.of(fileOf("Util \"v2\"", "object:5d2c6a5b-8a5e-4d1e-9b8a-3c2a1f0e9d8c:1a2b3c4d-1111-2222-3333-444455556666"), false), + UNSCOPED_INFO("one under no blessed folder"); + CHECK(label.of(fileOf("x.luau", "disk:/opt/x.luau"), false) == std::string("../../../opt/x.luau")); + UNSCOPED_INFO("one from the world, by its name"); + CHECK(label.of(fileOf("Util \"v2\"", "object:5d2c6a5b-8a5e-4d1e-9b8a-3c2a1f0e9d8c:1a2b3c4d-1111-2222-3333-444455556666"), false) == std::string("Util 'v2'")); - ensure_equals("one named no other way", label.of(fileOf("lib", "lib"), false), std::string("lib")); - ensure_equals("a call", label(fileOf("util.luau", "disk:/home/me/proj/lib/util.luau"), false), std::string("lib/util.luau")); + UNSCOPED_INFO("one named no other way"); + CHECK(label.of(fileOf("lib", "lib"), false) == std::string("lib")); + UNSCOPED_INFO("a call"); + CHECK(label(fileOf("util.luau", "disk:/home/me/proj/lib/util.luau"), false) == std::string("lib/util.luau")); for (const char* path : { "disk:/home/me/proj/lib/util.luau", "disk:/home/me/shared/util.luau", "disk:/opt/x.luau" }) { - ensure("never from a root: " + std::string(path), !fromRoot(label.of(fileOf("x", path), false))); + CAPTURE(path); + REQUIRE_FALSE(fromRoot(label.of(fileOf("x", path), false))); } } - template<> template<> - void allinelabel_object::test<5>() + TEST_CASE_METHOD(allinelabel_data, "allinelabel: off disk a file is labelled from the outermost blessed folder holding it", "[allinelabel]") { - set_test_name("a script not on disk, or a file on another drive: from the outermost blessed folder that holds it, as an #include names it; under none, by its name"); const ALLineLabel world("object:5d2c6a5b-8a5e-4d1e-9b8a-3c2a1f0e9d8c:1a2b3c4d-1111-2222-3333-444455556666", { "C:\\Users\\me\\includes\\net", "C:\\Users\\me\\includes", "D:\\lib" }); - ensure_equals("itself, by the name it is given", world.of(fileOf("Door \\ main", ""), true), std::string("Door / main")); - ensure_equals("the outermost folder's", world.of(fileOf("util.lsl", "disk:C:\\Users\\me\\includes\\net\\util.lsl"), false), + UNSCOPED_INFO("itself, by the name it is given"); + CHECK(world.of(fileOf("Door \\ main", ""), true) == std::string("Door / main")); + UNSCOPED_INFO("the outermost folder's"); + CHECK(world.of(fileOf("util.lsl", "disk:C:\\Users\\me\\includes\\net\\util.lsl"), false) == std::string("net/util.lsl")); - ensure_equals("one folder's", world.of(fileOf("util.lsl", "disk:c:\\users\\me\\Includes\\util.lsl"), false), std::string("util.lsl")); - ensure_equals("another drive's", world.of(fileOf("util.luau", "disk:D:\\lib\\x\\util.luau"), false), std::string("x/util.luau")); - ensure_equals("under none, by its name", world.of(fileOf("util.luau", "disk:E:\\util.luau"), false), std::string("util.luau")); + UNSCOPED_INFO("one folder's"); + CHECK(world.of(fileOf("util.lsl", "disk:c:\\users\\me\\Includes\\util.lsl"), false) == std::string("util.lsl")); + UNSCOPED_INFO("another drive's"); + CHECK(world.of(fileOf("util.luau", "disk:D:\\lib\\x\\util.luau"), false) == std::string("x/util.luau")); + UNSCOPED_INFO("under none, by its name"); + CHECK(world.of(fileOf("util.luau", "disk:E:\\util.luau"), false) == std::string("util.luau")); const ALLineLabel nothing("", {}); - ensure_equals("no identity at all: itself by its name", nothing.of(fileOf("main.lsl", ""), true), std::string("main.lsl")); - ensure_equals("and a file by its name", nothing.of(fileOf("util.lsl", "disk:/x/util.lsl"), false), std::string("util.lsl")); + UNSCOPED_INFO("no identity at all: itself by its name"); + CHECK(nothing.of(fileOf("main.lsl", ""), true) == std::string("main.lsl")); + UNSCOPED_INFO("and a file by its name"); + CHECK(nothing.of(fileOf("util.lsl", "disk:/x/util.lsl"), false) == std::string("util.lsl")); // On disk, but its include on another drive: from the folder that // holds that. const ALLineLabel drive("disk:C:\\Users\\me\\proj\\main.lsl", { "D:\\includes" }); - ensure_equals("itself", drive.of(fileOf("main.lsl", ""), true), std::string("main.lsl")); - ensure_equals("the same drive's from its folder", drive.of(fileOf("a.lsl", "disk:C:\\Users\\me\\lib\\a.lsl"), false), std::string("../lib/a.lsl")); - ensure_equals("another drive's from its blessed folder", drive.of(fileOf("a.lsl", "disk:D:\\includes\\net\\a.lsl"), false), std::string("net/a.lsl")); + UNSCOPED_INFO("itself"); + CHECK(drive.of(fileOf("main.lsl", ""), true) == std::string("main.lsl")); + UNSCOPED_INFO("the same drive's from its folder"); + CHECK(drive.of(fileOf("a.lsl", "disk:C:\\Users\\me\\lib\\a.lsl"), false) == std::string("../lib/a.lsl")); + UNSCOPED_INFO("another drive's from its blessed folder"); + CHECK(drive.of(fileOf("a.lsl", "disk:D:\\includes\\net\\a.lsl"), false) == std::string("net/a.lsl")); const ALLineLabel share("disk:\\\\host\\share\\proj\\main.lsl", { "C:\\includes" }); - ensure_equals("a script on a share: the share's from its folder", share.of(fileOf("a.lsl", "disk:\\\\host\\share\\proj\\lib\\a.lsl"), false), + UNSCOPED_INFO("a script on a share: the share's from its folder"); + CHECK(share.of(fileOf("a.lsl", "disk:\\\\host\\share\\proj\\lib\\a.lsl"), false) == std::string("lib/a.lsl")); - ensure_equals("and a drive's from its blessed folder", share.of(fileOf("a.lsl", "disk:C:\\includes\\a.lsl"), false), std::string("a.lsl")); + UNSCOPED_INFO("and a drive's from its blessed folder"); + CHECK(share.of(fileOf("a.lsl", "disk:C:\\includes\\a.lsl"), false) == std::string("a.lsl")); } - template<> template<> - void allinelabel_object::test<6>() + TEST_CASE_METHOD(allinelabel_data, "allinelabel: through a run: two SLua modules of one name read apart by their folders", "[allinelabel]") { - set_test_name("through a run: two SLua modules of one name in different folders read apart, the script by its name, and what the map calls each is as before"); add("lib/util", "util.luau", "disk:/home/me/proj/lib/util.luau", "return { a = 1 }\n"); add("@shared/util", "util.luau", "disk:/home/me/shared/util.luau", "return { b = 2 }\n"); ALPreprocessor::Options lua = options(true); lua.lineLabel = ALLineLabel("disk:/home/me/proj/main.luau", { "/home/me/shared" }); const std::string script = "local a = require(\"lib/util\")\nlocal b = require(\"@shared/util\")\nprint(a.a, b.b)\n"; ALPreprocessor::Result r = ALPreprocessor::run(script, lua); - ensure("no errors: " + r.text, !r.hasErrors()); - ensure("the one under it: " + r.text, r.text.find("-- @line 1 \"lib/util.luau\"\nreturn { a = 1 }\n") != std::string::npos); - ensure("the alias's, from its folder: " + r.text, r.text.find("-- @line 1 \"../shared/util.luau\"\nreturn { b = 2 }\n") != std::string::npos); - ensure("the script's: " + r.text, r.text.find("-- @line 1 \"main.luau\"\nlocal a = require(1)\n") != std::string::npos); - ensure_equals("three files", r.map.files().size(), size_t(3)); - ensure("each still called its __SHORTFILE__", r.map.files()[0].name == "main.luau" && r.map.files()[1].name == "util.luau" && - r.map.files()[2].name == "util.luau"); + UNSCOPED_INFO("no errors: " + r.text); + CHECK_FALSE(r.hasErrors()); + UNSCOPED_INFO("the one under it: " + r.text); + REQUIRE(r.text.find("-- @line 1 \"lib/util.luau\"\nreturn { a = 1 }\n") != std::string::npos); + UNSCOPED_INFO("the alias's, from its folder: " + r.text); + REQUIRE(r.text.find("-- @line 1 \"../shared/util.luau\"\nreturn { b = 2 }\n") != std::string::npos); + UNSCOPED_INFO("the script's: " + r.text); + REQUIRE(r.text.find("-- @line 1 \"main.luau\"\nlocal a = require(1)\n") != std::string::npos); + REQUIRE(r.map.files().size() == size_t(3)); + { + INFO("each still called its __SHORTFILE__"); + CHECK(r.map.files()[0].name == "main.luau"); + CHECK(r.map.files()[1].name == "util.luau"); + CHECK(r.map.files()[2].name == "util.luau"); + } // Unset, each by its name, as before. ALPreprocessor::Options plain = options(true); r = ALPreprocessor::run(script, plain); - ensure("unset, by name: " + r.text, r.text.find("-- @line 1 \"util.luau\"\nreturn { a = 1 }\n") != std::string::npos && - r.text.find("-- @line 1 \"main.luau\"\n") != std::string::npos); + { + INFO("unset, by name: " + r.text); + REQUIRE(r.text.find("-- @line 1 \"util.luau\"\nreturn { a = 1 }\n") != std::string::npos); + REQUIRE(r.text.find("-- @line 1 \"main.luau\"\n") != std::string::npos); + } } - template<> template<> - void allinelabel_object::test<7>() + TEST_CASE_METHOD(allinelabel_data, "allinelabel: through a run: LSL includes labelled from disk or by name and made quotable", "[allinelabel]") { - set_test_name("through a run: an LSL include from the world keeps its name, one on disk is from the script's folder, and whatever a label holds is made quotable"); add("net/util.lsl", "util.lsl", "disk:C:\\Users\\me\\includes\\net\\util.lsl", "integer x;\n"); add("World lib", "World \"lib\"", "inventory:1a2b3c4d-1111-2222-3333-444455556666", "integer y;\n"); ALPreprocessor::Options lsl = options(false); lsl.lineLabel = ALLineLabel("", { "C:\\Users\\me\\includes" }); const std::string script = "integer a;\n#include \"net/util.lsl\"\n#include \"World lib\"\ndefault { state_entry() { } }\n"; ALPreprocessor::Result r = ALPreprocessor::run(script, lsl); - ensure("no errors: " + r.text, !r.hasErrors()); - ensure("the disk's, as an #include names it: " + r.text, r.text.find("// @line 1 \"net/util.lsl\"\ninteger x;\n") != std::string::npos); - ensure("the world's, by its name: " + r.text, r.text.find("// @line 1 \"World 'lib'\"\ninteger y;\n") != std::string::npos); - ensure("the script's after: " + r.text, r.text.find("// @line 4 \"main.lsl\"\ndefault") != std::string::npos); + UNSCOPED_INFO("no errors: " + r.text); + CHECK_FALSE(r.hasErrors()); + UNSCOPED_INFO("the disk's, as an #include names it: " + r.text); + REQUIRE(r.text.find("// @line 1 \"net/util.lsl\"\ninteger x;\n") != std::string::npos); + UNSCOPED_INFO("the world's, by its name: " + r.text); + REQUIRE(r.text.find("// @line 1 \"World 'lib'\"\ninteger y;\n") != std::string::npos); + UNSCOPED_INFO("the script's after: " + r.text); + REQUIRE(r.text.find("// @line 4 \"main.lsl\"\ndefault") != std::string::npos); // A caller's own, quote and all, made quotable. lsl.lineLabel = [](const ALSourceMap::File& file, bool self) { return self ? std::string("my \"main\"") : "C:\\" + file.name; }; r = ALPreprocessor::run(script, lsl); - ensure("quotable: " + r.text, r.text.find("// @line 1 \"C:/util.lsl\"\n") != std::string::npos && - r.text.find("// @line 4 \"my 'main'\"\n") != std::string::npos); + { + INFO("quotable: " + r.text); + REQUIRE(r.text.find("// @line 1 \"C:/util.lsl\"\n") != std::string::npos); + REQUIRE(r.text.find("// @line 4 \"my 'main'\"\n") != std::string::npos); + } } } diff --git a/indra/alscript/tests/allslconsts_test.cpp b/indra/alscript/tests/allslconsts_test.cpp index 4c5c93ae29..336b7106d4 100644 --- a/indra/alscript/tests/allslconsts_test.cpp +++ b/indra/alscript/tests/allslconsts_test.cpp @@ -28,9 +28,9 @@ #include "../lsl/allslservice.h" #include "../preprocessor/alpreprocessor.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { struct allslconsts_data { @@ -58,63 +58,56 @@ namespace tut return out; } }; - typedef test_group allslconsts_group; - typedef allslconsts_group::object allslconsts_object; - allslconsts_group allslconsts_instance("ALLSLConsts"); - template<> template<> - void allslconsts_object::test<1>() + TEST_CASE_METHOD(allslconsts_data, "ALLSLConsts: a const variable is given its value where declared and set nowhere else", "[ALLSLConsts]") { - set_test_name("a const variable is given its value where it is declared, and set nowhere else"); - ensure("builtins: " + error, lslLoaded); - ensure_equals("nothing wrong", said("const integer A = 1;\n" - "default { state_entry() { const float f = A * 2; llOwnerSay((string)(f + A)); } }\n"), + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + CHECK(said("const integer A = 1;\n" + "default { state_entry() { const float f = A * 2; llOwnerSay((string)(f + A)); } }\n") == std::string()); - ensure_equals("each write, where it is", - said("const integer A = 1;\n" + CHECK(said("const integer A = 1;\n" "f(const integer n) { n = 2; }\n" "default\n{\n state_entry()\n {\n const vector v = <1, 2, 3>;\n A += 1;\n v.x = 2;\n ++A;\n" - " llOwnerSay((string)v);\n }\n}\n"), + " llOwnerSay((string)v);\n }\n}\n") == std::string("ConstAssigned 1:21 n\n" "ConstAssigned 7:8 A\n" "ConstAssigned 8:8 v\n" "ConstAssigned 9:8 A\n")); - ensure_equals("no value", said("const integer A;\ndefault { state_entry() { const string s; llOwnerSay(s + (string)A); } }\n"), + CHECK(said("const integer A;\ndefault { state_entry() { const string s; llOwnerSay(s + (string)A); } }\n") == std::string("ConstWithoutValue 0:14 A\n" "ConstWithoutValue 1:39 s\n")); - ensure_equals("a script that does not compile is the compiler's", said("const integer A = ;\n"), std::string()); + UNSCOPED_INFO("a script that does not compile is the compiler's"); + CHECK(said("const integer A = ;\n") == std::string()); } - template<> template<> - void allslconsts_object::test<2>() + TEST_CASE_METHOD(allslconsts_data, "ALLSLConsts: a const function works out what it gives back from its arguments alone", "[ALLSLConsts]") { - set_test_name("a const function works out what it gives back from its arguments alone"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string globals = "integer counter;\ninteger fixed = 3;\n"; const std::string handler = "default { state_entry() { counter = 1; } }\n"; - ensure_equals("pure, calling what is pure, reading what never changes", - said(globals + + UNSCOPED_INFO("pure, calling what is pure, reading what never changes"); + CHECK(said(globals + "integer twice(integer x) { return x * 2; }\n" - "const integer f(integer x) { integer y = twice(x) + fixed; return llAbs(y); }\n" + handler), + "const integer f(integer x) { integer y = twice(x) + fixed; return llAbs(y); }\n" + handler) == std::string()); - ensure_equals("each thing it may not do, where it does it", - said(globals + + CHECK(said(globals + "const integer f(integer x)\n{\n counter = x;\n llOwnerSay(\"hi\");\n if (x) state other;\n return x + counter;\n}\n" - "default { state_entry() { counter = 1; } }\nstate other { state_entry() { } }\n"), + "default { state_entry() { counter = 1; } }\nstate other { state_entry() { } }\n") == std::string("ConstFunctionSets 4:4 f counter\n" "ConstFunctionCalls 5:4 f llOwnerSay\n" "ConstFunctionChangesState 6:11 f\n" "ConstFunctionReads 7:15 f counter\n")); - ensure_equals("a function it calls that sets a global", - said(globals + "bump() { counter += 1; }\nconst integer f(integer x) { bump(); return x; }\n" + handler), + UNSCOPED_INFO("a function it calls that sets a global"); + CHECK(said(globals + "bump() { counter += 1; }\nconst integer f(integer x) { bump(); return x; }\n" + handler) == std::string("ConstFunctionCalls 3:29 f bump\n")); } - template<> template<> - void allslconsts_object::test<3>() + TEST_CASE_METHOD(allslconsts_data, "ALLSLConsts: optimizer off: a const global is written as the literal of its value", "[ALLSLConsts]") { - set_test_name("a const global's value is worked out, and written as the literal LSL takes there, with the optimizer off"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string source = "const integer FLAGS = PERMISSION_TAKE_CONTROLS | PERMISSION_TRIGGER_ANIMATION;\n" "const float STEP = TWO_PI / 12;\n" "const integer TWICE = FLAGS * 2;\n" @@ -127,9 +120,8 @@ namespace tut "const integer C = changing + 1;\n" "default { state_entry() { changing = 2; llOwnerSay((string)[FLAGS, STEP, TWICE, UP, NAME, SAME, PLAIN, C] + (string)L); } }\n"; const ALPreprocessor::Result r = ALPreprocessor::run(source, ALPreprocessor::Options()); - ensure_equals("nothing wrong", said(source), std::string()); - ensure_equals("each written", - r.text, + CHECK(said(source) == std::string()); + CHECK(r.text == std::string("integer FLAGS = 20;\n" "float STEP = 0.5235988;\n" "integer TWICE = 40;\n" @@ -143,60 +135,78 @@ namespace tut "default { state_entry() { changing = 2; llOwnerSay((string)[FLAGS, STEP, TWICE, UP, NAME, SAME, PLAIN, C] + (string)L); } }\n")); // What was written maps to what it was written for. const ALSourceMap::Loc twenty = r.map.toSource(0, 16); - ensure("the value maps to the expression", twenty.found() && twenty.line == 0 && twenty.column == 22); + { + INFO("the value maps to the expression"); + REQUIRE(twenty.found()); + CHECK(twenty.line == 0); + CHECK(twenty.column == 22); + } // The names are where they are now: the list's two lines are one. - ensure("nine", r.consts.size() == 9); - ensure("a name after the folded list, a line up", r.consts[6].name == "SAME" && r.consts[6].line == 6 && r.consts[6].column == 8); + REQUIRE(r.consts.size() == 9); + { + INFO("a name after the folded list, a line up"); + CHECK(r.consts[6].name == "SAME"); + CHECK(r.consts[6].line == 6); + CHECK(r.consts[6].column == 8); + } const ALSourceMap::Loc same = r.map.toSource(6, 8); - ensure("and mapped to where it was", same.found() && same.line == 7); + { + INFO("and mapped to where it was"); + REQUIRE(same.found()); + CHECK(same.line == 7); + } // The target's arithmetic: Luau's doubles. ALPreprocessor::Options luau = ALPreprocessor::Options(); luau.optimizer.target = ALLSLOptimizer::Target::Luau; const ALPreprocessor::Result wide = ALPreprocessor::run("const float STEP = TWO_PI / 12;\ndefault { state_entry() { llOwnerSay((string)STEP); } }\n", luau); - ensure("in doubles on Luau: " + wide.text, wide.text.find("float STEP = 0.5235987") == 0 && wide.text.find(";") > 26); + { + INFO("in doubles on Luau: " + wide.text); + CHECK(wide.text.find("float STEP = 0.5235987") == 0); + CHECK(wide.text.find(";") > 26); + } } - template<> template<> - void allslconsts_object::test<4>() + TEST_CASE_METHOD(allslconsts_data, "ALLSLConsts: a const global known only at run time or not writable is an error", "[ALLSLConsts]") { - set_test_name("a const global whose value cannot be worked out before the script runs is an error, and so is one that cannot be written"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); // A global the script changes is no bar: globals are given their // values as the script starts, when each has its first. - ensure_equals("each, at its value", - said("const integer T = llGetUnixTime();\n" + UNSCOPED_INFO("each, at its value"); + CHECK(said("const integer T = llGetUnixTime();\n" "const float BIG = 1e30 * 1e30;\n" "const list KEYS = [(key)NULL_KEY] + [];\n" - "default { state_entry() { llOwnerSay((string)[T, BIG] + (string)KEYS); } }\n"), + "default { state_entry() { llOwnerSay((string)[T, BIG] + (string)KEYS); } }\n") == std::string("ConstNotKnown 0:18 T\n" "ConstNotKnown 1:18 BIG\n" "ConstNotKnown 2:18 KEYS\n")); // A plain global is left to the compiler: only a const is worked out. const std::string plain = "integer A = 2 * 3;\ndefault { state_entry() { llOwnerSay((string)A); } }\n"; - ensure_equals("not a const", ALPreprocessor::run(plain, ALPreprocessor::Options()).text, plain); + CHECK(ALPreprocessor::run(plain, ALPreprocessor::Options()).text == plain); } - template<> template<> - void allslconsts_object::test<5>() + TEST_CASE_METHOD(allslconsts_data, "ALLSLConsts: optimizer on: a const's value goes where it is read and its global goes", "[ALLSLConsts]") { - set_test_name("with the optimizer on, a const's value goes where it is read, and the global with it"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); ALPreprocessor::Options o = ALPreprocessor::Options(); o.optimize = true; const ALPreprocessor::Result r = ALPreprocessor::run("const integer FLAGS = PERMISSION_TAKE_CONTROLS | PERMISSION_TRIGGER_ANIMATION;\n" "default { state_entry() { llRequestPermissions(llGetOwner(), FLAGS); } }\n", o); - ensure("optimized: " + r.text, r.optimized); - ensure("the value in place: " + r.text, r.text.find("llRequestPermissions(llGetOwner(), 20);") != std::string::npos); - ensure("and no global: " + r.text, r.text.find("FLAGS") == std::string::npos); + UNSCOPED_INFO("optimized: " + r.text); + CHECK(r.optimized); + UNSCOPED_INFO("the value in place: " + r.text); + REQUIRE(r.text.find("llRequestPermissions(llGetOwner(), 20);") != std::string::npos); + UNSCOPED_INFO("and no global: " + r.text); + REQUIRE(r.text.find("FLAGS") == std::string::npos); } - template<> template<> - void allslconsts_object::test<6>() + TEST_CASE_METHOD(allslconsts_data, "ALLSLConsts: optimizer on: a const function folds on constants and other calls stay", "[ALLSLConsts]") { - set_test_name("with the optimizer on, a const function called with constants folds to its answer, and a call with anything else stays"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string source = "const integer sq(integer x) { return x * x; }\n" "const float deg(float d) { return d * DEG_TO_RAD; }\n" "const integer clampi(integer v, integer lo, integer hi)\n{\n if (v < lo)\n return lo;\n" @@ -206,29 +216,36 @@ namespace tut ALPreprocessor::Options o = ALPreprocessor::Options(); o.optimize = true; const ALPreprocessor::Result r = ALPreprocessor::run(source, o); - ensure("optimized: " + r.text, r.optimized); - ensure_equals("nothing wrong", said(source), std::string()); + UNSCOPED_INFO("optimized: " + r.text); + CHECK(r.optimized); + CHECK(said(source) == std::string()); // The list stays a literal: as a sum it would need a helper the // script has no other use for, which costs Mono more than it saves. - ensure("each worked out: " + r.text, r.text.find("[9, 1.5707964, 10, sq(n)]") != std::string::npos); - ensure("the functions no call is left to go: " + r.text, r.text.find("deg(") == std::string::npos && r.text.find("clampi(") == std::string::npos); - ensure("and the one a call is left to stays: " + r.text, r.text.find("integer sq(integer x)") != std::string::npos); + UNSCOPED_INFO("each worked out: " + r.text); + REQUIRE(r.text.find("[9, 1.5707964, 10, sq(n)]") != std::string::npos); + { + INFO("the functions no call is left to go: " + r.text); + REQUIRE(r.text.find("deg(") == std::string::npos); + REQUIRE(r.text.find("clampi(") == std::string::npos); + } + UNSCOPED_INFO("and the one a call is left to stays: " + r.text); + REQUIRE(r.text.find("integer sq(integer x)") != std::string::npos); // With the optimizer off, a call is a call. const ALPreprocessor::Result off = ALPreprocessor::run(source, ALPreprocessor::Options()); - ensure("calls stay: " + off.text, off.text.find("sq(3), deg(90), clampi(12, 0, 10), sq(n)") != std::string::npos); + UNSCOPED_INFO("calls stay: " + off.text); + REQUIRE(off.text.find("sq(3), deg(90), clampi(12, 0, 10), sq(n)") != std::string::npos); } - template<> template<> - void allslconsts_object::test<7>() + TEST_CASE_METHOD(allslconsts_data, "ALLSLConsts: the analyzer and the preprocessor agree where each const is declared", "[ALLSLConsts]") { - set_test_name("where the analyzer says a name was declared is where the preprocessor says a const is: a global, a parameter, a function"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const ALPreprocessor::Result r = ALPreprocessor::run("const integer LIMIT = 3 * 4;\n" "const integer twice(const integer n) { return n * 2; }\n" "default { state_entry() { llOwnerSay((string)twice(LIMIT)); } }\n", ALPreprocessor::Options()); - ensure_equals("three", r.consts.size(), size_t(3)); + REQUIRE(r.consts.size() == size_t(3)); const auto declaredAt = [&](const std::string& word, size_t from) { const size_t at = r.text.find(word, from); const S32 line = static_cast(std::count(r.text.begin(), r.text.begin() + at, '\n')); @@ -239,12 +256,15 @@ namespace tut const size_t handler = r.text.find("default"); for (const auto& [word, from] : std::vector>{ { "LIMIT", handler }, { "twice", handler }, { "n * 2", 0 } }) { + CAPTURE(word); const ALScriptHover hover = declaredAt(word, from); - ensure(word + ": found", hover.found && hover.hasDefinition); + REQUIRE(hover.found); + REQUIRE(hover.hasDefinition); const bool matches = std::any_of(r.consts.begin(), r.consts.end(), [&](const ALPreprocessor::Result::Const& c) { return c.line == hover.definitionLine && c.column == hover.definitionColumn; }); - ensure(word + ": at a const's place, " + std::to_string(hover.definitionLine) + ":" + std::to_string(hover.definitionColumn), matches); + CAPTURE(hover.definitionLine, hover.definitionColumn); + REQUIRE(matches); } } } diff --git a/indra/alscript/tests/allslcosts_test.cpp b/indra/alscript/tests/allslcosts_test.cpp index eea81dc9ef..1c81249e1c 100644 --- a/indra/alscript/tests/allslcosts_test.cpp +++ b/indra/alscript/tests/allslcosts_test.cpp @@ -38,14 +38,14 @@ #include "../lsl/allslservice.h" #include "../core/alscriptweight.h" -#include "../test/lltut.h" +#include #include #include #include #include -namespace tut +namespace { struct allslcosts_data { @@ -84,13 +84,14 @@ namespace tut body + "llOwnerSay((string)[i, j, f, s, k, v] + (string)l);\n} }\n"; } - S32 size(Target t, const Snippet& s, int copies) + S32 require_compiled_size(Target t, const Snippet& s, int copies) { const std::string text = script(s, copies); const ALScriptWeight weight = t == Target::LSO ? ALScriptWeigh::lso(text) : t == Target::Mono ? ALScriptWeigh::mono(text) : ALScriptWeigh::lslLuau(text); - ensure("compiles for " + std::string(nameOf(t)) + ": " + weight.error + "\n" + text, weight.compiled); + INFO("compiles for " << nameOf(t) << ": " << weight.error << "\n" << text); + REQUIRE(weight.compiled); return S32(weight.total); } @@ -104,11 +105,11 @@ namespace tut }; Delta delta(Target t, const Snippet& a, const Snippet& b) { - const S32 a1 = size(t, a, 1); - const S32 b1 = size(t, b, 1); + const S32 a1 = require_compiled_size(t, a, 1); + const S32 b1 = require_compiled_size(t, b, 1); Delta d; d.first = b1 - a1; - d.each = !a.body.empty() && !b.body.empty() ? (size(t, b, 2) - b1) - (size(t, a, 2) - a1) : d.first; + d.each = !a.body.empty() && !b.body.empty() ? (require_compiled_size(t, b, 2) - b1) - (require_compiled_size(t, a, 2) - a1) : d.first; return d; } @@ -154,18 +155,15 @@ namespace tut } } }; - typedef test_group allslcosts_group; - typedef allslcosts_group::object allslcosts_object; - allslcosts_group allslcosts_instance("ALLSLCosts"); // The rewrites: each pair's verdict on LSO, Mono and Luau in turn -- // '<' where the second form is smaller -- and the flag that stands for // it where the optimizer has one. The rest are what rewrites still to // come will read: here so that what was measured stays measured. - template<> template<> - void allslcosts_object::test<1>() + TEST_CASE_METHOD(allslcosts_data, "ALLSLCosts: the rewrites: each pair's verdict on each target and the flag for it", "[ALLSLCosts]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); using Flag = bool ALLSLCosts::*; struct Fact { @@ -387,72 +385,74 @@ namespace tut const Delta d = delta(t, fact.a, fact.b); said(fact.what, t, d, 0); const char v = fact.once ? verdict({ d.first, d.first }) : verdict(d); - ensure_equals(std::string(fact.what) + " on " + nameOf(t), v, fact.verdicts[n]); + INFO(fact.what << " on " << nameOf(t)); + REQUIRE(v == fact.verdicts[n]); // A flag on is a form smaller there; one off may be for what // a first use costs a script that the pair does not show // (allslcosts.cpp says where). if (fact.flag && ALLSLCosts::of(t).*fact.flag) { - ensure(std::string(fact.what) + ": the flag on " + nameOf(t) + ", and the form not smaller", v == '<'); + UNSCOPED_INFO("the flag is on and the form not smaller"); + REQUIRE(v == '<'); } } } } - // What a function costs, and a call of it, a local and a jump. - template<> template<> - void allslcosts_object::test<2>() + TEST_CASE_METHOD(allslcosts_data, "ALLSLCosts: what a function costs and a call of it and a local and a jump", "[ALLSLCosts]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const Snippet none{ "", "" }; const Snippet two{ "f1() { }", "" }; const Snippet eighteen{ "a_much_longer_name() { }", "" }; const Snippet param{ "f1(integer a) { }", "" }; for (const Target t : TARGETS) { - const ALLSLCosts& c = ALLSLCosts::of(t); - const std::string where = std::string(" on ") + nameOf(t); + const ALLSLCosts& c = ALLSLCosts::of(t); + INFO("on " << nameOf(t)); const Delta local = delta(t, { "", "integer z = i; j = z;" }, { "", "j = i;" }); said("a local", t, local, -c.local); - ensure_equals("a local" + where, -local.each, c.local); + REQUIRE(-local.each == c.local); const Delta jump = delta(t, { "", "jump L; @L;" }, { "", "" }); said("a jump", t, jump, -c.jump); - ensure("a jump" + where + ": " + std::to_string(-jump.first), -jump.first == c.jump); + REQUIRE(-jump.first == c.jump); const Delta f2 = delta(t, none, two); const Delta f18 = delta(t, none, eighteen); said("a function", t, f2, c.function + 2 * c.functionChar); - ensure_equals("a function's name, by the character" + where, (f18.first - f2.first) / 16, c.functionChar); - ensure_equals("a function" + where, f2.first - 2 * c.functionChar, c.function); + REQUIRE((f18.first - f2.first) / 16 == c.functionChar); + REQUIRE(f2.first - 2 * c.functionChar == c.function); const Delta p = delta(t, two, param); said("a parameter", t, p, c.parameter); - ensure_equals("a parameter" + where, p.first, c.parameter); + REQUIRE(p.first == c.parameter); const Delta call = delta(t, { "f1() { }", "f1();" }, { "f1() { }", "f1(); f1();" }); said("a call", t, call, c.call); - ensure_equals("a call" + where, call.each, c.call); + REQUIRE(call.each == c.call); const Delta argued = delta(t, { "f1(integer a) { }", "f1(i);" }, { "f1(integer a) { }", "f1(i); f1(i);" }); said("a call with an argument", t, argued, c.call + c.argument); - ensure_equals("an argument" + where, argued.each - c.call, c.argument); + REQUIRE(argued.each - c.call == c.argument); for (const char* text : { "hello", "hello world, hello" }) { const S32 length = S32(strlen(text)); const Delta string = delta(t, { "", "s = s;" }, { "", "s = \"" + std::string(text) + "\";" }); said("a string of " + std::to_string(length) + ", held once", t, string, c.stringHeld + length * c.stringHeldChar); - ensure_equals("a string of " + std::to_string(length) + ", held once" + where, string.first - string.each, + CAPTURE(length); + REQUIRE(string.first - string.each == c.stringHeld + length * c.stringHeldChar); } const Delta first2 = delta(t, two, { "f1() { }", "f1();" }); const Delta first18 = delta(t, eighteen, { "a_much_longer_name() { }", "a_much_longer_name();" }); said("a first call", t, first2, c.call + c.reference + 2 * c.referenceChar); - ensure_equals("a first call's reference, by the character" + where, (first18.first - first2.first) / 16, c.referenceChar); - ensure_equals("a first call's reference" + where, first2.first - c.call - 2 * c.referenceChar, c.reference); + REQUIRE((first18.first - first2.first) / 16 == c.referenceChar); + REQUIRE(first2.first - c.call - 2 * c.referenceChar == c.reference); // The most elements of a literal added to a list bare: at it // smaller, one past it larger; with none, eight smaller. @@ -468,21 +468,23 @@ namespace tut }; if (c.elementsForListMost > 0) { - ensure("the most added bare, smaller" + where, verdict(bare(c.elementsForListMost)) == '<'); - ensure("one more, larger" + where, verdict(bare(c.elementsForListMost + 1)) == '>'); + UNSCOPED_INFO("the most added bare, smaller"); + REQUIRE(verdict(bare(c.elementsForListMost)) == '<'); + UNSCOPED_INFO("one more, larger"); + REQUIRE(verdict(bare(c.elementsForListMost + 1)) == '>'); } else { - ensure("eight added bare, smaller" + where, verdict(bare(8)) == '<'); + UNSCOPED_INFO("eight added bare, smaller"); + REQUIRE(verdict(bare(8)) == '<'); } } } - // A global never assigned, against its value written where it is read. - template<> template<> - void allslcosts_object::test<3>() + TEST_CASE_METHOD(allslcosts_data, "ALLSLCosts: a global never assigned against its value written where it is read", "[ALLSLCosts]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); struct Kind { const char* what; @@ -503,8 +505,8 @@ namespace tut const auto put = [](std::string text, const std::string& what) { return text.replace(text.find('@'), 1, what); }; for (const Target t : TARGETS) { - const ALLSLCosts& c = ALLSLCosts::of(t); - const std::string where = std::string(" on ") + nameOf(t); + const ALLSLCosts& c = ALLSLCosts::of(t); + INFO("on " << nameOf(t)); for (const Kind& kind : kinds) { std::string value; @@ -519,9 +521,10 @@ namespace tut const ALLSLCosts::Held& held = c.*kind.held; said(std::string(kind.what) + " global", t, own, held.global); said(std::string(kind.what) + " written out", t, out, held.first); - ensure_equals(std::string(kind.what) + " global" + where, own.first, held.global); - ensure_equals(std::string(kind.what) + " written out, first" + where, out.first, held.first); - ensure_equals(std::string(kind.what) + " written out, each" + where, out.each, held.each); + CAPTURE(kind.what); + REQUIRE(own.first == held.global); + REQUIRE(out.first == held.first); + REQUIRE(out.each == held.each); } // A string at two lengths, what a character adds the same. for (const char* text : { "hello", "hello world, hello" }) @@ -535,9 +538,10 @@ namespace tut const std::string what = "a string of " + std::to_string(length); said(what + " global", t, own, held.global); said(what + " written out", t, out, held.first); - ensure_equals(what + " global" + where, own.first, held.global); - ensure_equals(what + " written out, first" + where, out.first, held.first); - ensure_equals(what + " written out, each" + where, out.each, held.each); + CAPTURE(length); + REQUIRE(own.first == held.global); + REQUIRE(out.first == held.first); + REQUIRE(out.each == held.each); } } } @@ -545,10 +549,10 @@ namespace tut // What putting a function in place is estimated to come to, against the // script written both ways and weighed: the same side of nothing, and // near it. - template<> template<> - void allslcosts_object::test<4>() + TEST_CASE_METHOD(allslcosts_data, "ALLSLCosts: what putting a function in place is estimated to come to", "[ALLSLCosts]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); struct Case { const char* what; @@ -583,14 +587,17 @@ namespace tut { bytes = part.name == one.name ? S32(part.bytes) : bytes; } - ensure(std::string(one.what) + ": the function weighed on " + nameOf(t) + ": " + whole.error, bytes > 0); - const S32 measured = size(t, one.inlined, 1) - size(t, one.called, 1); + INFO(one.what << " on " << nameOf(t)); + UNSCOPED_INFO("the function weighed: " << whole.error); + REQUIRE(bytes > 0); + const S32 measured = require_compiled_size(t, one.inlined, 1) - require_compiled_size(t, one.called, 1); ALLSLCosts::Inlining f = one.f; f.bytes = bytes; const S32 estimate = ALLSLCosts::of(t).inlined(f); said(std::string(one.what) + " (estimated)", t, { measured, measured }, estimate); - ensure(std::string(one.what) + " on " + nameOf(t) + ": estimated " + std::to_string(estimate) + ", measured " + std::to_string(measured), - (estimate < 0) == (measured < 0) && std::abs(estimate - measured) <= std::max(12, std::abs(measured) / 4)); + CAPTURE(estimate, measured); + REQUIRE((estimate < 0) == (measured < 0)); + REQUIRE(std::abs(estimate - measured) <= std::max(12, std::abs(measured) / 4)); } } } @@ -598,14 +605,14 @@ namespace tut // Where a list's helpers are held once for the script: the least a // list's shape saves at a place, and the most a helper new to the // script costs. - template<> template<> - void allslcosts_object::test<5>() + TEST_CASE_METHOD(allslcosts_data, "ALLSLCosts: where a list's helpers are held once for the script", "[ALLSLCosts]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); for (const Target t : TARGETS) { - const ALLSLCosts& c = ALLSLCosts::of(t); - const std::string where = std::string(" on ") + nameOf(t); + const ALLSLCosts& c = ALLSLCosts::of(t); + INFO("on " << nameOf(t)); if (c.listShapeLeast == 0) { continue; @@ -625,7 +632,7 @@ namespace tut least = std::min(least, -delta(t, a, b).each); } said("the least a list's shape saves", t, { least, least }, c.listShapeLeast); - ensure_equals("the least a list's shape saves at a place" + where, least, c.listShapeLeast); + REQUIRE(least == c.listShapeLeast); // A helper each: a cast to a list, something added after one, // something added before one. const Snippet had{ "", "l = l + [i];" }; @@ -636,7 +643,7 @@ namespace tut most = std::max(most, d.first - d.each); } said("the most a helper costs", t, { most, most }, c.listHelperMost); - ensure_equals("the most a list's helper new to the script costs" + where, most, c.listHelperMost); + REQUIRE(most == c.listHelperMost); } } } diff --git a/indra/alscript/tests/allsleffects_test.cpp b/indra/alscript/tests/allsleffects_test.cpp index c61fde9763..8c9217db14 100644 --- a/indra/alscript/tests/allsleffects_test.cpp +++ b/indra/alscript/tests/allsleffects_test.cpp @@ -28,7 +28,7 @@ #include "../lsl/allsleffects.h" #include "../lsl/allslservice.h" -#include "../test/lltut.h" +#include #include @@ -94,7 +94,7 @@ namespace } } -namespace tut +namespace { struct allsleffects_data { @@ -108,20 +108,14 @@ namespace tut loaded = service.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error); if (!loaded) { - fail("the builtins did not load: " + error); + FAIL("the builtins did not load: " + error); } } } }; - typedef test_group allsleffects_group; - typedef allsleffects_group::object allsleffects_object; - allsleffects_group allsleffects_instance("allsleffects"); - - template<> template<> - void allsleffects_object::test<1>() + TEST_CASE_METHOD(allsleffects_data, "allsleffects: a function's writes and impurity take in its callees even round a loop", "[allsleffects]") { - set_test_name("a function's writes are the globals it assigns, itself and through what it calls, round a loop of calls too; a library call that is not pure makes it impure"); const Parsed p("integer g;\ninteger h;\ninteger k;\n" "a() { g = 1; }\n" "b() { a(); h++; }\n" @@ -132,7 +126,7 @@ namespace tut "r() { k += 2; s(); }\n" "s() { r(); }\n" "default { state_entry() { f(); } }\n"); - ensure("parsed", p.script != nullptr); + REQUIRE(p.script != nullptr); const ALLSLEffects effects(p.script); const auto is = [&](const char* name, std::initializer_list globals, bool impure) { const ALLSLEffects::Writes& w = effects.ofFunction(p.function(name)); @@ -143,20 +137,28 @@ namespace tut } return same; }; - ensure("a writes g", is("a", { "g" }, false)); - ensure("b writes h, and g through a", is("b", { "g", "h" }, false)); - ensure("c writes only its own local, which no caller sees", is("c", {}, false)); - ensure("d says something: impure", is("d", {}, true)); - ensure("e's library call is pure", is("e", { "k" }, false)); - ensure("f through b through a", is("f", { "g", "h" }, false)); - ensure("r and s call each other, and both write k", is("r", { "k" }, false) && is("s", { "k" }, false)); - ensure("one the script does not define: impure", effects.ofFunction(nullptr).impure); + CHECK(is("a", { "g" }, false)); + UNSCOPED_INFO("b writes h, and g through a"); + CHECK(is("b", { "g", "h" }, false)); + UNSCOPED_INFO("c writes only its own local, which no caller sees"); + CHECK(is("c", {}, false)); + UNSCOPED_INFO("d says something: impure"); + CHECK(is("d", {}, true)); + UNSCOPED_INFO("e's library call is pure"); + CHECK(is("e", { "k" }, false)); + UNSCOPED_INFO("f through b through a"); + CHECK(is("f", { "g", "h" }, false)); + { + INFO("r and s call each other, and both write k"); + CHECK(is("r", { "k" }, false)); + CHECK(is("s", { "k" }, false)); + } + UNSCOPED_INFO("one the script does not define: impure"); + CHECK(effects.ofFunction(nullptr).impure); } - template<> template<> - void allsleffects_object::test<2>() + TEST_CASE_METHOD(allsleffects_data, "allsleffects: a call may go first past what changes nothing nor reads its writes", "[allsleffects]") { - set_test_name("a call may run first where what runs before it -- a right operand, earlier arguments, a list's other elements -- changes nothing and reads nothing it changes"); const Parsed p("integer g;\ninteger h;\n" "integer bump() { return ++g; }\n" "integer same(integer v) { return v; }\n" @@ -177,27 +179,42 @@ namespace tut " x = y + same(y++);\n" // 11: and here after " g = bump();\n" // 12: the store is after "} }\n"); - ensure("parsed", p.script != nullptr); + REQUIRE(p.script != nullptr); const ALLSLEffects effects(p.script); const std::vector bumps = p.calls("bump"); const std::vector sames = p.calls("same"); - ensure_equals("the calls", bumps.size(), size_t(11)); + REQUIRE(bumps.size() == size_t(11)); const auto first = [&](LSLFunctionExpression* call) { return effects.mayRunFirst(statementOf(call), call); }; - ensure("0: the right operand runs first", first(bumps[0])); - ensure("1: g before it, and it writes g", !first(bumps[1])); - ensure("2: h before it, unwritten", first(bumps[2])); - ensure("3: an impure call before it", !first(bumps[3])); - ensure("4: a pure call before it", first(bumps[4])); - ensure("5: an increment before it", !first(bumps[5])); - ensure("6: an earlier argument reads what it writes", !first(bumps[6])); - ensure("7: the first argument", first(bumps[7])); - ensure("8: a list's other element, unwritten", first(bumps[8])); - ensure("9: a list's other element it writes, the order not known", !first(bumps[9])); - ensure("10: its own argument writes what runs before it", !first(sames[0])); - ensure("11: what reads it runs after", first(sames[1])); - ensure("12: an assignment's store is after its value", first(bumps[10])); - ensure_equals("before the right operand: nothing", ALLSLEffects::before(statementOf(bumps[0]), bumps[0]).size(), size_t(0)); - ensure_equals("before the left: the right", ALLSLEffects::before(statementOf(bumps[1]), bumps[1]).size(), size_t(1)); + UNSCOPED_INFO("0: the right operand runs first"); + CHECK(first(bumps[0])); + UNSCOPED_INFO("1: g before it, and it writes g"); + CHECK_FALSE(first(bumps[1])); + UNSCOPED_INFO("2: h before it, unwritten"); + CHECK(first(bumps[2])); + UNSCOPED_INFO("3: an impure call before it"); + CHECK_FALSE(first(bumps[3])); + UNSCOPED_INFO("4: a pure call before it"); + CHECK(first(bumps[4])); + UNSCOPED_INFO("5: an increment before it"); + CHECK_FALSE(first(bumps[5])); + UNSCOPED_INFO("6: an earlier argument reads what it writes"); + CHECK_FALSE(first(bumps[6])); + UNSCOPED_INFO("7: the first argument"); + CHECK(first(bumps[7])); + UNSCOPED_INFO("8: a list's other element, unwritten"); + CHECK(first(bumps[8])); + UNSCOPED_INFO("9: a list's other element it writes, the order not known"); + CHECK_FALSE(first(bumps[9])); + UNSCOPED_INFO("10: its own argument writes what runs before it"); + CHECK_FALSE(first(sames[0])); + UNSCOPED_INFO("11: what reads it runs after"); + CHECK(first(sames[1])); + UNSCOPED_INFO("12: an assignment's store is after its value"); + CHECK(first(bumps[10])); + UNSCOPED_INFO("before the right operand: nothing"); + REQUIRE(ALLSLEffects::before(statementOf(bumps[0]), bumps[0]).size() == size_t(0)); + UNSCOPED_INFO("before the left: the right"); + REQUIRE(ALLSLEffects::before(statementOf(bumps[1]), bumps[1]).size() == size_t(1)); // Calls of the script's own that change nothing, either side. const Parsed q("integer g;\n" @@ -210,18 +227,18 @@ namespace tut " l = [look(), bump()];\n" // bump writes what look reads " l = [bump(), same(2)];\n" // same changes nothing, and bump does "} }\n"); - ensure("parsed", q.script != nullptr); + REQUIRE(q.script != nullptr); const ALLSLEffects quiet(q.script); const auto firstQ = [&](LSLFunctionExpression* call) { return quiet.mayRunFirst(statementOf(call), call); }; - ensure("a quiet call before a quiet one", firstQ(q.calls("same")[0])); - ensure("a call that reads before one that writes", !firstQ(q.calls("bump")[0])); - ensure("a call that writes before a quiet one", !firstQ(q.calls("same")[1])); + UNSCOPED_INFO("a quiet call before a quiet one"); + CHECK(firstQ(q.calls("same")[0])); + UNSCOPED_INFO("a call that reads before one that writes"); + CHECK_FALSE(firstQ(q.calls("bump")[0])); + UNSCOPED_INFO("a call that writes before a quiet one"); + CHECK_FALSE(firstQ(q.calls("same")[1])); } - template<> template<> - void allsleffects_object::test<3>() + TEST_CASE_METHOD(allsleffects_data, "allsleffects: a function's changes past the script and globals it reads through calls", "[allsleffects]") { - set_test_name("what a function changes past the script, apart from what it only reads there; and the globals it reads, " - "through what it calls"); const Parsed p("integer g;\n" "integer h;\n" "key me() { return llGetKey(); }\n" @@ -231,19 +248,33 @@ namespace tut "integer near() { return look(); }\n" "integer bump() { return ++h; }\n" "default { state_entry() { me(); loud(); near(); bump(); } }\n"); - ensure("parsed", p.script != nullptr); + REQUIRE(p.script != nullptr); const ALLSLEffects effects(p.script); - ensure("reading the world is past the script", effects.ofFunction(p.function("me")).impure); - ensure("and changes nothing there", !effects.ofFunction(p.function("me")).changes); - ensure("saying something changes it", effects.ofFunction(p.function("say")).changes); - ensure("through what it calls too", effects.ofFunction(p.function("loud")).changes); - ensure("a read of a global", effects.reads(p.function("look"), p.global("g"))); - ensure("through what it calls", effects.reads(p.function("near"), p.global("g"))); - ensure("not one it does not name", !effects.reads(p.function("near"), p.global("h"))); - ensure("nor one only another function names", !effects.reads(p.function("me"), p.global("g"))); - ensure("one it steps", effects.reads(p.function("bump"), p.global("h"))); - ensure("a function the script does not define reads any", effects.reads(nullptr, p.global("g"))); + UNSCOPED_INFO("reading the world is past the script"); + CHECK(effects.ofFunction(p.function("me")).impure); + UNSCOPED_INFO("and changes nothing there"); + CHECK_FALSE(effects.ofFunction(p.function("me")).changes); + UNSCOPED_INFO("saying something changes it"); + CHECK(effects.ofFunction(p.function("say")).changes); + UNSCOPED_INFO("through what it calls too"); + CHECK(effects.ofFunction(p.function("loud")).changes); + UNSCOPED_INFO("a read of a global"); + CHECK(effects.reads(p.function("look"), p.global("g"))); + UNSCOPED_INFO("through what it calls"); + CHECK(effects.reads(p.function("near"), p.global("g"))); + UNSCOPED_INFO("not one it does not name"); + CHECK_FALSE(effects.reads(p.function("near"), p.global("h"))); + UNSCOPED_INFO("nor one only another function names"); + CHECK_FALSE(effects.reads(p.function("me"), p.global("g"))); + UNSCOPED_INFO("one it steps"); + CHECK(effects.reads(p.function("bump"), p.global("h"))); + UNSCOPED_INFO("a function the script does not define reads any"); + CHECK(effects.reads(nullptr, p.global("g"))); LSLASTNode* body = p.script->getStates()->getChild(0); - ensure("a statement's, its calls'", effects.of(body).changes && effects.of(body).impure); + { + INFO("a statement's, its calls'"); + CHECK(effects.of(body).changes); + CHECK(effects.of(body).impure); + } } } diff --git a/indra/alscript/tests/allsloptimizer_test.cpp b/indra/alscript/tests/allsloptimizer_test.cpp index 26c2a7eb9f..5f936f1ae4 100644 --- a/indra/alscript/tests/allsloptimizer_test.cpp +++ b/indra/alscript/tests/allsloptimizer_test.cpp @@ -31,7 +31,7 @@ #include "../lsl/allslservice.h" #include "../lsl/allsltraits.h" -#include "../test/lltut.h" +#include #include #include @@ -44,7 +44,7 @@ #include #include -namespace tut +namespace { struct allsloptimizer_data { @@ -58,7 +58,7 @@ namespace tut loaded = service.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error); if (!loaded) { - fail("the builtins did not load: " + error); + FAIL("the builtins did not load: " + error); } } } @@ -173,16 +173,18 @@ namespace tut // A script optimized for a target with the defaults, what it became // checked to compile there. - static std::string optimized(const std::string& source, ALLSLOptimizer::Target target) + static std::string require_optimized(const std::string& source, ALLSLOptimizer::Target target) { ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text, target != ALLSLOptimizer::Target::LSO)) { - ensure("no error in what was written: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("no error in what was written: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } return r.text; } @@ -204,206 +206,247 @@ namespace tut } }; - // Past TUT's fifty tests a group runs only what it is told it has. - typedef test_group allsloptimizer_group; - typedef allsloptimizer_group::object allsloptimizer_object; - allsloptimizer_group allsloptimizer_instance("allsloptimizer"); - - template<> template<> - void allsloptimizer_object::test<1>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: constants fold and never-assigned variables inline and unused ones go", "[allsloptimizer]") { - set_test_name("constants fold, never-assigned variables inline, and what is left unused goes"); const std::string source = "integer unused = 5;\ninteger used = 2;\n" + wrap("", " integer x = used * 3 + 1;\n llSay(0, (string)x);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); - ensure_equals("text", r.text, wrap("", " llSay(0, \"7\");\n")); - ensure("noted the global", has(r, "removed the unused global unused")); - ensure("noted the fold: " + notes(r), has(r, "folded used * 3 + 1 to 7")); - ensure("noted the local", has(r, "removed the unused local x")); - ensure("smaller", r.sizeAfter < r.sizeBefore); + CHECK(r.optimized); + CHECK(r.text == wrap("", " llSay(0, \"7\");\n")); + CHECK(has(r, "removed the unused global unused")); + UNSCOPED_INFO("noted the fold: " + notes(r)); + CHECK(has(r, "folded used * 3 + 1 to 7")); + CHECK(has(r, "removed the unused local x")); + CHECK(r.sizeAfter < r.sizeBefore); } - template<> template<> - void allsloptimizer_object::test<2>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: floats fold in single precision and print so that they read back exactly", "[allsloptimizer]") { - set_test_name("floats fold in single precision and print so that they read back exactly"); // Each variable may be assigned again before it is read, so that // it stays a variable and the fold shows in its declaration. const std::string source = wrap("", " float a = 0.1 + 0.2;\n float b = PI * 2;\n float c = 1.0 / 3;\n if (llFrand(1) < 0.5)\n {\n a = b = c = 0;\n }\n llSay(0, (string)a + (string)b + (string)c);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); + CHECK(r.optimized); // 0.1f + 0.2f is 0.3 exactly as a single, PI * 2 is 6.2831855f, // and each reads back as the same single. - ensure("a: " + r.text, r.text.find("float a = 0.3;") != std::string::npos); - ensure("b", r.text.find("float b = 6.2831855;") != std::string::npos); - ensure("c", r.text.find("float c = 0.33333334;") != std::string::npos); + UNSCOPED_INFO("as written: " + r.text); + REQUIRE(r.text.find("float a = 0.3;") != std::string::npos); + REQUIRE(r.text.find("float b = 6.2831855;") != std::string::npos); + REQUIRE(r.text.find("float c = 0.33333334;") != std::string::npos); // A float that is a whole number prints as an integer where LSL // would convert one, and stays a float where it would not. r = ALLSLOptimizer::run(wrap("", " float f = 2.0;\n list l = [2.0];\n string s = (string)2.0;\n vector v = <1.0, 2.0, 3.5>;\n if (llFrand(1) < 0.5)\n {\n f = 0; l = []; s = \"\"; v = ZERO_VECTOR;\n }\n llSay(0, (string)f + llList2CSV(l) + s + (string)v);\n"), options()); - ensure("whole float as integer: " + r.text, r.text.find("float f = 2;") != std::string::npos); + UNSCOPED_INFO("whole float as integer: " + r.text); + REQUIRE(r.text.find("float f = 2;") != std::string::npos); // As the cast of an integer on Mono, where that is smaller (ALLSLCosts::castForWholeFloat). - ensure("list element stays a float: " + r.text, r.text.find("list l = (list)((float)2);") != std::string::npos); - ensure("cast folded with six places", r.text.find("string s = \"2.000000\";") != std::string::npos); - ensure("vector components", r.text.find("vector v = <1, 2, 3.5>;") != std::string::npos); + UNSCOPED_INFO("list element stays a float: " + r.text); + REQUIRE(r.text.find("list l = (list)((float)2);") != std::string::npos); + UNSCOPED_INFO("cast folded with six places"); + REQUIRE(r.text.find("string s = \"2.000000\";") != std::string::npos); + UNSCOPED_INFO("vector components"); + REQUIRE(r.text.find("vector v = <1, 2, 3.5>;") != std::string::npos); } - template<> template<> - void allsloptimizer_object::test<3>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: integer arithmetic wraps as the VM's does and division by zero is left", "[allsloptimizer]") { - set_test_name("the arithmetic is the VM's: wrapping integers, division by zero left alone"); const std::string source = wrap("", " llSay(0, (string)(2147483647 + 1));\n llSay(0, (string)(1 / 0));\n llSay(0, (string)(-2147483648 / -1));\n llSay(0, (string)(7 / 2 * 2));\n llSay(0, (string)(1 << 40));\n llSay(0, (string)(-7 % 3));\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); - ensure("wrapped: " + r.text, r.text.find("llSay(0, \"-2147483648\");") != std::string::npos); - ensure("zero stays", r.text.find("(string)(1 / 0)") != std::string::npos); + CHECK(r.optimized); + UNSCOPED_INFO("wrapped: " + r.text); + REQUIRE(r.text.find("llSay(0, \"-2147483648\");") != std::string::npos); + UNSCOPED_INFO("zero stays"); + REQUIRE(r.text.find("(string)(1 / 0)") != std::string::npos); // On Mono, each negative number as its cast (ALLSLCosts::castForNegative). - ensure("the overflow stays: " + r.text, r.text.find("(string)(((integer)-2147483648) / ((integer)-1))") != std::string::npos); - ensure("integer division", r.text.find("llSay(0, \"6\");") != std::string::npos); - ensure("a wide shift stays", r.text.find("(string)(1 << 40)") != std::string::npos); - ensure("modulo keeps the sign", r.text.find("llSay(0, \"-1\");") != std::string::npos); + UNSCOPED_INFO("the overflow stays: " + r.text); + REQUIRE(r.text.find("(string)(((integer)-2147483648) / ((integer)-1))") != std::string::npos); + UNSCOPED_INFO("integer division"); + REQUIRE(r.text.find("llSay(0, \"6\");") != std::string::npos); + UNSCOPED_INFO("a wide shift stays"); + REQUIRE(r.text.find("(string)(1 << 40)") != std::string::npos); + UNSCOPED_INFO("modulo keeps the sign"); + REQUIRE(r.text.find("llSay(0, \"-1\");") != std::string::npos); } - template<> template<> - void allsloptimizer_object::test<4>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: pure library calls with constant arguments are evaluated and others left", "[allsloptimizer]") { - set_test_name("pure library calls with constant arguments are evaluated, others left"); const std::string source = wrap("", " integer a = llAbs(-3);\n float b = llSqrt(16);\n string c = llToUpper(\"abc\");\n string d = llGetSubString(\"abcdef\", 4, 1);\n string e = llDeleteSubString(\"abcdef\", 1, 3);\n integer f = llStringLength(\"hello\");\n string g = llEscapeURL(\"a b\");\n string h = llBase64ToString(llStringToBase64(\"hi\"));\n integer i = llList2Integer([1, 2, 3], -1);\n string j = llList2String([1, 2.5, <1,2,3>], 1);\n list k = llList2List([1, 2, 3, 4], 1, 2);\n integer l = llFloor(2.7) + llCeil(2.2);\n string m = llGetSubString(\"h\xc3\xa9llo\", 0, 1);\n integer n = llGetUnixTime();\n string o = llList2CSV([1, \"a\", 2.0]);\n if (llFrand(1) < 0.5)\n {\n a = f = i = l = n = 0; b = 0; c = d = e = g = h = j = m = o = \"\"; k = [];\n }\n llSay(0, (string)a + (string)b + c + d + e + (string)f + g + h + (string)i + j + llList2CSV(k) + (string)l + m + (string)n + o);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); - ensure("abs: " + r.text, r.text.find("integer a = 3;") != std::string::npos); - ensure("sqrt", r.text.find("float b = 4;") != std::string::npos); - ensure("upper", r.text.find("string c = \"ABC\";") != std::string::npos); - ensure("wrapped range", r.text.find("string d = \"abef\";") != std::string::npos); - ensure("delete", r.text.find("string e = \"aef\";") != std::string::npos); - ensure("length", r.text.find("integer f = 5;") != std::string::npos); - ensure("escape", r.text.find("string g = \"a%20b\";") != std::string::npos); - ensure("base64 there and back", r.text.find("string h = \"hi\";") != std::string::npos); - ensure("list get from the end", r.text.find("integer i = 3;") != std::string::npos); - ensure("list to string", r.text.find("string j = \"2.500000\";") != std::string::npos); - ensure("list slice: " + r.text, r.text.find("list k = [2, 3];") != std::string::npos); - ensure("floor and ceil", r.text.find("integer l = 5;") != std::string::npos); - ensure("not ascii, not folded", r.text.find("llGetSubString(\"h\xc3\xa9llo\", 0, 1)") != std::string::npos); - ensure("impure, not folded", r.text.find("llGetUnixTime()") != std::string::npos); - ensure("csv", r.text.find("string o = \"1, a, 2.000000\";") != std::string::npos); - } - - template<> template<> - void allsloptimizer_object::test<5>() - { - set_test_name("identities, signs, casts and conditions are simplified"); + CHECK(r.optimized); + UNSCOPED_INFO("abs: " + r.text); + REQUIRE(r.text.find("integer a = 3;") != std::string::npos); + UNSCOPED_INFO("sqrt"); + REQUIRE(r.text.find("float b = 4;") != std::string::npos); + UNSCOPED_INFO("upper"); + REQUIRE(r.text.find("string c = \"ABC\";") != std::string::npos); + UNSCOPED_INFO("wrapped range"); + REQUIRE(r.text.find("string d = \"abef\";") != std::string::npos); + UNSCOPED_INFO("delete"); + REQUIRE(r.text.find("string e = \"aef\";") != std::string::npos); + UNSCOPED_INFO("length"); + REQUIRE(r.text.find("integer f = 5;") != std::string::npos); + UNSCOPED_INFO("escape"); + REQUIRE(r.text.find("string g = \"a%20b\";") != std::string::npos); + UNSCOPED_INFO("base64 there and back"); + REQUIRE(r.text.find("string h = \"hi\";") != std::string::npos); + UNSCOPED_INFO("list get from the end"); + REQUIRE(r.text.find("integer i = 3;") != std::string::npos); + UNSCOPED_INFO("list to string"); + REQUIRE(r.text.find("string j = \"2.500000\";") != std::string::npos); + UNSCOPED_INFO("list slice: " + r.text); + REQUIRE(r.text.find("list k = [2, 3];") != std::string::npos); + UNSCOPED_INFO("floor and ceil"); + REQUIRE(r.text.find("integer l = 5;") != std::string::npos); + UNSCOPED_INFO("not ascii, not folded"); + REQUIRE(r.text.find("llGetSubString(\"h\xc3\xa9llo\", 0, 1)") != std::string::npos); + UNSCOPED_INFO("impure, not folded"); + REQUIRE(r.text.find("llGetUnixTime()") != std::string::npos); + UNSCOPED_INFO("csv"); + REQUIRE(r.text.find("string o = \"1, a, 2.000000\";") != std::string::npos); + } + + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: algebraic identities and redundant casts and conditions simplify", "[allsloptimizer]") + { const std::string source = wrap("integer g;\ninteger h;\n", " integer a = g + 0;\n integer b = g * 1;\n integer c = g - -1;\n integer d = g + -2;\n integer e = (integer)g;\n integer f = g * 0;\n integer i = (g == 0);\n if (g != 0) h = 1;\n if (!!g) h = 2;\n if (!(g == h)) h = 3;\n if (!g) h = 4; else h = 5;\n h = a + b + c + d + e + f + i;\n a = b = c = d = e = f = i = 0;\n g = h;\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); - ensure("plus zero: " + r.text, r.text.find("integer a = g;") != std::string::npos); - ensure("times one", r.text.find("integer b = g;") != std::string::npos); + CHECK(r.optimized); + UNSCOPED_INFO("plus zero: " + r.text); + REQUIRE(r.text.find("integer a = g;") != std::string::npos); + UNSCOPED_INFO("times one"); + REQUIRE(r.text.find("integer b = g;") != std::string::npos); // One more as -~g on Mono, whose ldc.i4 1 is five bytes // (ALLSLCosts::negateComplementForIncrement). - ensure("minus minus: " + r.text, r.text.find("integer c = -~g;") != std::string::npos); + UNSCOPED_INFO("minus minus: " + r.text); + REQUIRE(r.text.find("integer c = -~g;") != std::string::npos); // And on Mono, two less as ~-~-g (ALLSLCosts::complementNegateForDecrement). - ensure("plus minus: " + r.text, r.text.find("integer d = ~-~-g;") != std::string::npos); - ensure("cast", r.text.find("integer e = g;") != std::string::npos); + UNSCOPED_INFO("plus minus: " + r.text); + REQUIRE(r.text.find("integer d = ~-~-g;") != std::string::npos); + UNSCOPED_INFO("cast"); + REQUIRE(r.text.find("integer e = g;") != std::string::npos); // Left at its default on Mono (ALLSLCosts::dropIntegerDefault). - ensure("times zero: " + r.text, r.text.find("integer f;") != std::string::npos); - ensure("equals zero", r.text.find("integer i = (!g);") != std::string::npos); - ensure("not equal zero as condition", r.text.find("if (g)\n h = 1;") != std::string::npos); - ensure("double not", r.text.find("if (g)\n h = 2;") != std::string::npos); + UNSCOPED_INFO("times zero: " + r.text); + REQUIRE(r.text.find("integer f;") != std::string::npos); + UNSCOPED_INFO("equals zero"); + REQUIRE(r.text.find("integer i = (!g);") != std::string::npos); + UNSCOPED_INFO("not equal zero as condition"); + REQUIRE(r.text.find("if (g)\n h = 1;") != std::string::npos); + UNSCOPED_INFO("double not"); + REQUIRE(r.text.find("if (g)\n h = 2;") != std::string::npos); // As a ^ on Mono, where only truth counts (ALLSLCosts::xorForNotEqual). - ensure("not equals: " + r.text, r.text.find("if (g ^ h)\n h = 3;") != std::string::npos); + UNSCOPED_INFO("not equals: " + r.text); + REQUIRE(r.text.find("if (g ^ h)\n h = 3;") != std::string::npos); // Turned round off its !, then a choice of two numbers as arithmetic. - ensure("swapped, then chosen by arithmetic: " + r.text, r.text.find("h = !!g + 4;") != std::string::npos); + UNSCOPED_INFO("swapped, then chosen by arithmetic: " + r.text); + REQUIRE(r.text.find("h = !!g + 4;") != std::string::npos); } - template<> template<> - void allsloptimizer_object::test<6>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: unreachable code and unused labels and functions and states go", "[allsloptimizer]") { - set_test_name("dead code goes: after a return, under a false condition, unused labels, functions and states"); const std::string source = "f()\n{\n return;\n llSay(0, \"never\");\n}\ng()\n{\n}\n" "default\n{\n state_entry()\n {\n if (0) llSay(0, \"no\");\n if (1) llSay(0, \"yes\"); else llSay(0, \"no\");\n while (0) llSay(0, \"no\");\n @unused;\n f();\n ;\n {}\n 3;\n }\n}\nstate other\n{\n state_entry()\n {\n }\n}\n"; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); - ensure_equals("text", r.text, "f()\n{\n return;\n}\n\ndefault\n{\n state_entry()\n {\n llSay(0, \"yes\");\n f();\n }\n}\n"); - ensure("noted the return", has(r, "can never run")); - ensure("noted g", has(r, "removed the unused function g")); - ensure("noted the state", has(r, "removed the state other")); - ensure("noted the label", has(r, "nothing jumps to")); + CHECK(r.optimized); + CHECK(r.text == "f()\n{\n return;\n}\n\ndefault\n{\n state_entry()\n {\n llSay(0, \"yes\");\n f();\n }\n}\n"); + UNSCOPED_INFO("noted the return"); + CHECK(has(r, "can never run")); + CHECK(has(r, "removed the unused function g")); + CHECK(has(r, "removed the state other")); + UNSCOPED_INFO("noted the label"); + CHECK(has(r, "nothing jumps to")); } - template<> template<> - void allsloptimizer_object::test<7>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: llGetListLength as a comparison and a list literal as a sum where smaller", "[allsloptimizer]") { - set_test_name("llGetListLength becomes a comparison, and a list literal a sum, with parentheses where they matter, each where smaller"); const std::string source = wrap("list l;\n", " integer n = llGetListLength(l) + 1;\n if (llGetListLength(l)) l = [1, \"a\"];\n l = [];\n llOwnerSay((string)n);\n"); // On LSO both are smaller. ALLSLOptimizer::Options lso = options(); lso.target = ALLSLOptimizer::Target::LSO; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, lso); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); // One more as -~x on LSO (ALLSLCosts::negateComplementForIncrement). - ensure("in a sum: " + r.text, r.text.find("integer n = -~(l != []);") != std::string::npos); - ensure("as a condition", r.text.find("if (l != [])") != std::string::npos); - ensure("list add: " + r.text, r.text.find("l = (list)1 + \"a\";") != std::string::npos); - ensure("an empty list stays", r.text.find("l = [];") != std::string::npos); + UNSCOPED_INFO("in a sum: " + r.text); + REQUIRE(r.text.find("integer n = -~(l != []);") != std::string::npos); + UNSCOPED_INFO("as a condition"); + REQUIRE(r.text.find("if (l != [])") != std::string::npos); + UNSCOPED_INFO("list add: " + r.text); + REQUIRE(r.text.find("l = (list)1 + \"a\";") != std::string::npos); + UNSCOPED_INFO("an empty list stays"); + REQUIRE(r.text.find("l = [];") != std::string::npos); // On Mono the comparison is not, though one more is -~x there too; // nor is the sum here, one sum not paying for the helper it would // be the script's only use of. r = ALLSLOptimizer::run(source, options()); - ensure("no comparison on Mono: " + r.text, r.text.find("integer n = -~llGetListLength(l);") != std::string::npos && r.text.find("l != []") == std::string::npos); - ensure("one sum not worth its helper on Mono: " + r.text, r.text.find("l = [1, \"a\"];") != std::string::npos); + { + INFO("no comparison on Mono: " + r.text); + REQUIRE(r.text.find("integer n = -~llGetListLength(l);") != std::string::npos); + REQUIRE(r.text.find("l != []") == std::string::npos); + } + UNSCOPED_INFO("one sum not worth its helper on Mono: " + r.text); + REQUIRE(r.text.find("l = [1, \"a\"];") != std::string::npos); // On Luau neither. ALLSLOptimizer::Options luau = options(); luau.target = ALLSLOptimizer::Target::Luau; r = ALLSLOptimizer::run(source, luau); - ensure("neither on Luau: " + r.text, r.text.find("l = [1, \"a\"];") != std::string::npos && r.text.find("l != []") == std::string::npos); + { + INFO("neither on Luau: " + r.text); + REQUIRE(r.text.find("l = [1, \"a\"];") != std::string::npos); + REQUIRE(r.text.find("l != []") == std::string::npos); + } } - template<> template<> - void allsloptimizer_object::test<8>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: names shrink to the shortest free ones and the map says which", "[allsloptimizer]") { - set_test_name("names shrink to the shortest that are free, most used first, and the map says which"); const std::string source = "integer counter;\nbump(integer by)\n{\n counter += by;\n}\ndefault\n{\n touch_start(integer total)\n {\n bump(total);\n bump(1);\n llSay(0, (string)counter);\n }\n}\n"; ALLSLOptimizer::Options o = options(); o.shrinknames = true; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized", r.optimized); - ensure("renamed counter", r.renamed.count("counter") == 1); - ensure("renamed bump", r.renamed.count("bump") == 1); - ensure("renamed total", r.renamed.count("total") == 1); - ensure("the event stays", r.text.find("touch_start(integer ") != std::string::npos); - ensure("default stays", r.text.find("default\n{") != std::string::npos); - ensure("single letters", r.renamed["bump"].size() == 1 && r.renamed["counter"].size() == 1); - ensure("nothing of the old names", r.text.find("counter") == std::string::npos && r.text.find("bump") == std::string::npos); - ensure("noted", has(r, "renamed the function bump to")); + CHECK(r.optimized); + REQUIRE(r.renamed.count("counter") == 1); + REQUIRE(r.renamed.count("bump") == 1); + REQUIRE(r.renamed.count("total") == 1); + UNSCOPED_INFO("the event stays"); + REQUIRE(r.text.find("touch_start(integer ") != std::string::npos); + UNSCOPED_INFO("default stays"); + REQUIRE(r.text.find("default\n{") != std::string::npos); + { + INFO("single letters"); + REQUIRE(r.renamed["bump"].size() == 1); + REQUIRE(r.renamed["counter"].size() == 1); + } + { + INFO("nothing of the old names"); + REQUIRE(r.text.find("counter") == std::string::npos); + REQUIRE(r.text.find("bump") == std::string::npos); + } + CHECK(has(r, "renamed the function bump to")); } - template<> template<> - void allsloptimizer_object::test<9>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a script with errors is left alone and the map leads back", "[allsloptimizer]") { - set_test_name("a script with errors is left alone, and the map leads back"); ALLSLOptimizer::Result r = ALLSLOptimizer::run("default { state_entry() { integer x = ; } }", options()); - ensure("not optimized", !r.optimized); - ensure("an error", r.problems.size() == 1 && r.problems[0].severity == ALScriptProblem::Severity::Error); - ensure("the source as it was", r.text == "default { state_entry() { integer x = ; } }"); + CHECK_FALSE(r.optimized); + REQUIRE(r.problems.size() == 1); + CHECK(r.problems[0].severity == ALScriptProblem::Severity::Error); + UNSCOPED_INFO("the source as it was"); + CHECK(r.text == "default { state_entry() { integer x = ; } }"); const std::string source = "integer g = 3;\n\n\ndefault\n{\n state_entry()\n {\n llSay(0, (string)g);\n }\n}\n"; r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); - ensure_equals("text", r.text, "default\n{\n state_entry()\n {\n llSay(0, \"3\");\n }\n}\n"); + CHECK(r.optimized); + CHECK(r.text == "default\n{\n state_entry()\n {\n llSay(0, \"3\");\n }\n}\n"); // The llSay is on output line 4 and came from source line 7. const ALSourceMap::Loc loc = r.map.toSource(4, 8); - ensure("found", loc.found()); - ensure_equals("line", loc.line, 7); - ensure_equals("column", loc.column, 8); + REQUIRE(loc.found()); + CHECK(loc.line == 7); + CHECK(loc.column == 8); } - template<> template<> - void allsloptimizer_object::test<10>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: every golden file comes out as its expected text", "[allsloptimizer]") { - set_test_name("every golden file comes out as its expected text"); namespace fs = std::filesystem; const fs::path dir = fsyspath(AL_ALSCRIPT_TEST_DIR) / "optimizer"; S32 checked = 0; @@ -414,6 +457,7 @@ namespace tut { continue; } + CAPTURE(name); const std::string stem = entry.path().stem().string(); const fs::path expected = dir / (stem + "_expected.lsl"); const auto read = [](const fs::path& path) { @@ -429,96 +473,141 @@ namespace tut o.addstrings = first.find("addstrings") != std::string::npos; o.target = first.find("lso") != std::string::npos ? ALLSLOptimizer::Target::LSO : first.find("luau") != std::string::npos ? ALLSLOptimizer::Target::Luau : ALLSLOptimizer::Target::Mono; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized " + name + ": " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); if (const char* write = std::getenv("AL_OPTIMIZER_WRITE_EXPECTED"); write && *write) { llofstream out(expected, std::ios::binary); out << r.text; continue; } - ensure("expected file for " + name, fs::exists(expected)); - ensure_equals("output of " + name, r.text, read(expected)); + REQUIRE(fs::exists(expected)); + REQUIRE(r.text == read(expected)); ++checked; } - ensure("some golden files", checked > 0 || std::getenv("AL_OPTIMIZER_WRITE_EXPECTED")); + UNSCOPED_INFO("some golden files"); + REQUIRE((checked > 0 || std::getenv("AL_OPTIMIZER_WRITE_EXPECTED"))); } - template<> template<> - void allsloptimizer_object::test<11>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: rotation functions fold over unit rotations and leave the rest", "[allsloptimizer]") { - set_test_name("the rotation functions fold over unit rotations by the viewer's own quaternion, and leave the rest"); const std::string source = wrap("", " rotation a = llEuler2Rot(<0, 0, PI_BY_TWO>);\n vector b = llRot2Euler(<0, 0, 0.7071068, 0.7071068>);\n rotation c = llAxisAngle2Rot(<0, 0, 2>, PI);\n vector d = llRot2Axis(<0, 0, 0.7071068, 0.7071068>);\n float e = llRot2Angle(<0, 0, 0.7071068, 0.7071068>);\n vector f = llRot2Fwd(<0, 0, 0.7071068, 0.7071068>);\n vector h = llRot2Up(<0.7071068, 0, 0, 0.7071068>);\n vector i = llRot2Axis(<0, 0, 0, 1>);\n vector j = llRot2Fwd(<0, 0, 2, 2>);\n if (llFrand(1) < 0.5)\n {\n a = c = ZERO_ROTATION; b = d = f = h = i = j = ZERO_VECTOR; e = 0;\n }\n llSay(0, (string)a + (string)b + (string)c + (string)d + (string)e + (string)f + (string)h + (string)i + (string)j);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); + CHECK(r.optimized); auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; // A quarter turn about z: x and y zero, z and s a half root two. - ensure("euler to rot: " + r.text, about(numbers(r.text, "rotation a"), { 0.f, 0.f, 0.70710677f, 0.70710677f })); - ensure("rot to euler: " + r.text, about(numbers(r.text, "vector b"), { 0.f, 0.f, 1.5707964f })); - ensure("axis and angle, the axis normalised: " + r.text, about(numbers(r.text, "rotation c"), { 0.f, 0.f, 1.f, 0.f }) && !has("llAxisAngle2Rot")); - ensure("the axis back: " + r.text, about(numbers(r.text, "vector d"), { 0.f, 0.f, 1.f })); - ensure("the angle back: " + r.text, about(numbers(r.text, "float e"), { 1.5707964f })); - ensure("fwd of a quarter turn is left: " + r.text, !has("llRot2Fwd(<0, 0, 0.7071068, 0.7071068>)") && has("vector f = <")); - ensure("up of a roll: " + r.text, !has("llRot2Up(") && has("vector h = <0, ((float)-")); - ensure("no rotation has no axis: left", has("llRot2Axis(<0, 0, 0, 1>)")); - ensure("not a unit rotation: left", has("llRot2Fwd(<0, 0, 2, 2>)")); - } - template<> template<> - void allsloptimizer_object::test<12>() - { - set_test_name("llJsonGetValue folds a string or a plain number out of strict JSON, and leaves the rest to the simulator"); + UNSCOPED_INFO("euler to rot: " + r.text); + CHECK(about(numbers(r.text, "rotation a"), { 0.f, 0.f, 0.70710677f, 0.70710677f })); + UNSCOPED_INFO("rot to euler: " + r.text); + CHECK(about(numbers(r.text, "vector b"), { 0.f, 0.f, 1.5707964f })); + { + INFO("axis and angle, the axis normalised: " + r.text); + CHECK(about(numbers(r.text, "rotation c"), { 0.f, 0.f, 1.f, 0.f })); + CHECK_FALSE(has("llAxisAngle2Rot")); + } + UNSCOPED_INFO("the axis back: " + r.text); + CHECK(about(numbers(r.text, "vector d"), { 0.f, 0.f, 1.f })); + UNSCOPED_INFO("the angle back: " + r.text); + CHECK(about(numbers(r.text, "float e"), { 1.5707964f })); + { + INFO("fwd of a quarter turn is left: " + r.text); + CHECK_FALSE(has("llRot2Fwd(<0, 0, 0.7071068, 0.7071068>)")); + CHECK(has("vector f = <")); + } + { + INFO("up of a roll: " + r.text); + CHECK_FALSE(has("llRot2Up(")); + CHECK(has("vector h = <0, ((float)-")); + } + UNSCOPED_INFO("no rotation has no axis: left"); + CHECK(has("llRot2Axis(<0, 0, 0, 1>)")); + UNSCOPED_INFO("not a unit rotation: left"); + CHECK(has("llRot2Fwd(<0, 0, 2, 2>)")); + } + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: the JSON functions fold strict plain JSON and leave the rest to the simulator", "[allsloptimizer]") + { const std::string source = wrap("", " string a = llJsonGetValue(\"{\\\"name\\\": \\\"Ann\\\", \\\"tags\\\": [\\\"x\\\", 7, {\\\"k\\\": \\\"v\\\"}]}\", [\"name\"]);\n string b = llJsonGetValue(\"{\\\"tags\\\": [\\\"x\\\", -7]}\", [\"tags\", 1]);\n string c = llJsonGetValue(\"{\\\"tags\\\": [\\\"x\\\", 7, {\\\"k\\\": \\\"v\\\"}]}\", [\"tags\", 2, \"k\"]);\n string d = llJsonGetValue(\"[1.5]\", [0]);\n string e = llJsonGetValue(\"[true]\", [0]);\n string f = llJsonGetValue(\"[1]\", [3]);\n string g = llJsonGetValue(\"{\\\"a\\\": 1, \\\"a\\\": 2}\", [\"a\"]);\n string h = llJsonGetValue(\"[1,]\", [0]);\n string i = llJsonGetValue(\"[\\\"a\\\\\\\"b\\\"]\", [0]);\n if (llFrand(1) < 0.5)\n {\n a = b = c = d = e = f = g = h = i = \"\";\n }\n llSay(0, a + b + c + d + e + f + g + h + i);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); + CHECK(r.optimized); auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; - ensure("a key: " + r.text, has("string a = \"Ann\";")); - ensure("an index, a negative integer: " + r.text, has("string b = \"-7\";")); - ensure("down a path: " + r.text, has("string c = \"v\";")); - ensure("a fraction is left: " + r.text, has("string d = llJsonGetValue")); - ensure("true is its constant: " + r.text, has("string e = JSON_TRUE;")); - ensure("out of range is JSON_INVALID: " + r.text, has("string f = JSON_INVALID;")); - ensure("duplicate keys are left", has("string g = llJsonGetValue")); - ensure("a trailing comma is left", has("string h = llJsonGetValue")); - ensure("an escape unescaped: " + r.text, has("string i = \"a\\\"b\";")); + UNSCOPED_INFO("a key: " + r.text); + CHECK(has("string a = \"Ann\";")); + UNSCOPED_INFO("an index, a negative integer: " + r.text); + CHECK(has("string b = \"-7\";")); + UNSCOPED_INFO("down a path: " + r.text); + CHECK(has("string c = \"v\";")); + UNSCOPED_INFO("a fraction is left: " + r.text); + CHECK(has("string d = llJsonGetValue")); + UNSCOPED_INFO("true is its constant: " + r.text); + CHECK(has("string e = JSON_TRUE;")); + UNSCOPED_INFO("out of range is JSON_INVALID: " + r.text); + CHECK(has("string f = JSON_INVALID;")); + UNSCOPED_INFO("duplicate keys are left"); + CHECK(has("string g = llJsonGetValue")); + UNSCOPED_INFO("a trailing comma is left"); + CHECK(has("string h = llJsonGetValue")); + UNSCOPED_INFO("an escape unescaped: " + r.text); + CHECK(has("string i = \"a\\\"b\";")); // The answers that are constants are the constants' names. const std::string more = wrap("", " string a = llJsonGetValue(\"[true, null]\", [0]);\n string b = llJsonGetValue(\"[1]\", [3]);\n string c = llJsonValueType(\"{\\\"a\\\": [1, {}]}\", [\"a\", 1]);\n string d = llJsonValueType(\"{\\\"a\\\": 1}\", [\"b\"]);\n string e = llJsonValueType(\"[1,]\", []);\n string f = llList2Json(JSON_OBJECT, [\"name\", \"Ann Lee\", \"count\", 3]);\n string g = llList2Json(JSON_ARRAY, [1, \"true\"]);\n string h = llList2Json(JSON_ARRAY, [1.5]);\n list i = llJson2List(\"[1, \\\"two\\\", -3]\");\n list j = llJson2List(\"{\\\"k\\\": \\\"v\\\"}\");\n list k = llJson2List(\"[true]\");\n if (llFrand(1) < 0.5)\n {\n a = b = c = d = e = f = g = h = \"\"; i = j = k = [];\n }\n llSay(0, a + b + c + d + e + f + g + h + llList2CSV(i) + llList2CSV(j) + llList2CSV(k));\n"); ALLSLOptimizer::Result m = ALLSLOptimizer::run(more, options()); - ensure("optimized", m.optimized); + CHECK(m.optimized); auto hasm = [&m](const char* text) { return m.text.find(text) != std::string::npos; }; - ensure("true is JSON_TRUE: " + m.text, hasm("string a = JSON_TRUE;")); - ensure("a miss is JSON_INVALID: " + m.text, hasm("string b = JSON_INVALID;")); - ensure("the type of a nested object: " + m.text, hasm("string c = JSON_OBJECT;")); - ensure("the type of nothing: " + m.text, hasm("string d = JSON_INVALID;")); - ensure("malformed is left to the simulator: " + m.text, hasm("string e = llJsonValueType")); - ensure("an object from plain strings and integers: " + m.text, hasm("string f = \"{\\\"name\\\":\\\"Ann Lee\\\",\\\"count\\\":3}\";")); - ensure("a string that reads as a JSON word is left: " + m.text, hasm("string g = llList2Json")); - ensure("a float is left: " + m.text, hasm("string h = llList2Json")); - ensure("an array to a list: " + m.text, hasm("list i = [1, \"two\", ((integer)-3)];")); - ensure("an object to a list: " + m.text, hasm("list j = [\"k\", \"v\"];")); - ensure("a true in it is left: " + m.text, hasm("list k = llJson2List")); + UNSCOPED_INFO("true is JSON_TRUE: " + m.text); + CHECK(hasm("string a = JSON_TRUE;")); + UNSCOPED_INFO("a miss is JSON_INVALID: " + m.text); + CHECK(hasm("string b = JSON_INVALID;")); + UNSCOPED_INFO("the type of a nested object: " + m.text); + CHECK(hasm("string c = JSON_OBJECT;")); + UNSCOPED_INFO("the type of nothing: " + m.text); + CHECK(hasm("string d = JSON_INVALID;")); + UNSCOPED_INFO("malformed is left to the simulator: " + m.text); + CHECK(hasm("string e = llJsonValueType")); + UNSCOPED_INFO("an object from plain strings and integers: " + m.text); + CHECK(hasm("string f = \"{\\\"name\\\":\\\"Ann Lee\\\",\\\"count\\\":3}\";")); + UNSCOPED_INFO("a string that reads as a JSON word is left: " + m.text); + CHECK(hasm("string g = llList2Json")); + UNSCOPED_INFO("a float is left: " + m.text); + CHECK(hasm("string h = llList2Json")); + UNSCOPED_INFO("an array to a list: " + m.text); + CHECK(hasm("list i = [1, \"two\", ((integer)-3)];")); + UNSCOPED_INFO("an object to a list: " + m.text); + CHECK(hasm("list j = [\"k\", \"v\"];")); + UNSCOPED_INFO("a true in it is left: " + m.text); + CHECK(hasm("list k = llJson2List")); // A nested value comes out written the compact way; a set puts a // value in, at a key or an index, where the document is plain. const std::string sets = wrap("", " string a = llJsonGetValue(\"{\\\"a\\\": [1, {\\\"b\\\": true}, \\\"c\\\"]}\", [\"a\"]);\n string b = llJsonSetValue(\"{\\\"a\\\": 1}\", [\"b\"], \"two\");\n string c = llJsonSetValue(\"[1, 2]\", [1], \"9\");\n string d = llJsonSetValue(\"[1, 2]\", [JSON_APPEND], JSON_TRUE);\n string e = llJsonSetValue(\"[1, 2]\", [5], \"9\");\n string f = llJsonSetValue(\"{}\", [\"a\", \"b\"], \"x\");\n string g = llJsonSetValue(\"[1]\", [0], \"1.5\");\n string h = llJsonGetValue(\"[1.5]\", []);\n if (llFrand(1) < 0.5)\n {\n a = b = c = d = e = f = g = h = \"\";\n }\n llSay(0, a + b + c + d + e + f + g + h);\n"); ALLSLOptimizer::Result n = ALLSLOptimizer::run(sets, options()); - ensure("optimized", n.optimized); + CHECK(n.optimized); auto hasn = [&n](const char* text) { return n.text.find(text) != std::string::npos; }; - ensure("a nested value, compact: " + n.text, hasn("string a = \"[1,{\\\"b\\\":true},\\\"c\\\"]\";")); - ensure("a new key at the end: " + n.text, hasn("string b = \"{\\\"a\\\":1,\\\"b\\\":\\\"two\\\"}\";")); - ensure("an index replaced with a number: " + n.text, hasn("string c = \"[1,9]\";")); - ensure("appended by JSON_APPEND with a constant: " + n.text, hasn("string d = \"[1,2,true]\";")); - ensure("past the end is JSON_INVALID: " + n.text, hasn("string e = JSON_INVALID;")); - ensure("a path to create is left: " + n.text, hasn("string f = llJsonSetValue")); - ensure("a value that is a fraction is left: " + n.text, hasn("string g = llJsonSetValue")); - ensure("a document with a fraction is left: " + n.text, hasn("string h = llJsonGetValue")); + UNSCOPED_INFO("a nested value, compact: " + n.text); + CHECK(hasn("string a = \"[1,{\\\"b\\\":true},\\\"c\\\"]\";")); + UNSCOPED_INFO("a new key at the end: " + n.text); + CHECK(hasn("string b = \"{\\\"a\\\":1,\\\"b\\\":\\\"two\\\"}\";")); + UNSCOPED_INFO("an index replaced with a number: " + n.text); + CHECK(hasn("string c = \"[1,9]\";")); + UNSCOPED_INFO("appended by JSON_APPEND with a constant: " + n.text); + CHECK(hasn("string d = \"[1,2,true]\";")); + UNSCOPED_INFO("past the end is JSON_INVALID: " + n.text); + CHECK(hasn("string e = JSON_INVALID;")); + UNSCOPED_INFO("a path to create is left: " + n.text); + CHECK(hasn("string f = llJsonSetValue")); + UNSCOPED_INFO("a value that is a fraction is left: " + n.text); + CHECK(hasn("string g = llJsonSetValue")); + UNSCOPED_INFO("a document with a fraction is left: " + n.text); + CHECK(hasn("string h = llJsonGetValue")); const std::string dels = wrap("", " string a = llJsonSetValue(\"{\\\"a\\\": 1, \\\"b\\\": 2}\", [\"a\"], JSON_DELETE);\n string b = llJsonSetValue(\"[1, 2, 3]\", [1], JSON_DELETE);\n string c = llJsonSetValue(\"[1, 2, 3]\", [7], JSON_DELETE);\n if (llFrand(1) < 0.5)\n {\n a = b = c = \"\";\n }\n llSay(0, a + b + c);\n"); ALLSLOptimizer::Result q = ALLSLOptimizer::run(dels, options()); auto hasq = [&q](const char* text) { return q.text.find(text) != std::string::npos; }; - ensure("a key deleted: " + q.text, hasq("string a = \"{\\\"b\\\":2}\";")); - ensure("an index deleted: " + q.text, hasq("string b = \"[1,3]\";")); - ensure("deleting what is not there is the simulator's: " + q.text, hasq("string c = llJsonSetValue")); + UNSCOPED_INFO("a key deleted: " + q.text); + CHECK(hasq("string a = \"{\\\"b\\\":2}\";")); + UNSCOPED_INFO("an index deleted: " + q.text); + CHECK(hasq("string b = \"[1,3]\";")); + UNSCOPED_INFO("deleting what is not there is the simulator's: " + q.text); + CHECK(hasq("string c = llJsonSetValue")); } - template<> template<> - void allsloptimizer_object::test<13>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a function called once goes in as a block or as its returned expression", "[allsloptimizer]") { - set_test_name("a function called once is put in its place: a void one as a block with its parameters as locals, a one-return one as its expression"); const std::string source = "integer g;\n" "say(string what, integer n)\n" @@ -554,38 +643,63 @@ namespace tut o.dcr = false; o.constfold = false; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); // The inliner's own text is what the optimizer read. const ALLSLInliner::Result put = ALLSLInliner::run(source); - ensure_equals("five put in place", put.inlined, 5); - ensure("the block where the call was: " + put.text, - put.text.find(" {\nstring what = \"hi\";\ninteger n = i;\n\n integer i_1 = n * 2;\n llSay(0, what + (string)i_1);\n\n}\n") != std::string::npos); - ensure("the expression where the other call was, the temporaries before the statement: " + put.text, - put.text.find(" integer _t_1 = g;\n integer _t_2 = i;\n g = (i * 2 + g) + (_t_1 + _t_1 + _t_1) + (_t_2 + _t_2 + _t_2);") != std::string::npos); + CHECK(put.inlined == 5); + UNSCOPED_INFO("the block where the call was: " + put.text); + REQUIRE(put.text.find(" {\nstring what = \"hi\";\ninteger n = i;\n\n integer i_1 = n * 2;\n llSay(0, what + (string)i_1);\n\n}\n") != std::string::npos); + UNSCOPED_INFO("the expression where the other call was, the temporaries before the statement: " + put.text); + REQUIRE(put.text.find(" integer _t_1 = g;\n integer _t_2 = i;\n g = (i * 2 + g) + (_t_1 + _t_1 + _t_1) + (_t_2 + _t_2 + _t_2);") != std::string::npos); auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; - ensure("say went into a block, its i renamed past the caller's: " + r.text, has("integer i_1 = n * 2;") && has("llSay(0, what + (string)i_1);")); - ensure("the parameters became locals set to the arguments: " + r.text, has("string what = \"hi\";") && has("integer n = i;")); - ensure("say is gone: " + r.text, !has("say(string what")); - ensure("twice became its expression with the argument in: " + r.text, has("(i * 2 + g)")); - ensure("thrice reads its parameter thrice, so each name is read once into a temporary before the statement: " + r.text, - has("integer _t_1 = g;") && has("integer _t_2 = i;") && has("(_t_1 + _t_1 + _t_1)") && has("(_t_2 + _t_2 + _t_2)") && !has("integer thrice(integer x)")); - ensure("loop went in with its label and jump given a fresh name: " + r.text, !has("loop();") && !has("loop()\n") && has("@again_1;") && has("jump again_1;")); + { + INFO("say went into a block, its i renamed past the caller's: " + r.text); + CHECK(has("integer i_1 = n * 2;")); + CHECK(has("llSay(0, what + (string)i_1);")); + } + { + INFO("the parameters became locals set to the arguments: " + r.text); + CHECK(has("string what = \"hi\";")); + CHECK(has("integer n = i;")); + } + UNSCOPED_INFO("say is gone: " + r.text); + CHECK_FALSE(has("say(string what")); + UNSCOPED_INFO("twice became its expression with the argument in: " + r.text); + CHECK(has("(i * 2 + g)")); + { + INFO("thrice reads its parameter thrice, so each name is read once into a temporary before the statement: " + r.text); + CHECK(has("integer _t_1 = g;")); + CHECK(has("integer _t_2 = i;")); + CHECK(has("(_t_1 + _t_1 + _t_1)")); + CHECK(has("(_t_2 + _t_2 + _t_2)")); + CHECK_FALSE(has("integer thrice(integer x)")); + } + { + INFO("loop went in with its label and jump given a fresh name: " + r.text); + CHECK_FALSE(has("loop();")); + CHECK_FALSE(has("loop()\n")); + CHECK(has("@again_1;")); + CHECK(has("jump again_1;")); + } S32 said = 0; for (const ALScriptProblem& p : r.problems) { said += p.severity == ALScriptProblem::Severity::Note && p.message.find("in place of") != std::string::npos; } - ensure_equals("five notes: say, twice, loop, and thrice twice", said, 5); + UNSCOPED_INFO("five notes: say, twice, loop, and thrice twice"); + CHECK(said == 5); // The map leads from the block back to the function's own lines. const size_t at = r.text.find("llSay(0, what + (string)i_1);"); - ensure("found", at != std::string::npos); + REQUIRE(at != std::string::npos); S32 line = 0; for (size_t i = 0; i < at; ++i) { line += r.text[i] == '\n'; } const ALSourceMap::Loc from = r.map.toSource(line, 8); - ensure_equals("the llSay came from the function's fifth line", from.line, 4); + UNSCOPED_INFO("the llSay came from the function's fifth line"); + CHECK(from.line == 4); // With the folds on, a note about the block's code points at the // function's own lines. o.constfold = true; @@ -598,12 +712,11 @@ namespace tut folded_in_body = p.line == 3; } } - ensure("a note on the body's code is at the function's line: " + notes(r), folded_in_body); + UNSCOPED_INFO("a note on the body's code is at the function's line: " + notes(r)); + CHECK(folded_in_body); } - template<> template<> - void allsloptimizer_object::test<14>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a small expression function goes everywhere and a marked one at any size", "[allsloptimizer]") { - set_test_name("a small function returning an expression goes in every place, and a marked one whatever its size"); const std::string source = "integer sq(integer x)\n" "{\n" @@ -629,22 +742,35 @@ namespace tut o.dcr = false; o.constfold = false; ALLSLInliner::Result put = ALLSLInliner::run(source); - ensure("sq went into both places, once each round: " + put.text, put.text.find("integer a = (2 * 2) + (3 * 3);") != std::string::npos); - ensure("and is gone", put.text.find("integer sq(") == std::string::npos); - ensure("shout, called twice and unmarked, stays: " + put.text, put.text.find("shout(\"x\");") != std::string::npos && put.text.find("shout(string what)") != std::string::npos); - ensure_equals("two rounds", put.inlined, 2); + UNSCOPED_INFO("sq went into both places, once each round: " + put.text); + REQUIRE(put.text.find("integer a = (2 * 2) + (3 * 3);") != std::string::npos); + UNSCOPED_INFO("and is gone"); + REQUIRE(put.text.find("integer sq(") == std::string::npos); + { + INFO("shout, called twice and unmarked, stays: " + put.text); + REQUIRE(put.text.find("shout(\"x\");") != std::string::npos); + REQUIRE(put.text.find("shout(string what)") != std::string::npos); + } + UNSCOPED_INFO("two rounds"); + CHECK(put.inlined == 2); put = ALLSLInliner::run(source, { "shout" }); - ensure("marked, shout goes into both places: " + put.text, - put.text.find("string what = \"x\";") != std::string::npos && put.text.find("string what = \"y\";") != std::string::npos && - put.text.find("shout(string what)") == std::string::npos); - ensure_equals("four rounds", put.inlined, 4); + { + INFO("marked, shout goes into both places: " + put.text); + REQUIRE(put.text.find("string what = \"x\";") != std::string::npos); + REQUIRE(put.text.find("string what = \"y\";") != std::string::npos); + REQUIRE(put.text.find("shout(string what)") == std::string::npos); + } + UNSCOPED_INFO("four rounds"); + CHECK(put.inlined == 4); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("the optimizer reads the result: " + notes(r), r.optimized && r.text.find("(2 * 2) + (3 * 3)") != std::string::npos); + { + INFO("the optimizer reads the result: " + notes(r)); + CHECK(r.optimized); + REQUIRE(r.text.find("(2 * 2) + (3 * 3)") != std::string::npos); + } } - template<> template<> - void allsloptimizer_object::test<15>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: returns become jumps to an end label and a round skips overlapping calls", "[allsloptimizer]") { - set_test_name("a return in the body becomes a jump to a label that ends the block, and a round takes every call that does not cross another's edit"); const std::string source = "check(integer n)\n" "{\n" @@ -674,25 +800,39 @@ namespace tut "}\n"; ALLSLInliner::Result put = ALLSLInliner::run(source); auto has = [&put](const char* text) { return put.text.find(text) != std::string::npos; }; - ensure("the returns are jumps: " + put.text, has("if (n < 0) jump _ret_1;") && has(" jump _ret_1;")); - ensure("to a label that ends the block: " + put.text, has("@_ret_1;\n}") || has("@_ret_1;\n }")); + { + INFO("the returns are jumps: " + put.text); + CHECK(has("if (n < 0) jump _ret_1;")); + CHECK(has(" jump _ret_1;")); + } + UNSCOPED_INFO("to a label that ends the block: " + put.text); + CHECK((has("@_ret_1;\n}") || has("@_ret_1;\n }"))); const ALLSLInliner::Result trailing = ALLSLInliner::run("f()\n{\n llSay(0, \"x\");\n return;\n}\ndefault\n{\n state_entry()\n {\n f();\n }\n}\n"); - ensure("a return that ends the body is just dropped: " + trailing.text, trailing.text.find("jump") == std::string::npos && trailing.text.find("@_ret") == std::string::npos && - trailing.text.find("llSay(0, \"x\");") != std::string::npos); - ensure("check is gone", !has("check(integer n)")); + { + INFO("a return that ends the body is just dropped: " + trailing.text); + REQUIRE(trailing.text.find("jump") == std::string::npos); + REQUIRE(trailing.text.find("@_ret") == std::string::npos); + REQUIRE(trailing.text.find("llSay(0, \"x\");") != std::string::npos); + } + UNSCOPED_INFO("check is gone"); + CHECK_FALSE(has("check(integer n)")); // outer's one call goes in the first round; inner's call is inside // outer, whose lines go that round, so inner waits for the next. - ensure("inner went in the end: " + put.text, has("integer k = 7;") && !has("inner(integer k)") && !has("outer()\n")); - ensure_equals("three calls went", put.inlined, 3); + { + INFO("inner went in the end: " + put.text); + CHECK(has("integer k = 7;")); + CHECK_FALSE(has("inner(integer k)")); + CHECK_FALSE(has("outer()\n")); + } + CHECK(put.inlined == 3); ALLSLOptimizer::Options o = options(); o.inlining = true; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("the optimizer reads it: " + notes(r), r.optimized); + UNSCOPED_INFO("the optimizer reads it: " + notes(r)); + CHECK(r.optimized); } - template<> template<> - void allsloptimizer_object::test<16>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: pure arguments go in directly or via a temporary and others force a block", "[allsloptimizer]") { - set_test_name("an argument that changes nothing goes in as a name does, a temporary where it is read twice; one that may change something keeps the expression out, and a function called once goes as a block"); const std::string source = "integer g;\n" "integer both(integer x)\n" @@ -720,19 +860,33 @@ namespace tut "}\n"; const ALLSLInliner::Result put = ALLSLInliner::run(source); auto has = [&put](const char* text) { return put.text.find(text) != std::string::npos; }; - ensure("a pure call read twice is made once into a temporary: " + put.text, has(" integer _t_1 = llAbs(g);\n integer a = (_t_1 + _t_1);")); - ensure("an expression read once goes in, in parentheses: " + put.text, has("integer b = ((g * 2) + 1);")); - ensure("so does a pure call: " + put.text, has("integer c = ((llAbs(g)) + 1);")); + UNSCOPED_INFO("a pure call read twice is made once into a temporary: " + put.text); + CHECK(has(" integer _t_1 = llAbs(g);\n integer a = (_t_1 + _t_1);")); + UNSCOPED_INFO("an expression read once goes in, in parentheses: " + put.text); + CHECK(has("integer b = ((g * 2) + 1);")); + UNSCOPED_INFO("so does a pure call: " + put.text); + CHECK(has("integer c = ((llAbs(g)) + 1);")); // An argument that may change something, or whose answer may // change, cannot go into an expression; a function called once // goes from a block before the statement instead, its value set by // the body. - ensure("bump, called once, is set by its body before the statement, and once takes its value as a name: " + put.text, - has("integer _r_1;") && has("_r_1 = ++g;") && has("integer d = (_r_1 + 1);") && !has("integer once(integer x)")); - ensure("both, its other call gone, sets e itself, the time read once into its parameter: " + put.text, - has("integer e;") && has("integer x = llRound(llGetTime());") && has("e = x + x;") && !has("integer both(integer x)")); - ensure("no stray ; where the declaration stood: " + put.text, !has("};")); - ensure_equals("six went", put.inlined, 6); + { + INFO("bump, called once, is set by its body before the statement, and once takes its value as a name: " + put.text); + CHECK(has("integer _r_1;")); + CHECK(has("_r_1 = ++g;")); + CHECK(has("integer d = (_r_1 + 1);")); + CHECK_FALSE(has("integer once(integer x)")); + } + { + INFO("both, its other call gone, sets e itself, the time read once into its parameter: " + put.text); + CHECK(has("integer e;")); + CHECK(has("integer x = llRound(llGetTime());")); + CHECK(has("e = x + x;")); + CHECK_FALSE(has("integer both(integer x)")); + } + UNSCOPED_INFO("no stray ; where the declaration stood: " + put.text); + CHECK_FALSE(has("};")); + CHECK(put.inlined == 6); // A temporary above an if whose condition has the call, and above // a for whose first part has it; not above a while, whose // condition is read every time round, nor an else-if. @@ -758,17 +912,18 @@ namespace tut "}\n"; const ALLSLInliner::Result above = ALLSLInliner::run(heads); auto got = [&above](const char* text) { return above.text.find(text) != std::string::npos; }; - ensure("above the if: " + above.text, got(" integer _t_1 = llAbs(g);\n if ((_t_1 + _t_1) > 3)")); - ensure("above the for, an assignment after the call's part not minded: " + above.text, - got(" integer _t_2 = llAbs(g);\n for (i = (_t_2 + _t_2), g = 0; i < 9; ++i) g++;")); - ensure("the while keeps its call: " + above.text, got("while (both(llAbs(g)) < 9) g++;")); - ensure("so does the else-if: " + above.text, got("else if (both(llAbs(g))) g = 2;")); - ensure_equals("two went", above.inlined, 2); - } - template<> template<> - void allsloptimizer_object::test<17>() + UNSCOPED_INFO("above the if: " + above.text); + CHECK(got(" integer _t_1 = llAbs(g);\n if ((_t_1 + _t_1) > 3)")); + UNSCOPED_INFO("above the for, an assignment after the call's part not minded: " + above.text); + CHECK(got(" integer _t_2 = llAbs(g);\n for (i = (_t_2 + _t_2), g = 0; i < 9; ++i) g++;")); + UNSCOPED_INFO("the while keeps its call: " + above.text); + CHECK(got("while (both(llAbs(g)) < 9) g++;")); + UNSCOPED_INFO("so does the else-if: " + above.text); + CHECK(got("else if (both(llAbs(g))) g = 2;")); + CHECK(above.inlined == 2); + } + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: recursive functions stay and a label after the call is the return's target", "[allsloptimizer]") { - set_test_name("a function that reaches itself is never put in place, marked or not; a label after the call's statement is the return's target"); const std::string source = "ping(integer n)\n" "{\n" @@ -803,36 +958,54 @@ namespace tut "}\n"; const ALLSLInliner::Result put = ALLSLInliner::run(source, { "ping", "pong", "self" }); auto has = [&put](const char* text) { return put.text.find(text) != std::string::npos; }; - ensure("ping and pong, each reaching the other, stay: " + put.text, has("ping(integer n)") && has("pong(integer n)") && has(" ping(3);")); - ensure("self stays: " + put.text, has("self(integer n)") && has(" self(3);")); - ensure("early went in, its return a jump to the label already there: " + put.text, has("if (n < 0) jump next;") && !has("_ret") && has(" @next;")); - ensure_equals("one went", put.inlined, 1); + { + INFO("ping and pong, each reaching the other, stay: " + put.text); + CHECK(has("ping(integer n)")); + CHECK(has("pong(integer n)")); + CHECK(has(" ping(3);")); + } + { + INFO("self stays: " + put.text); + CHECK(has("self(integer n)")); + CHECK(has(" self(3);")); + } + { + INFO("early went in, its return a jump to the label already there: " + put.text); + CHECK(has("if (n < 0) jump next;")); + CHECK_FALSE(has("_ret")); + CHECK(has(" @next;")); + } + CHECK(put.inlined == 1); } - template<> template<> - void allsloptimizer_object::test<18>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a run without notes says nothing and one out of budget stops where it is", "[allsloptimizer]") { - set_test_name("a run nobody reads the notes of says nothing, and one that reaches its budget stops where it is"); const std::string source = wrap("", " integer a = 1 + 2;\n integer b = 3 * 4;\n llSay(0, (string)(a + b));\n"); ALLSLOptimizer::Options o = options(); ALLSLOptimizer::Result said = ALLSLOptimizer::run(source, o); - ensure("the folds are said", !said.problems.empty()); + UNSCOPED_INFO("the folds are said"); + REQUIRE_FALSE(said.problems.empty()); o.notes = false; ALLSLOptimizer::Result quiet = ALLSLOptimizer::run(source, o); - ensure("nothing said", quiet.problems.empty()); - ensure_equals("and the same text either way", quiet.text, said.text); + UNSCOPED_INFO("nothing said"); + REQUIRE(quiet.problems.empty()); + UNSCOPED_INFO("and the same text either way"); + CHECK(quiet.text == said.text); // A budget too small for a round stops before the first, and // says so where the notes are read. o.notes = true; o.visitBudget = 1; ALLSLOptimizer::Result stopped = ALLSLOptimizer::run(source, o); - ensure("it stopped early", stopped.stoppedEarly); - ensure("and said so: " + notes(stopped), !stopped.problems.empty() && stopped.problems.back().key == std::string("OptimizerStoppedEarly")); - ensure("what it has stands", !stopped.text.empty()); + CHECK(stopped.stoppedEarly); + { + INFO("and said so: " + notes(stopped)); + REQUIRE_FALSE(stopped.problems.empty()); + CHECK(stopped.problems.back().key == std::string("OptimizerStoppedEarly")); + } + UNSCOPED_INFO("what it has stands"); + REQUIRE_FALSE(stopped.text.empty()); } - template<> template<> - void allsloptimizer_object::test<19>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: adding to a list never drops a zero or empty string or float element", "[allsloptimizer]") { - set_test_name("a list plus anything is one element longer: no zero, empty string or float is dropped from one, nor a literal's element"); const std::string source = wrap("", " list l = llGetPrimitiveParams([PRIM_SIZE]);\n" " l = l + 0;\n" @@ -845,30 +1018,43 @@ namespace tut " llSetPrimitiveParams([PRIM_TEXTURE, 0, s, <1, 1, 0>, ZERO_VECTOR, 0.0]);\n" " llSay(n, llList2CSV(l) + s);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized", r.optimized); - ensure("a zero appended stays: " + r.text, r.text.find("l = l + 0;") != std::string::npos); - ensure("an empty string appended stays: " + r.text, r.text.find("l = l + \"\";") != std::string::npos); - ensure("a zero put in front stays: " + r.text, r.text.find("l = ((float)0) + l;") != std::string::npos); - ensure("a negative appended is not turned into a subtraction: " + r.text, r.text.find("l - 1") == std::string::npos); - ensure("an empty list adds nothing: " + r.text, r.text.find("l + [];") == std::string::npos); - ensure("nor an empty string to a string: " + r.text, r.text.find("llList2String(l, 0) + \"\"") == std::string::npos); + CHECK(r.optimized); + UNSCOPED_INFO("a zero appended stays: " + r.text); + REQUIRE(r.text.find("l = l + 0;") != std::string::npos); + UNSCOPED_INFO("an empty string appended stays: " + r.text); + REQUIRE(r.text.find("l = l + \"\";") != std::string::npos); + UNSCOPED_INFO("a zero put in front stays: " + r.text); + REQUIRE(r.text.find("l = ((float)0) + l;") != std::string::npos); + UNSCOPED_INFO("a negative appended is not turned into a subtraction: " + r.text); + REQUIRE(r.text.find("l - 1") == std::string::npos); + UNSCOPED_INFO("an empty list adds nothing: " + r.text); + REQUIRE(r.text.find("l + [];") == std::string::npos); + UNSCOPED_INFO("nor an empty string to a string: " + r.text); + REQUIRE(r.text.find("llList2String(l, 0) + \"\"") == std::string::npos); // The count is read once, and goes where it is read. - ensure("nor a zero to an integer: " + r.text, r.text.find("llGetListLength(l) + 0") == std::string::npos && r.text.find("l != [] + 0") == std::string::npos && - r.text.find("integer n") == std::string::npos); + { + INFO("nor a zero to an integer: " + r.text); + REQUIRE(r.text.find("llGetListLength(l) + 0") == std::string::npos); + REQUIRE(r.text.find("l != [] + 0") == std::string::npos); + REQUIRE(r.text.find("integer n") == std::string::npos); + } // The literal's elements, however it is written now, all there: // the texture's face and the glow. const size_t call = r.text.find("llSetPrimitiveParams("); - ensure("the call", call != std::string::npos); + REQUIRE(call != std::string::npos); const std::string args = r.text.substr(call, r.text.find(';', call) - call); - ensure("the face, a zero: " + args, args.find("PRIM_TEXTURE + 0") != std::string::npos || args.find("PRIM_TEXTURE, 0") != std::string::npos); - ensure("and the last element, a float: " + args, args.find("ZERO_VECTOR + ((float)0)") != std::string::npos || - args.find("ZERO_VECTOR, ((float)0)") != std::string::npos); + UNSCOPED_INFO("the face, a zero: " + args); + REQUIRE((args.find("PRIM_TEXTURE + 0") != std::string::npos || args.find("PRIM_TEXTURE, 0") != std::string::npos)); + UNSCOPED_INFO("and the last element, a float: " + args); + REQUIRE((args.find("ZERO_VECTOR + ((float)0)") != std::string::npos || + args.find("ZERO_VECTOR, ((float)0)") != std::string::npos)); // And what it wrote compiles. ALLSLService service; const ALScriptProblems said = service.check(r.text); for (const ALScriptProblem& p : said) { - ensure("no error in what was written: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("no error in what was written: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } // The preprocessor's lazy-list helper grows its list one zero at a @@ -876,14 +1062,14 @@ namespace tut const std::string lazy = "list lazy_list_set(list L, integer i, list v)\n{\n while (llGetListLength(L) < i)\n L = L + 0;\n return llListReplaceList(L, v, i, i);\n}\n" + wrap("", " list x;\n x = lazy_list_set(x, 3, [1]);\n llSay(0, llList2CSV(x));\n"); r = ALLSLOptimizer::run(lazy, options()); - ensure("the list still grows: " + r.text, r.text.find("L = L + 0;") != std::string::npos); - ensure("rather than standing still", r.text.find("L = L;") == std::string::npos); + UNSCOPED_INFO("the list still grows: " + r.text); + REQUIRE(r.text.find("L = L + 0;") != std::string::npos); + UNSCOPED_INFO("rather than standing still"); + REQUIRE(r.text.find("L = L;") == std::string::npos); } - template<> template<> - void allsloptimizer_object::test<20>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: not-equal replaces a negated equals only where it means the same", "[allsloptimizer]") { - set_test_name("not-equal only takes the place of not-equals where the two say the same: numbers, or any two that are compared not-equal"); const std::string source = wrap("", " list a = llGetPrimitiveParams([PRIM_SIZE]);\n" " list b = llGetPrimitiveParams([PRIM_TYPE]);\n" @@ -895,15 +1081,16 @@ namespace tut " llSay(0, (string)(lists + same + ints));\n" " a = b = [];\n i = j = 0;\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("a list's not-equal is the difference of the lengths, so !(a == b) stays: " + r.text, r.text.find("!(a == b)") != std::string::npos); + UNSCOPED_INFO("a list's not-equal is the difference of the lengths, so !(a == b) stays: " + r.text); + REQUIRE(r.text.find("!(a == b)") != std::string::npos); // Each read once, and gone where it is read. - ensure("!(a != b) is a == b for lists too: " + r.text, r.text.find("+ (a == b) +") != std::string::npos); - ensure("and for integers !(i == j) is i != j: " + r.text, r.text.find("(i != j)") != std::string::npos); + UNSCOPED_INFO("!(a != b) is a == b for lists too: " + r.text); + REQUIRE(r.text.find("+ (a == b) +") != std::string::npos); + UNSCOPED_INFO("and for integers !(i == j) is i != j: " + r.text); + REQUIRE(r.text.find("(i != j)") != std::string::npos); } - template<> template<> - void allsloptimizer_object::test<21>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a function stays called where a caller's local shadows a global it reads", "[allsloptimizer]") { - set_test_name("a function goes in place only where the globals it reads are the same there: a caller's local of the name keeps the call"); const std::string source = "integer count;\n" "bump()\n" @@ -930,21 +1117,31 @@ namespace tut "}\n"; const ALLSLInliner::Result put = ALLSLInliner::run(source, { "bump", "plus" }); const size_t touch = put.text.find("touch_start"); - ensure("both events there", touch != std::string::npos); + UNSCOPED_INFO("both events there"); + REQUIRE(touch != std::string::npos); const std::string entry = put.text.substr(0, touch); const std::string touched = put.text.substr(touch); - ensure("where count is the caller's own, the calls stay: " + entry, - entry.find(" bump();") != std::string::npos && entry.find("plus(1)") != std::string::npos); - ensure("where it is the global, bump goes in: " + touched, touched.find("bump();") == std::string::npos && touched.find("count = count + 1;") != std::string::npos); - ensure("and plus: " + touched, touched.find("(n + count)") != std::string::npos); - ensure("so both functions stay, for the calls that kept them: " + put.text, - put.text.find("bump()\n{") != std::string::npos && put.text.find("integer plus(integer x)") != std::string::npos); + { + INFO("where count is the caller's own, the calls stay: " + entry); + REQUIRE(entry.find(" bump();") != std::string::npos); + REQUIRE(entry.find("plus(1)") != std::string::npos); + } + { + INFO("where it is the global, bump goes in: " + touched); + REQUIRE(touched.find("bump();") == std::string::npos); + REQUIRE(touched.find("count = count + 1;") != std::string::npos); + } + UNSCOPED_INFO("and plus: " + touched); + REQUIRE(touched.find("(n + count)") != std::string::npos); + { + INFO("so both functions stay, for the calls that kept them: " + put.text); + REQUIRE(put.text.find("bump()\n{") != std::string::npos); + REQUIRE(put.text.find("integer plus(integer x)") != std::string::npos); + } } - template<> template<> - void allsloptimizer_object::test<22>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: code after a jump or in a dead branch stays when it holds the jump's label", "[allsloptimizer]") { - set_test_name("what follows a jump stays where it holds the label the jump goes to, and so does a branch never taken that holds one"); // A jump in SL goes to the last label of its name in the function, // whatever block it is in: here, the one in the block after it. const std::string source = wrap("", @@ -971,27 +1168,33 @@ namespace tut " if (llGetUnixTime() > 0) jump b;\n" " jump c;\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; - ensure("the block the jump lands in stays: " + r.text, has("llOwnerSay(\"second\");")); - ensure("and what follows it: " + r.text, has("llOwnerSay(\"third\");")); - ensure("the if whose branch a jump lands in stays: " + r.text, has("llOwnerSay(\"fourth\");")); - ensure("and the loop whose body one does: " + r.text, has("llOwnerSay(\"fifth\");")); - ensure("nothing said to be unreachable: " + notes(r), notes(r).find("can never run") == std::string::npos); + UNSCOPED_INFO("the block the jump lands in stays: " + r.text); + CHECK(has("llOwnerSay(\"second\");")); + UNSCOPED_INFO("and what follows it: " + r.text); + CHECK(has("llOwnerSay(\"third\");")); + UNSCOPED_INFO("the if whose branch a jump lands in stays: " + r.text); + CHECK(has("llOwnerSay(\"fourth\");")); + UNSCOPED_INFO("and the loop whose body one does: " + r.text); + CHECK(has("llOwnerSay(\"fifth\");")); + UNSCOPED_INFO("nothing said to be unreachable: " + notes(r)); + REQUIRE(notes(r).find("can never run") == std::string::npos); // Where nothing jumps into it, it is still gone; and a jump from // inside it to its own label is no way in. r = ALLSLOptimizer::run(wrap("", " return;\n {\n @x;\n llOwnerSay(\"never\");\n }\n"), options()); - ensure("a block after a return that nothing jumps into goes: " + r.text, r.text.find("never") == std::string::npos); + UNSCOPED_INFO("a block after a return that nothing jumps into goes: " + r.text); + REQUIRE(r.text.find("never") == std::string::npos); r = ALLSLOptimizer::run(wrap("", " integer i;\n return;\n {\n @again;\n llOwnerSay(\"never\");\n if (++i < 3) jump again;\n }\n"), options()); - ensure("nor one that only jumps to itself: " + r.text, r.text.find("never") == std::string::npos); + UNSCOPED_INFO("nor one that only jumps to itself: " + r.text); + REQUIRE(r.text.find("never") == std::string::npos); } - template<> template<> - void allsloptimizer_object::test<23>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: an inlined block evaluates its arguments left to right as a call does", "[allsloptimizer]") { - set_test_name("a function put in place as a block takes its arguments in the order a call does: left to right, as LSO and Mono both push them"); const std::string source = "integer n;\n" "integer next()\n" @@ -1013,31 +1216,39 @@ namespace tut const ALLSLInliner::Result put = ALLSLInliner::run(source); const size_t a = put.text.find("integer a = next();"); const size_t b = put.text.find("integer b = next() * 10;"); - ensure("both arguments set to locals: " + put.text, a != std::string::npos && b != std::string::npos); - ensure("the first first: " + put.text, a < b); + { + INFO("both arguments set to locals: " + put.text); + REQUIRE(a != std::string::npos); + REQUIRE(b != std::string::npos); + } + UNSCOPED_INFO("the first first: " + put.text); + CHECK(a < b); } - template<> template<> - void allsloptimizer_object::test<24>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a true branch ending in an if gets braces so the else stays the outer if's", "[allsloptimizer]") { - set_test_name("a true branch that ends in an if of its own is printed in braces where an else follows, so that the else stays the outer if's"); // if (!c) A; else if (d) B; swapped round: the inner if has no else. ALLSLOptimizer::Result r = ALLSLOptimizer::run(wrap("integer c;\ninteger d = 1;\n", " if (!c) llOwnerSay(\"A\"); else if (d) llOwnerSay(\"B\");\n c = 1; d = 0;\n"), options()); - ensure("swapped: " + notes(r), has(r, "swapped the branches")); - ensure("the inner if in braces: " + r.text, r.text.find(" if (c)\n {\n if (d)\n llOwnerSay(\"B\");\n }\n") != std::string::npos); - ensure("the else the outer if's: " + r.text, r.text.find(" }\n else\n llOwnerSay(\"A\");\n") != std::string::npos); + UNSCOPED_INFO("swapped: " + notes(r)); + CHECK(has(r, "swapped the branches")); + UNSCOPED_INFO("the inner if in braces: " + r.text); + REQUIRE(r.text.find(" if (c)\n {\n if (d)\n llOwnerSay(\"B\");\n }\n") != std::string::npos); + UNSCOPED_INFO("the else the outer if's: " + r.text); + REQUIRE(r.text.find(" }\n else\n llOwnerSay(\"A\");\n") != std::string::npos); // And one whose inner if stands in a loop. r = ALLSLOptimizer::run(wrap("integer c;\ninteger d = 1;\n", " if (!c) llOwnerSay(\"A\"); else while (d--) if (d == 3) llOwnerSay(\"B\");\n c = 1;\n"), options()); - ensure("the loop in braces: " + r.text, r.text.find(" if (c)\n {\n while (d--)\n") != std::string::npos && - r.text.find(" }\n else\n llOwnerSay(\"A\");\n") != std::string::npos); + { + INFO("the loop in braces: " + r.text); + REQUIRE(r.text.find(" if (c)\n {\n while (d--)\n") != std::string::npos); + REQUIRE(r.text.find(" }\n else\n llOwnerSay(\"A\");\n") != std::string::npos); + } // Where the inner if has an else of its own, nothing is added. r = ALLSLOptimizer::run(wrap("integer c;\ninteger d = 1;\n", " if (!c) llOwnerSay(\"A\"); else if (d) llOwnerSay(\"B\"); else llOwnerSay(\"C\");\n c = 1; d = 0;\n"), options()); - ensure("no braces an else chain does not need: " + r.text, r.text.find('{', r.text.find("if (c)")) > r.text.find("llOwnerSay(\"A\")")); + UNSCOPED_INFO("no braces an else chain does not need: " + r.text); + CHECK(r.text.find('{', r.text.find("if (c)")) > r.text.find("llOwnerSay(\"A\")")); } - template<> template<> - void allsloptimizer_object::test<25>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: an inlined call as a brace-less loop body or branch keeps its return inside", "[allsloptimizer]") { - set_test_name("a call that is the whole of a loop's body or an if's branch, written without braces, keeps its return inside the loop or the branch"); const std::string fn = "skip(integer n)\n" "{\n" @@ -1048,24 +1259,36 @@ namespace tut ALLSLInliner::Result put = ALLSLInliner::run(fn + wrap("", " integer i;\n for (i = 0; i < 5; ++i) skip(i);\n llOwnerSay(\"done\");\n")); const size_t label = put.text.find("@_ret_1;"); const size_t close = put.text.find('}', label); - ensure("went in: " + put.text, put.inlined == 1 && label != std::string::npos && put.text.find("jump _ret_1;") != std::string::npos); - ensure("the label ends the loop's body, before anything after the loop: " + put.text, - close != std::string::npos && close < put.text.find("llOwnerSay(\"done\");")); + { + INFO("went in: " + put.text); + CHECK(put.inlined == 1); + REQUIRE(label != std::string::npos); + REQUIRE(put.text.find("jump _ret_1;") != std::string::npos); + } + { + INFO("the label ends the loop's body, before anything after the loop: " + put.text); + REQUIRE(close != std::string::npos); + CHECK(close < put.text.find("llOwnerSay(\"done\");")); + } ALLSLOptimizer::Options o = options(); o.inlining = true; ALLSLOptimizer::Result r = ALLSLOptimizer::run(fn + wrap("", " integer i;\n for (i = 0; i < 5; ++i) skip(i);\n llOwnerSay(\"done\");\n"), o); - ensure("the optimizer reads it: " + notes(r) + r.text, r.optimized); + UNSCOPED_INFO("the optimizer reads it: " + notes(r) + r.text); + CHECK(r.optimized); // Between an if and its else, a label after the block was a // syntax error the user never wrote. put = ALLSLInliner::run(fn + wrap("integer g;\n", " if (g) skip(g); else llOwnerSay(\"none\");\n")); - ensure("went in: " + put.text, put.inlined == 1); + UNSCOPED_INFO("went in: " + put.text); + CHECK(put.inlined == 1); r = ALLSLOptimizer::run(fn + wrap("integer g;\n", " if (g) skip(g); else llOwnerSay(\"none\");\n g = llGetUnixTime();\n"), o); - ensure("the else still follows its if: " + notes(r) + r.text, r.optimized && r.text.find("else") != std::string::npos); + { + INFO("the else still follows its if: " + notes(r) + r.text); + CHECK(r.optimized); + REQUIRE(r.text.find("else") != std::string::npos); + } } - template<> template<> - void allsloptimizer_object::test<26>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: the engine counts a clash from another thread but not a nested hold", "[allsloptimizer]") { - set_test_name("the engine is one thread's at a time: another coming in while one is in is said, and a hold within a hold is none"); const unsigned before = al_script_engine::clashes().load(); { AL_SCRIPT_ENGINE_HELD; @@ -1073,30 +1296,32 @@ namespace tut // A hold within a hold is the same thread's. AL_SCRIPT_ENGINE_HELD; } - ensure_equals("none from within", al_script_engine::clashes().load(), before); + UNSCOPED_INFO("none from within"); + CHECK(al_script_engine::clashes().load() == before); std::thread other([]() { AL_SCRIPT_ENGINE_HELD; }); other.join(); - ensure_equals("another thread, while this one is in: said", al_script_engine::clashes().load(), before + 1); + UNSCOPED_INFO("another thread, while this one is in: said"); + CHECK(al_script_engine::clashes().load() == before + 1); } std::thread after([]() { AL_SCRIPT_ENGINE_HELD; }); after.join(); - ensure_equals("another, once it is out: nothing", al_script_engine::clashes().load(), before + 1); + UNSCOPED_INFO("another, once it is out: nothing"); + CHECK(al_script_engine::clashes().load() == before + 1); } - template<> template<> - void allsloptimizer_object::test<27>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: list literal sums bracket elements and stay literals where order shows", "[allsloptimizer]") { - set_test_name("a list literal as a sum brackets an element where the sum would bind it otherwise, and stays a literal where the order would show"); // On LSO, where a list's shapes are made wherever they are smaller: - // Mono weighs them against the helpers they need (test 41). + // Mono weighs them against the helpers they need (see the case on Mono's list shapes). ALLSLOptimizer::Options lso = options(); lso.target = ALLSLOptimizer::Target::LSO; - const auto compiles = [](const ALLSLOptimizer::Result& r) { + const auto require_compiles = [](const ALLSLOptimizer::Result& r) { ALLSLService service; const ALScriptProblems said = service.check(r.text); for (const ALScriptProblem& p : said) { - ensure("no error in what was written: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("no error in what was written: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } }; // A comparison among the elements, as a preprocessor writes one: @@ -1109,22 +1334,27 @@ namespace tut " , <((float)0), ((float)0), ((float)0), ((float)1)>\n" " ];\n"), lso); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); // Each element added to the list bare (ALLSLCosts::elementForList). - ensure("the comparison bracketed: " + r.text, - r.text.find("loc_params + 41 + (-1 < --loc_sitTargetsRemaining) + <0.0, 0.0, 0.0> + <0.0, 0.0, 0.0, 1.0>") != std::string::npos); - compiles(r); + UNSCOPED_INFO("the comparison bracketed: " + r.text); + REQUIRE(r.text.find("loc_params + 41 + (-1 < --loc_sitTargetsRemaining) + <0.0, 0.0, 0.0> + <0.0, 0.0, 0.0, 1.0>") != std::string::npos); + require_compiles(r); // The first element under the cast, bracketed where the cast would // take less of it; the rest where the sum would. r = ALLSLOptimizer::run(wrap("", " integer a = (integer)llFrand(9);\n integer b = (integer)llFrand(9);\n list l;\n" " l = [a - b, 1];\n l = [--a, 2];\n l = [-1, 3];\n l = [a * b, a - b];\n llSay(0, llList2CSV(l));\n"), lso); - ensure("a difference: " + r.text, r.text.find("l = (list)(a - b) + 1;") != std::string::npos); - ensure("a step down: " + r.text, r.text.find("l = (list)(--a) + 2;") != std::string::npos); - ensure("a negative constant bare: " + r.text, r.text.find("l = (list)-1 + 3;") != std::string::npos); - ensure("a product under the cast, a difference after it: " + r.text, r.text.find("l = (list)(a * b) + (a - b);") != std::string::npos); - compiles(r); + UNSCOPED_INFO("a difference: " + r.text); + REQUIRE(r.text.find("l = (list)(a - b) + 1;") != std::string::npos); + UNSCOPED_INFO("a step down: " + r.text); + REQUIRE(r.text.find("l = (list)(--a) + 2;") != std::string::npos); + UNSCOPED_INFO("a negative constant bare: " + r.text); + REQUIRE(r.text.find("l = (list)-1 + 3;") != std::string::npos); + UNSCOPED_INFO("a product under the cast, a difference after it: " + r.text); + REQUIRE(r.text.find("l = (list)(a * b) + (a - b);") != std::string::npos); + require_compiles(r); // A sum takes its right side first, so the elements would be taken // last to first: where more than one of them could see another's @@ -1134,52 +1364,62 @@ namespace tut " l = [i, --i];\n l = [bump(), bump()];\n l = [llGetUnixTime(), bump()];\n l = [i, g];\n" " llSay(0, llList2CSV(l));\n"), lso); - ensure("a name and its change: " + r.text, r.text.find("l = [i, --i];") != std::string::npos); - ensure("two calls that change: " + r.text, r.text.find("l = [bump(), bump()];") != std::string::npos); - ensure("a read and a call that changes: " + r.text, r.text.find("l = [llGetUnixTime(), bump()];") != std::string::npos); - ensure("reads alone, a sum: " + r.text, r.text.find("l = (list)i + g;") != std::string::npos); - compiles(r); + UNSCOPED_INFO("a name and its change: " + r.text); + REQUIRE(r.text.find("l = [i, --i];") != std::string::npos); + UNSCOPED_INFO("two calls that change: " + r.text); + REQUIRE(r.text.find("l = [bump(), bump()];") != std::string::npos); + UNSCOPED_INFO("a read and a call that changes: " + r.text); + REQUIRE(r.text.find("l = [llGetUnixTime(), bump()];") != std::string::npos); + UNSCOPED_INFO("reads alone, a sum: " + r.text); + REQUIRE(r.text.find("l = (list)i + g;") != std::string::npos); + require_compiles(r); } - template<> template<> - void allsloptimizer_object::test<28>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: removed unreachable code is quoted by its first line cut to a few words", "[allsloptimizer]") { - set_test_name("what can never run, removed, is said as far as its first line and a few words of it"); std::string body = " return;\n llOwnerSay(\""; body += std::string(300, 'x'); body += "\");\n"; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(wrap("", body), options()); const auto note = std::find_if(r.problems.begin(), r.problems.end(), [](const ALScriptProblem& p) { return p.key == "OptimizerRemovedUnreachable"; }); - ensure("said", note != r.problems.end() && note->args.size() == 1); - ensure("not all of it", note->args[0].size() < 80 && note->args[0].find("\xE2\x80\xA6") != std::string::npos); - ensure("from its start", note->args[0].rfind("llOwnerSay", 0) == 0); + REQUIRE(note != r.problems.end()); + REQUIRE(note->args.size() == 1); + { + INFO("not all of it"); + REQUIRE(note->args[0].size() < 80); + REQUIRE(note->args[0].find("\xE2\x80\xA6") != std::string::npos); + } + UNSCOPED_INFO("from its start"); + CHECK(note->args[0].rfind("llOwnerSay", 0) == 0); } - template<> template<> - void allsloptimizer_object::test<29>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: the inliner and optimizer share one visit budget and rounds stop within it", "[allsloptimizer]") { - set_test_name("one budget for the inliner and the optimizer: what the inliner visits is spent, and a round is not begun the budget cannot see through"); const std::string source = wrap("integer twice(integer n) { return n * 2; }\ninteger thrice(integer n) { return twice(n) + n; }\n", " llSay(0, (string)thrice(2));\n llSay(0, (string)(1 + 2));\n"); ALLSLOptimizer::Options o = options(); o.inlining = true; const ALLSLOptimizer::Result whole = ALLSLOptimizer::run(source, o); - ensure("with room: not stopped", !whole.stoppedEarly); + UNSCOPED_INFO("with room: not stopped"); + CHECK_FALSE(whole.stoppedEarly); // Room for the inliner's first round and not much more: it stops, // and leaves the optimizer nothing. const ALLSLInliner::Result once = ALLSLInliner::run(source, ALLSLInliner::Asked(), size_t(-1)); - ensure("the inliner visits", once.visited > 0); + CHECK(once.visited > 0); o.visitBudget = once.visited / 4; const ALLSLOptimizer::Result short_of = ALLSLOptimizer::run(source, o); - ensure("the optimizer stopped for what the inliner spent", short_of.stoppedEarly); - ensure("said", std::any_of(short_of.problems.begin(), short_of.problems.end(), [](const ALScriptProblem& p) { return p.key == "OptimizerStoppedEarly"; })); + UNSCOPED_INFO("the optimizer stopped for what the inliner spent"); + CHECK(short_of.stoppedEarly); + CHECK(std::any_of(short_of.problems.begin(), short_of.problems.end(), [](const ALScriptProblem& p) { return p.key == "OptimizerStoppedEarly"; })); const ALLSLInliner::Result cut = ALLSLInliner::run(source, ALLSLInliner::Asked(), 1); - ensure("the inliner's one round done, the next not begun", cut.inlined > 0 && (cut.stoppedEarly || cut.inlined == once.inlined)); + { + INFO("the inliner's one round done, the next not begun"); + CHECK(cut.inlined > 0); + CHECK((cut.stoppedEarly || cut.inlined == once.inlined)); + } } - template<> template<> - void allsloptimizer_object::test<30>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: an always-true if stays when it guards a state change in a function", "[allsloptimizer]") { - set_test_name("an if whose condition is always true stays where it is the last around a state change in a function, which may change state only under one"); const std::string source = "f()\n{\n if (TRUE) state other;\n}\n" "g()\n{\n if (TRUE)\n {\n if (llGetUnixTime()) state other;\n }\n}\n" "h()\n{\n if (FALSE) llOwnerSay(\"x\");\n else state other;\n}\n" @@ -1191,41 +1431,51 @@ namespace tut { refused = refused || p.severity == ALScriptProblem::Severity::Error; } - ensure("a bare state change in a function does not compile", refused); + UNSCOPED_INFO("a bare state change in a function does not compile"); + CHECK(refused); const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); for (const ALScriptProblem& p : service.check(r.text)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } const auto body = [&r](const char* name) { const size_t at = r.text.find(std::string(name) + "()\n{"); return at == std::string::npos ? std::string() : r.text.substr(at, r.text.find("\n}", at) - at); }; - ensure("f keeps its if: " + r.text, body("f").find("if (") != std::string::npos && body("f").find("state other;") != std::string::npos); - ensure("g loses the outer if, the inner one standing: " + r.text, body("g").find("llGetUnixTime()") != std::string::npos); - ensure("h keeps its if too: " + r.text, body("h").find("if (") != std::string::npos); + { + INFO("f keeps its if: " + r.text); + REQUIRE(body("f").find("if (") != std::string::npos); + REQUIRE(body("f").find("state other;") != std::string::npos); + } + UNSCOPED_INFO("g loses the outer if, the inner one standing: " + r.text); + REQUIRE(body("g").find("llGetUnixTime()") != std::string::npos); + UNSCOPED_INFO("h keeps its if too: " + r.text); + REQUIRE(body("h").find("if (") != std::string::npos); const size_t entry = r.text.find("state_entry()"); - ensure("an event changes state as it likes: its if goes: " + r.text, - entry != std::string::npos && r.text.find("if (", entry) > r.text.find("state other;", entry)); + { + INFO("an event changes state as it likes: its if goes: " + r.text); + REQUIRE(entry != std::string::npos); + CHECK(r.text.find("if (", entry) > r.text.find("state other;", entry)); + } } - template<> template<> - void allsloptimizer_object::test<31>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: llList2Key out of range folds to NULL_KEY on LSO and stays a call on Mono", "[allsloptimizer]") { - set_test_name("a key out of a list's range is NULL_KEY under LSO, written by its name; under Mono an empty key, which a key cannot be written as"); const std::string source = "default\n{\n state_entry()\n {\n llOwnerSay((string)llList2Key([1, 2], 5));\n }\n}\n"; ALLSLOptimizer::Options o = options(); o.target = ALLSLOptimizer::Target::LSO; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("LSO: NULL_KEY: " + r.text, r.text.find("llOwnerSay((string)NULL_KEY);") != std::string::npos); + UNSCOPED_INFO("LSO: NULL_KEY: " + r.text); + REQUIRE(r.text.find("llOwnerSay((string)NULL_KEY);") != std::string::npos); o.target = ALLSLOptimizer::Target::Mono; r = ALLSLOptimizer::run(source, o); - ensure("Mono: still the call: " + r.text, r.text.find("llList2Key(") != std::string::npos); + UNSCOPED_INFO("Mono: still the call: " + r.text); + REQUIRE(r.text.find("llList2Key(") != std::string::npos); } - template<> template<> - void allsloptimizer_object::test<32>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: unused world reads go but never move and ALLSLTraits describes each call", "[allsloptimizer]") { - set_test_name("a read of the world or the clock whose answer nobody takes goes, but is never read at another time; a function whose answer is its arguments' alone is pure"); const std::string source = "default\n{\n state_entry()\n {\n" " llGetPos();\n" " llSetPos(<1, 2, 3>);\n" @@ -1236,44 +1486,88 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = ALLSLOptimizer::Target::Mono; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); - ensure("the unused read goes: " + r.text, r.text.find(" llGetPos();") == std::string::npos); - ensure("the change stays: " + r.text, r.text.find("llSetPos(") != std::string::npos); - ensure("so does the unused local read from the world: " + r.text, r.text.find("vector p") == std::string::npos); - ensure("two reads of the clock are not put in another order: " + r.text, r.text.find("[llGetTime(), llGetTime()]") != std::string::npos); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); + UNSCOPED_INFO("the unused read goes: " + r.text); + REQUIRE(r.text.find(" llGetPos();") == std::string::npos); + UNSCOPED_INFO("the change stays: " + r.text); + REQUIRE(r.text.find("llSetPos(") != std::string::npos); + UNSCOPED_INFO("so does the unused local read from the world: " + r.text); + REQUIRE(r.text.find("vector p") == std::string::npos); + UNSCOPED_INFO("two reads of the clock are not put in another order: " + r.text); + REQUIRE(r.text.find("[llGetTime(), llGetTime()]") != std::string::npos); for (const char* pure : { "llRound", "llListFindList", "llListSort", "llDumpList2String", "llMD5String", "llAcos", "llAsin" }) { - ensure(std::string(pure) + " is pure", ALLSLTraits::pure(pure)); + CAPTURE(pure); + REQUIRE(ALLSLTraits::pure(pure)); + } + { + INFO("llGetPos is not"); + CHECK_FALSE(ALLSLTraits::pure("llGetPos")); + CHECK(ALLSLTraits::of("llGetPos")->mustUse); } - ensure("llGetPos is not", !ALLSLTraits::pure("llGetPos") && ALLSLTraits::of("llGetPos")->mustUse); // What SLua makes of each, from the same definitions. - ensure("llGetSubString takes its second and third from one in SLua's ll", - ALLSLTraits::of("llGetSubString")->slua == ALLSLTraits::SluaIndexArgs && ALLSLTraits::of("llGetSubString")->sluaIndexArgs == 0x6); - ensure("llListFindList answers an index, and SLua deprecates it", - ALLSLTraits::of("llListFindList")->slua == (ALLSLTraits::SluaIndexResult | ALLSLTraits::SluaDeprecated)); - ensure("llSameGroup answers a boolean", ALLSLTraits::of("llSameGroup")->slua == ALLSLTraits::SluaBool); - ensure("llSetTimerEvent is llcompat's alone", ALLSLTraits::of("llSetTimerEvent")->slua == ALLSLTraits::SluaRemoved); - ensure("llSay is as it was", ALLSLTraits::of("llSay")->slua == 0 && !ALLSLTraits::of("llSay")->sluaUse); - ensure("llAbs has math.abs in its stead", ALLSLTraits::of("llAbs")->sluaUse && std::string(ALLSLTraits::of("llAbs")->sluaUse) == "math.abs"); + { + INFO("llGetSubString takes its second and third from one in SLua's ll"); + CHECK(ALLSLTraits::of("llGetSubString")->slua == ALLSLTraits::SluaIndexArgs); + CHECK(ALLSLTraits::of("llGetSubString")->sluaIndexArgs == 0x6); + } + UNSCOPED_INFO("llListFindList answers an index, and SLua deprecates it"); + CHECK(ALLSLTraits::of("llListFindList")->slua == (ALLSLTraits::SluaIndexResult | ALLSLTraits::SluaDeprecated)); + UNSCOPED_INFO("llSameGroup answers a boolean"); + CHECK(ALLSLTraits::of("llSameGroup")->slua == ALLSLTraits::SluaBool); + UNSCOPED_INFO("llSetTimerEvent is llcompat's alone"); + CHECK(ALLSLTraits::of("llSetTimerEvent")->slua == ALLSLTraits::SluaRemoved); + { + INFO("llSay is as it was"); + CHECK(ALLSLTraits::of("llSay")->slua == 0); + REQUIRE_FALSE(ALLSLTraits::of("llSay")->sluaUse); + } + { + INFO("llAbs has math.abs in its stead"); + REQUIRE(ALLSLTraits::of("llAbs")->sluaUse); + CHECK(std::string(ALLSLTraits::of("llAbs")->sluaUse) == "math.abs"); + } // How long each sleeps, under LSO and under Mono. - ensure("llSetPos sleeps 0.2 s", ALLSLTraits::of("llSetPos")->sleep == 0.2f && ALLSLTraits::of("llSetPos")->monoSleep == 0.2f); - ensure("llSetLinkPrimitiveParamsFast does not", ALLSLTraits::of("llSetLinkPrimitiveParamsFast")->sleep == 0.0f); - ensure("llTextBox sleeps under LSO alone", ALLSLTraits::of("llTextBox")->sleep == 1.0f && ALLSLTraits::of("llTextBox")->monoSleep == 0.0f); - ensure("llOpenFloater under Mono alone", ALLSLTraits::of("llOpenFloater")->sleep == 0.0f && ALLSLTraits::of("llOpenFloater")->monoSleep == 0.2f); + { + INFO("llSetPos sleeps 0.2 s"); + CHECK(ALLSLTraits::of("llSetPos")->sleep == 0.2f); + CHECK(ALLSLTraits::of("llSetPos")->monoSleep == 0.2f); + } + UNSCOPED_INFO("llSetLinkPrimitiveParamsFast does not"); + CHECK(ALLSLTraits::of("llSetLinkPrimitiveParamsFast")->sleep == 0.0f); + { + INFO("llTextBox sleeps under LSO alone"); + CHECK(ALLSLTraits::of("llTextBox")->sleep == 1.0f); + CHECK(ALLSLTraits::of("llTextBox")->monoSleep == 0.0f); + } + { + INFO("llOpenFloater under Mono alone"); + CHECK(ALLSLTraits::of("llOpenFloater")->sleep == 0.0f); + CHECK(ALLSLTraits::of("llOpenFloater")->monoSleep == 0.2f); + } // What item of the object's an argument names, by its LSL name or SLua's. using Item = ALLSLTraits::Item; - ensure("llPlaySound's first, a sound", ALLSLTraits::itemArg("llPlaySound", 0) == Item::Sound); - ensure("its second names nothing", ALLSLTraits::itemArg("llPlaySound", 1) == Item::None); - ensure("llGiveInventory's second, whatever it is", ALLSLTraits::itemArg("llGiveInventory", 1) == Item::Any); - ensure("llMakeFountain's seventh, a texture", ALLSLTraits::itemArg("llMakeFountain", 6) == Item::Texture); - ensure("SLua's ll.GetNotecardLine, a notecard", ALLSLTraits::itemArg("ll.GetNotecardLine", 0) == Item::Notecard); - ensure("llcompat's the same", ALLSLTraits::itemArg("llcompat.RezObject", 0) == Item::Object); - ensure("llSay names none", ALLSLTraits::itemArg("llSay", 1) == Item::None && ALLSLTraits::itemArg("ll.Say", 1) == Item::None); - } - template<> template<> - void allsloptimizer_object::test<33>() - { - set_test_name("a function returning a value goes in whatever its body: into what a declaration or an assignment sets, back out of a function returning the same, into a fresh local before the statement -- where nothing run before it would see a difference"); + UNSCOPED_INFO("llPlaySound's first, a sound"); + CHECK(ALLSLTraits::itemArg("llPlaySound", 0) == Item::Sound); + UNSCOPED_INFO("its second names nothing"); + CHECK(ALLSLTraits::itemArg("llPlaySound", 1) == Item::None); + UNSCOPED_INFO("llGiveInventory's second, whatever it is"); + CHECK(ALLSLTraits::itemArg("llGiveInventory", 1) == Item::Any); + UNSCOPED_INFO("llMakeFountain's seventh, a texture"); + CHECK(ALLSLTraits::itemArg("llMakeFountain", 6) == Item::Texture); + UNSCOPED_INFO("SLua's ll.GetNotecardLine, a notecard"); + CHECK(ALLSLTraits::itemArg("ll.GetNotecardLine", 0) == Item::Notecard); + UNSCOPED_INFO("llcompat's the same"); + CHECK(ALLSLTraits::itemArg("llcompat.RezObject", 0) == Item::Object); + { + INFO("llSay names none"); + CHECK(ALLSLTraits::itemArg("llSay", 1) == Item::None); + CHECK(ALLSLTraits::itemArg("ll.Say", 1) == Item::None); + } + } + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a value-returning function goes in whatever its body where order allows", "[allsloptimizer]") + { const std::string source = "integer g;\n" "integer f(integer k)\n{\n if (k) return k + g;\n g = 7;\n return 0;\n}\n" @@ -1289,41 +1583,61 @@ namespace tut " }\n}\n"; const ALLSLInliner::Result put = ALLSLInliner::run(source, { "f" }); auto has = [&put](const std::string& text) { return put.text.find(text) != std::string::npos; }; - ensure("the declaration's variable set by the body, its returns but the last jumps: " + put.text, - has("integer x;\n{\ninteger k = 1;") && has("{ x = k + g; jump _ret_") && has(" x = 0;")); - ensure("the assignment's too: " + put.text, has("integer k = 2;")); - ensure("a value nobody takes: the returns that change nothing go: " + put.text, has("integer k = 3;\n\n if (k) jump _ret_3;\n g = 7;")); + { + INFO("the declaration's variable set by the body, its returns but the last jumps: " + put.text); + CHECK(has("integer x;\n{\ninteger k = 1;")); + CHECK(has("{ x = k + g; jump _ret_")); + CHECK(has(" x = 0;")); + } + UNSCOPED_INFO("the assignment's too: " + put.text); + CHECK(has("integer k = 2;")); + UNSCOPED_INFO("a value nobody takes: the returns that change nothing go: " + put.text); + CHECK(has("integer k = 3;\n\n if (k) jump _ret_3;\n g = 7;")); // A label is one to an event: every block's end its own, however // many rounds made them. for (const char* label : { "@_ret_1;", "@_ret_2;", "@_ret_3;", "@_ret_4;", "@_ret_5;", "@_ret_6;" }) { const size_t first = put.text.find(label); - ensure(std::string("one ") + label + ": " + put.text, first == std::string::npos || put.text.find(label, first + 1) == std::string::npos); + CAPTURE(label); + UNSCOPED_INFO("at most once: " + put.text); + REQUIRE((first == std::string::npos || put.text.find(label, first + 1) == std::string::npos)); + } + UNSCOPED_INFO("g runs before f(4) and f writes g: the call stays: " + put.text); + CHECK(has("x = f(4) + g;")); + { + INFO("f(5) is the right operand and runs first: set before the statement: " + put.text); + CHECK(has("integer _r_")); + CHECK(has("x = g + _r_")); + } + UNSCOPED_INFO("a branch standing alone gets braces for the block: " + put.text); + CHECK((has("if (x) {") || has("if (x) {\n"))); + { + INFO("outer returns f's value as its own, then goes where it is called: " + put.text); + CHECK_FALSE(has("integer outer(integer k)")); + CHECK(has("integer k_1 = k;")); } - ensure("g runs before f(4) and f writes g: the call stays: " + put.text, has("x = f(4) + g;")); - ensure("f(5) is the right operand and runs first: set before the statement: " + put.text, has("integer _r_") && has("x = g + _r_")); - ensure("a branch standing alone gets braces for the block: " + put.text, has("if (x) {") || has("if (x) {\n")); - ensure("outer returns f's value as its own, then goes where it is called: " + put.text, !has("integer outer(integer k)") && has("integer k_1 = k;")); - ensure("f stays, one call of it standing: " + put.text, has("integer f(integer k)")); + UNSCOPED_INFO("f stays, one call of it standing: " + put.text); + CHECK(has("integer f(integer k)")); ALLSLService service; for (const ALScriptProblem& p : service.check(put.text)) { - ensure("what was written compiles: " + p.message + "\n" + put.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + put.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } ALLSLOptimizer::Options o = options(); o.inlining = true; o.inlineNames = { "f" }; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("the optimizer reads it: " + notes(r), r.optimized); + UNSCOPED_INFO("the optimizer reads it: " + notes(r)); + CHECK(r.optimized); for (const ALScriptProblem& p : service.check(r.text)) { - ensure("and what it wrote compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("and what it wrote compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } - template<> template<> - void allsloptimizer_object::test<34>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: calls in loop conditions and steps go in with loops rewritten as jumps", "[allsloptimizer]") { - set_test_name("a call in a loop's condition or a for's step goes too: a while or a for written as its label, if and jump back, a do's value set at the end of its body"); const std::string source = "integer g;\n" "integer next()\n{\n g = g + 1;\n return g;\n}\n" @@ -1337,16 +1651,36 @@ namespace tut " }\n}\n"; const ALLSLInliner::Result put = ALLSLInliner::run(source, { "next", "tick" }); auto has = [&put](const std::string& text) { return put.text.find(text) != std::string::npos; }; - ensure("every call went, and both functions: " + put.text, !has("next()") && !has("tick(") && !has("integer next()")); - ensure("the while and the for are labels and ifs: " + put.text, !has("while (next") && !has("for (") && has("@_loop_1;") && has("jump _loop_1;") && - has("@_loop_2;") && has("jump _loop_2;")); - ensure("the for's first part runs once, before its label: " + put.text, put.text.find("i = 0;") < put.text.find("@_loop_2;")); - ensure("its step's call goes as a statement's, the argument set in the call's order: " + put.text, has("integer i_1 = i++;")); - ensure("the do stays a do, braces round the if's branch for its value's local: " + put.text, has("if (g) {") && has("do {") && has("} while (_r_")); + { + INFO("every call went, and both functions: " + put.text); + CHECK_FALSE(has("next()")); + CHECK_FALSE(has("tick(")); + CHECK_FALSE(has("integer next()")); + } + { + INFO("the while and the for are labels and ifs: " + put.text); + CHECK_FALSE(has("while (next")); + CHECK_FALSE(has("for (")); + CHECK(has("@_loop_1;")); + CHECK(has("jump _loop_1;")); + CHECK(has("@_loop_2;")); + CHECK(has("jump _loop_2;")); + } + UNSCOPED_INFO("the for's first part runs once, before its label: " + put.text); + CHECK(put.text.find("i = 0;") < put.text.find("@_loop_2;")); + UNSCOPED_INFO("its step's call goes as a statement's, the argument set in the call's order: " + put.text); + CHECK(has("integer i_1 = i++;")); + { + INFO("the do stays a do, braces round the if's branch for its value's local: " + put.text); + CHECK(has("if (g) {")); + CHECK(has("do {")); + CHECK(has("} while (_r_")); + } ALLSLService service; for (const ALScriptProblem& p : service.check(put.text)) { - ensure("what was written compiles: " + p.message + "\n" + put.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + put.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } ALLSLOptimizer::Options o = options(); o.inlining = true; @@ -1355,17 +1689,17 @@ namespace tut { o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); for (const ALScriptProblem& p : service.check(r.text)) { - ensure("and what the optimizer wrote compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("and what the optimizer wrote compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } } - template<> template<> - void allsloptimizer_object::test<35>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a pure local read once moves to its read and a jump to the next line goes", "[allsloptimizer]") { - set_test_name("a local set from what changes nothing and read once goes where it is read, where nothing between writes what it reads; a jump that goes where running on goes goes"); const std::string source = wrap("integer g;\n", " integer a = llGetUnixTime();\n" " integer b = g * 2;\n" @@ -1390,27 +1724,42 @@ namespace tut " if (a) { @twice; g = 3; }\n" " llOwnerSay((string)(a + g));\n"); const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); auto has = [&r](const std::string& text) { return r.text.find(text) != std::string::npos; }; - ensure("b goes where it is read: " + r.text, !has("integer b") && has("llOwnerSay((string)(g * 2));")); - ensure("c's g is written between: it stays: " + r.text, has("integer c = -~g;")); - ensure("d is read twice: " + r.text, has("integer d = g * 3;")); - ensure("e is read round a loop: " + r.text, has("integer e = g * 4;")); - ensure("f is read in a branch, a statement of its own: " + r.text, has("integer f = g * 5;")); - ensure("h is read after a label: " + r.text, has("integer h = g * 6;")); - ensure("the read of the clock stays: " + r.text, has("integer a = llGetUnixTime();")); - ensure("a jump to the label next goes, and the label with it: " + r.text, !has("jump out;") && !has("@out;")); - ensure("a jump to a label whose name is twice in the event stays: " + r.text, has("jump twice;")); + { + INFO("b goes where it is read: " + r.text); + CHECK_FALSE(has("integer b")); + CHECK(has("llOwnerSay((string)(g * 2));")); + } + UNSCOPED_INFO("c's g is written between: it stays: " + r.text); + CHECK(has("integer c = -~g;")); + UNSCOPED_INFO("d is read twice: " + r.text); + CHECK(has("integer d = g * 3;")); + UNSCOPED_INFO("e is read round a loop: " + r.text); + CHECK(has("integer e = g * 4;")); + UNSCOPED_INFO("f is read in a branch, a statement of its own: " + r.text); + CHECK(has("integer f = g * 5;")); + UNSCOPED_INFO("h is read after a label: " + r.text); + CHECK(has("integer h = g * 6;")); + UNSCOPED_INFO("the read of the clock stays: " + r.text); + CHECK(has("integer a = llGetUnixTime();")); + { + INFO("a jump to the label next goes, and the label with it: " + r.text); + CHECK_FALSE(has("jump out;")); + CHECK_FALSE(has("@out;")); + } + UNSCOPED_INFO("a jump to a label whose name is twice in the event stays: " + r.text); + CHECK(has("jump twice;")); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } - template<> template<> - void allsloptimizer_object::test<36>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: nested blocks flatten and declarations move down to their first assignment", "[allsloptimizer]") { - set_test_name("a block in a block is flattened where its names are its own, and a declaration moves down to its first assignment where nothing between jumps or names it"); const std::string source = wrap("integer g;\n", " { llOwnerSay(\"a\"); }\n" " { integer u = llGetUnixTime(); llOwnerSay((string)(u + u)); }\n" @@ -1426,23 +1775,37 @@ namespace tut " @over;\n" " llOwnerSay((string)(y + y));\n"); const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); auto has = [&r](const std::string& text) { return r.text.find(text) != std::string::npos; }; - ensure("a block of one statement is that statement: " + r.text, has(" llOwnerSay(\"a\");") && !has("{\n llOwnerSay(\"a\");")); - ensure("one whose local is named nowhere else too: " + r.text, has(" integer u = llGetUnixTime();")); - ensure("two whose locals share a name stay blocks: " + r.text, has(" integer v = llGetUnixTime();")); - ensure("x declared where it is first set: " + r.text, !has("integer x;") && has("integer x = llGetUnixTime();")); - ensure("y not past the jump: " + r.text, has("integer y;") && has("y = llGetUnixTime();")); + { + INFO("a block of one statement is that statement: " + r.text); + CHECK(has(" llOwnerSay(\"a\");")); + CHECK_FALSE(has("{\n llOwnerSay(\"a\");")); + } + UNSCOPED_INFO("one whose local is named nowhere else too: " + r.text); + CHECK(has(" integer u = llGetUnixTime();")); + UNSCOPED_INFO("two whose locals share a name stay blocks: " + r.text); + CHECK(has(" integer v = llGetUnixTime();")); + { + INFO("x declared where it is first set: " + r.text); + CHECK_FALSE(has("integer x;")); + CHECK(has("integer x = llGetUnixTime();")); + } + { + INFO("y not past the jump: " + r.text); + CHECK(has("integer y;")); + CHECK(has("y = llGetUnixTime();")); + } ALLSLService service; for (const ALScriptProblem& p : service.check(r.text)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } - template<> template<> - void allsloptimizer_object::test<37>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: infinity reads back on every target and uncompilable output is refused", "[allsloptimizer]") { - set_test_name("infinity is written as a literal that reads back as it, on each target (O0g); what was written is checked, and refused where it does not compile"); const std::string source = "float foo = -2.0e+9999;\nfloat bar = 2.0e+9999;\n" "default\n{\n state_entry()\n {\n llSetPos(<-2.0e+9999, 2.0e+9999, 0>);\n" " llSetRot(<-2.0e+9999, 2.0e+9999, 0, 0>);\n llOwnerSay((string)(foo + bar));\n }\n}\n"; @@ -1451,28 +1814,35 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); - ensure("written as past the range: " + r.text, r.text.find("<-1.0e+999, 1.0e+999, 0") != std::string::npos); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); + UNSCOPED_INFO("written as past the range: " + r.text); + REQUIRE(r.text.find("<-1.0e+999, 1.0e+999, 0") != std::string::npos); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text, target != ALLSLOptimizer::Target::LSO)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } // The check a run makes of what it wrote, which gives the source // back where what was written does not compile. - ensure("a script that compiles passes", !ALLSLOptimizer::checkWritten(source).has_value()); + UNSCOPED_INFO("a script that compiles passes"); + REQUIRE_FALSE(ALLSLOptimizer::checkWritten(source).has_value()); const std::optional refused = ALLSLOptimizer::checkWritten("default { state_entry() { llOwnerSay(inf.0); } }\n"); - ensure("one that does not is refused", refused.has_value()); - ensure("as a warning with the parser's words", - refused->key == "OptimizerWroteUncompilable" && refused->severity == ALScriptProblem::Severity::Warning && !refused->args[0].empty()); + UNSCOPED_INFO("one that does not is refused"); + REQUIRE(refused.has_value()); + { + INFO("as a warning with the parser's words"); + CHECK(refused->key == "OptimizerWroteUncompilable"); + CHECK(refused->severity == ALScriptProblem::Severity::Warning); + REQUIRE_FALSE(refused->args[0].empty()); + } } - template<> template<> - void allsloptimizer_object::test<38>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a global constant is inlined only where that is cheaper on the target", "[allsloptimizer]") { - set_test_name("a global's value is written where it is read only where that costs less than the global, on each target"); const auto reads = [](const std::string& global, const std::string& read, int times) { std::string body; for (int i = 0; i < times; ++i) @@ -1492,35 +1862,48 @@ namespace tut // is not on LSO, nor on Mono unless its parts are whole. const std::string V = "vector V = <1.5, 2.5, 3.5>;"; const std::string W = "vector W = <1.0, 2.0, 3.0>;"; - ensure("a vector read once goes on LSO", !kept(ALLSLOptimizer::run(reads(V, "v += V;", 1), lso), "vector V")); - ensure("read three times it stays on LSO", kept(ALLSLOptimizer::run(reads(V, "v += V;", 3), lso), "vector V")); - ensure("and on Mono", kept(ALLSLOptimizer::run(reads(V, "v += V;", 3), mono), "vector V")); - ensure("but not whole on Mono", !kept(ALLSLOptimizer::run(reads(W, "v += W;", 3), mono), "vector W")); - ensure("and not on Luau, which keeps a constant once", !kept(ALLSLOptimizer::run(reads(V, "v += V;", 3), luau), "vector V")); + UNSCOPED_INFO("a vector read once goes on LSO"); + CHECK_FALSE(kept(ALLSLOptimizer::run(reads(V, "v += V;", 1), lso), "vector V")); + UNSCOPED_INFO("read three times it stays on LSO"); + CHECK(kept(ALLSLOptimizer::run(reads(V, "v += V;", 3), lso), "vector V")); + UNSCOPED_INFO("and on Mono"); + CHECK(kept(ALLSLOptimizer::run(reads(V, "v += V;", 3), mono), "vector V")); + UNSCOPED_INFO("but not whole on Mono"); + CHECK_FALSE(kept(ALLSLOptimizer::run(reads(W, "v += W;", 3), mono), "vector W")); + UNSCOPED_INFO("and not on Luau, which keeps a constant once"); + CHECK_FALSE(kept(ALLSLOptimizer::run(reads(V, "v += V;", 3), luau), "vector V")); // A float goes on LSO and Luau however often; on Mono, up to where // nine bytes a read come to the field. const std::string F = "float F = 2.5;"; - ensure("a float read twenty times goes on LSO", !kept(ALLSLOptimizer::run(reads(F, "f += F;", 20), lso), "float F")); - ensure("and on Luau", !kept(ALLSLOptimizer::run(reads(F, "f += F;", 20), luau), "float F")); - ensure("but stays on Mono", kept(ALLSLOptimizer::run(reads(F, "f += F;", 20), mono), "float F")); - ensure("which takes it read three times", !kept(ALLSLOptimizer::run(reads(F, "f += F;", 3), mono), "float F")); + UNSCOPED_INFO("a float read twenty times goes on LSO"); + CHECK_FALSE(kept(ALLSLOptimizer::run(reads(F, "f += F;", 20), lso), "float F")); + UNSCOPED_INFO("and on Luau"); + CHECK_FALSE(kept(ALLSLOptimizer::run(reads(F, "f += F;", 20), luau), "float F")); + UNSCOPED_INFO("but stays on Mono"); + CHECK(kept(ALLSLOptimizer::run(reads(F, "f += F;", 20), mono), "float F")); + UNSCOPED_INFO("which takes it read three times"); + CHECK_FALSE(kept(ALLSLOptimizer::run(reads(F, "f += F;", 3), mono), "float F")); // A string: Mono holds it once whatever; LSO writes it at each // place, which a long one read often does not pay for. const std::string S = "string S = \"a sentence long enough to matter\";"; - ensure("a long string read five times stays on LSO", kept(ALLSLOptimizer::run(reads(S, "s += S;", 5), lso), "string S")); - ensure("and goes on Mono", !kept(ALLSLOptimizer::run(reads(S, "s += S;", 5), mono), "string S")); + UNSCOPED_INFO("a long string read five times stays on LSO"); + CHECK(kept(ALLSLOptimizer::run(reads(S, "s += S;", 5), lso), "string S")); + UNSCOPED_INFO("and goes on Mono"); + CHECK_FALSE(kept(ALLSLOptimizer::run(reads(S, "s += S;", 5), mono), "string S")); // Folded into a larger value, a global is no read of it. const ALLSLOptimizer::Result folded = ALLSLOptimizer::run(reads(V, "v += V * 2;", 3), lso); - ensure("a product folds: " + folded.text, !kept(folded, "vector V") && folded.text.find("<3.0, 5.0, 7.0>") != std::string::npos); + { + INFO("a product folds: " + folded.text); + CHECK_FALSE(kept(folded, "vector V")); + REQUIRE(folded.text.find("<3.0, 5.0, 7.0>") != std::string::npos); + } } - template<> template<> - void allsloptimizer_object::test<39>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: inlining by cost inlines a many-called function only where it is smaller", "[allsloptimizer]") { - set_test_name("with inlining, a function called from several places goes where the target's compiler says that is smaller, and says so"); const std::string source = "say(integer n)\n{\n llOwnerSay(\"n=\" + (string)n);\n}\n" "tell(integer n)\n{\n if (n > 2)\n llOwnerSay(\"many: \" + (string)n + \" of them, which is a lot\");\n" " else\n llOwnerSay(\"a few: \" + (string)n);\n llSetText((string)n, <1.0, 0.5, 0.25>, 1.0);\n}\n" @@ -1533,23 +1916,37 @@ namespace tut o.target = target; o.inlineByCost = true; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); - ensure("the small one goes: " + r.text, r.text.find("say(") == std::string::npos); - ensure("the large one stays: " + r.text, r.text.find("tell(") != std::string::npos); - ensure("weighed", r.weight.compiled && r.weight.total > 0); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); + UNSCOPED_INFO("the small one goes: " + r.text); + REQUIRE(r.text.find("say(") == std::string::npos); + UNSCOPED_INFO("the large one stays: " + r.text); + REQUIRE(r.text.find("tell(") != std::string::npos); + REQUIRE(r.weight.compiled); + REQUIRE(r.weight.total > 0); const ALScriptProblem* chose = nullptr; for (const ALScriptProblem& p : r.problems) { chose = p.key == "InlinerChoseFunction" ? &p : chose; } - ensure("said: " + notes(r), chose && chose->args.size() == 4 && chose->args[0] == "say" && chose->args[1] == "3"); - ensure_equals("at the function", chose->line, 0); - ensure_equals("to its end", chose->endLine, 3); + { + INFO("said: " + notes(r)); + REQUIRE(chose); + REQUIRE(chose->args.size() == 4); + REQUIRE(chose->args[0] == "say"); + REQUIRE(chose->args[1] == "3"); + } + UNSCOPED_INFO("at the function"); + REQUIRE(chose->line == 0); + UNSCOPED_INFO("to its end"); + REQUIRE(chose->endLine == 3); o.inlineByCost = false; const ALLSLOptimizer::Result left = ALLSLOptimizer::run(source, o); - ensure("left alone without it: " + left.text, left.text.find("say(") != std::string::npos); - ensure("and nothing weighed", !left.weight.compiled); + UNSCOPED_INFO("left alone without it: " + left.text); + REQUIRE(left.text.find("say(") != std::string::npos); + UNSCOPED_INFO("and nothing weighed"); + REQUIRE_FALSE(left.weight.compiled); } // On Luau, where a call costs little, putting it in place comes to // a byte more: tried, and not kept. @@ -1558,13 +1955,15 @@ namespace tut const ALLSLOptimizer::Result weighed = ALLSLOptimizer::run(source, o); o.inlineByCost = false; const ALLSLOptimizer::Result unweighed = ALLSLOptimizer::run(source, o); - ensure("never larger for trying", weighed.weight.compiled && weighed.weight.total <= ALScriptWeigh::lslLuau(unweighed.text).total); + { + INFO("never larger for trying"); + CHECK(weighed.weight.compiled); + CHECK(weighed.weight.total <= ALScriptWeigh::lslLuau(unweighed.text).total); + } } - template<> template<> - void allsloptimizer_object::test<40>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: -1 tests and small steps and list elements take each target's smaller form", "[allsloptimizer]") { - set_test_name("-1 tests, one and two either way, increments and list elements are written as each target has them smaller, and compile"); const std::string source = kept(wrap("list l;\ninteger g;\n", " integer a = (integer)llFrand(9);\n integer b = (integer)llFrand(9);\n string s = llGetObjectName();\n" " integer found = llListFindList(l, [a]);\n" @@ -1591,57 +1990,85 @@ namespace tut ALLSLOptimizer::Options o = allsloptimizer_data::options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + allsloptimizer_data::notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + allsloptimizer_data::notes(r)); + REQUIRE(r.optimized); ALLSLService service; const ALScriptProblems said = service.check(r.text, target != ALLSLOptimizer::Target::LSO); for (const ALScriptProblem& p : said) { - ensure("no error in what was written: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("no error in what was written: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } return r.text; }; const auto has = [](const std::string& text, const std::string& what) { return text.find(what) != std::string::npos; }; const std::string lso = run(ALLSLOptimizer::Target::LSO); - ensure("x != -1 as ~x, in a condition: " + lso, has(lso, "if (~a)")); - ensure("finds, both ways round, inside &&: " + lso, has(lso, "if (~found && ~llSubStringIndex(s, \"x\"))")); - ensure("a find < 0 as !~find, under a !", has(lso, "if (!!~found)") || has(lso, "if (~found)")); - ensure("x == -1 as !~x, as a value", has(lso, "g = !~a;")); - ensure("a find < 0 as !~find, as a value, a local set to a find", has(lso, "g = !~found;")); - ensure("x + 1 as -~x", has(lso, "g = -~a;")); - ensure("2 + x as -~-~x, the product bracketed", has(lso, "g = -~-~(a * b);")); - ensure("x - 1 as ~-x", has(lso, "g = ~-b;")); - ensure("x - 2 as ~-~-x, a minus bracketed after a minus", has(lso, "g = ~-~-(-a);")); - ensure("x != -1 as a value stays", has(lso, "g = a != -1;")); - ensure("an increment before", has(lso, "++a;")); - ensure("and a for's step, the loop a do on LSO, its first check known", has(lso, "--b")); - ensure("l + [a] as l + a", has(lso, "l = l + a;")); - ensure("l + [a, s] as l + a + s", has(lso, "l = l + a + s;")); - ensure("[s] + l as s + l", has(lso, "l = s + l;")); - ensure("l + (list)s as l + s", has(lso, "l = l + s;")); - ensure("l += [b] as l += b", has(lso, "l += b;")); - ensure("elements whose order would show stay together", !has(lso, "l + llFrand(1) + llFrand(2)")); + UNSCOPED_INFO("x != -1 as ~x, in a condition: " + lso); + CHECK(has(lso, "if (~a)")); + UNSCOPED_INFO("finds, both ways round, inside &&: " + lso); + CHECK(has(lso, "if (~found && ~llSubStringIndex(s, \"x\"))")); + UNSCOPED_INFO("a find < 0 as !~find, under a !"); + CHECK((has(lso, "if (!!~found)") || has(lso, "if (~found)"))); + UNSCOPED_INFO("x == -1 as !~x, as a value"); + CHECK(has(lso, "g = !~a;")); + UNSCOPED_INFO("a find < 0 as !~find, as a value, a local set to a find"); + CHECK(has(lso, "g = !~found;")); + UNSCOPED_INFO("x + 1 as -~x"); + CHECK(has(lso, "g = -~a;")); + UNSCOPED_INFO("2 + x as -~-~x, the product bracketed"); + CHECK(has(lso, "g = -~-~(a * b);")); + UNSCOPED_INFO("x - 1 as ~-x"); + CHECK(has(lso, "g = ~-b;")); + UNSCOPED_INFO("x - 2 as ~-~-x, a minus bracketed after a minus"); + CHECK(has(lso, "g = ~-~-(-a);")); + UNSCOPED_INFO("x != -1 as a value stays"); + CHECK(has(lso, "g = a != -1;")); + UNSCOPED_INFO("an increment before"); + CHECK(has(lso, "++a;")); + UNSCOPED_INFO("and a for's step, the loop a do on LSO, its first check known"); + CHECK(has(lso, "--b")); + UNSCOPED_INFO("l + [a] as l + a"); + CHECK(has(lso, "l = l + a;")); + UNSCOPED_INFO("l + [a, s] as l + a + s"); + CHECK(has(lso, "l = l + a + s;")); + UNSCOPED_INFO("[s] + l as s + l"); + CHECK(has(lso, "l = s + l;")); + UNSCOPED_INFO("l + (list)s as l + s"); + CHECK(has(lso, "l = l + s;")); + UNSCOPED_INFO("l += [b] as l += b"); + CHECK(has(lso, "l += b;")); + UNSCOPED_INFO("elements whose order would show stay together"); + CHECK_FALSE(has(lso, "l + llFrand(1) + llFrand(2)")); const std::string mono = run(ALLSLOptimizer::Target::Mono); - ensure("Mono: x != -1 as ~x: " + mono, has(mono, "if (~a)")); + UNSCOPED_INFO("Mono: x != -1 as ~x: " + mono); + CHECK(has(mono, "if (~a)")); // Mono's ldc.i4 is five bytes, -1 and 1 too. - ensure("Mono: x == -1 as !~x: " + mono, has(mono, "g = !~a;")); - ensure("Mono: x + 1 as -~x", has(mono, "g = -~a;")); - ensure("Mono: x - 1 as ~-x", has(mono, "g = ~-b;")); - ensure("Mono: ++a", has(mono, "++a;")); - ensure("Mono: what it wrote weighs what it says", mono.find("l = l") != std::string::npos); + UNSCOPED_INFO("Mono: x == -1 as !~x: " + mono); + CHECK(has(mono, "g = !~a;")); + UNSCOPED_INFO("Mono: x + 1 as -~x"); + CHECK(has(mono, "g = -~a;")); + UNSCOPED_INFO("Mono: x - 1 as ~-x"); + CHECK(has(mono, "g = ~-b;")); + UNSCOPED_INFO("Mono: ++a"); + CHECK(has(mono, "++a;")); + UNSCOPED_INFO("Mono: what it wrote weighs what it says"); + REQUIRE(mono.find("l = l") != std::string::npos); const std::string luau = run(ALLSLOptimizer::Target::Luau); - ensure("Luau: x != -1 stays, ~ a library call there: " + luau, has(luau, "if (a != -1)")); - ensure("Luau: x - 1 stays", has(luau, "g = b - 1;")); - ensure("Luau: a++ stays", has(luau, "a++;")); - ensure("Luau: l + a", has(luau, "l = l + a;")); + UNSCOPED_INFO("Luau: x != -1 stays, ~ a library call there: " + luau); + CHECK(has(luau, "if (a != -1)")); + UNSCOPED_INFO("Luau: x - 1 stays"); + CHECK(has(luau, "g = b - 1;")); + UNSCOPED_INFO("Luau: a++ stays"); + CHECK(has(luau, "a++;")); + UNSCOPED_INFO("Luau: l + a"); + CHECK(has(luau, "l = l + a;")); } - template<> template<> - void allsloptimizer_object::test<41>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: on Mono list shapes are kept only where they pay for the helpers they need", "[allsloptimizer]") { - set_test_name("on Mono a list's shapes are weighed against the helpers they need, and kept only where they pay for them"); // A literal whose order would show, which stays, and a list added // to a list: the helpers a literal and a list's sum need are the // script's whatever happens to the rest. @@ -1656,23 +2083,36 @@ namespace tut // Once: adding a string bare needs a helper nothing else in the // script uses, which costs more than the one place saves. const ALLSLOptimizer::Result once = ALLSLOptimizer::run(adding(1), options()); - ensure("optimized: " + notes(once), once.optimized); - ensure("once, the literal stays: " + once.text, once.text.find("l += [llGetSubString") != std::string::npos); - ensure("weighed, as written", once.weight.compiled && once.weight.total == ALScriptWeigh::mono(once.text).total); + UNSCOPED_INFO("optimized: " + notes(once)); + CHECK(once.optimized); + UNSCOPED_INFO("once, the literal stays: " + once.text); + REQUIRE(once.text.find("l += [llGetSubString") != std::string::npos); + { + INFO("weighed, as written"); + CHECK(once.weight.compiled); + CHECK(once.weight.total == ALScriptWeigh::mono(once.text).total); + } // Often: the places pay for it. const ALLSLOptimizer::Result often = ALLSLOptimizer::run(adding(4), options()); - ensure("often, bare: " + often.text, often.text.find("l += llGetSubString") != std::string::npos && often.text.find("l += [") == std::string::npos); - ensure("weighed, as written", often.weight.compiled && often.weight.total == ALScriptWeigh::mono(often.text).total); + { + INFO("often, bare: " + often.text); + REQUIRE(often.text.find("l += llGetSubString") != std::string::npos); + REQUIRE(often.text.find("l += [") == std::string::npos); + } + { + INFO("weighed, as written"); + CHECK(often.weight.compiled); + CHECK(often.weight.total == ALScriptWeigh::mono(often.text).total); + } // LSO holds no helpers for a list: bare, once or often. ALLSLOptimizer::Options lso = options(); lso.target = ALLSLOptimizer::Target::LSO; - ensure("LSO, bare once", ALLSLOptimizer::run(adding(1), lso).text.find("l += llGetSubString") != std::string::npos); + UNSCOPED_INFO("LSO, bare once"); + REQUIRE(ALLSLOptimizer::run(adding(1), lso).text.find("l += llGetSubString") != std::string::npos); } - template<> template<> - void allsloptimizer_object::test<42>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a looked-up key given as NULL_KEY or a non-key becomes the empty string", "[allsloptimizer]") { - set_test_name("NULL_KEY, or no key at all, given to a library function for a key it looks up, is given as \"\"; not to llMessageLinked, nor for a string"); const std::string source = wrap("say(key k)\n{\n llOwnerSay((string)k);\n}\n", " key o = llGetOwnerKey(NULL_KEY);\n" " llListen(0, \"\", NULL_KEY, \"\");\n" @@ -1687,27 +2127,34 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); const auto has = [&r](const std::string& what) { return r.text.find(what) != std::string::npos; }; - ensure("a lookup's null key: " + r.text, has("llGetOwnerKey(\"\")")); - ensure("a listen's", has("llListen(0, \"\", \"\", \"\")")); - ensure("no key at all", has("llGiveInventory(\"\", \"x\")")); - ensure("a key stays", has("\"01234567-89ab-cdef-0123-456789abcdef\"")); - ensure("a linked message's stays, passed on as it came", has("llMessageLinked(LINK_SET, 0, \"\", NULL_KEY)")); - ensure("a string's stays", has("llOwnerSay(NULL_KEY)")); - ensure("a function of the script's has its own", !has("say(\"\")")); + UNSCOPED_INFO("a lookup's null key: " + r.text); + REQUIRE(has("llGetOwnerKey(\"\")")); + UNSCOPED_INFO("a listen's"); + REQUIRE(has("llListen(0, \"\", \"\", \"\")")); + UNSCOPED_INFO("no key at all"); + REQUIRE(has("llGiveInventory(\"\", \"x\")")); + UNSCOPED_INFO("a key stays"); + REQUIRE(has("\"01234567-89ab-cdef-0123-456789abcdef\"")); + UNSCOPED_INFO("a linked message's stays, passed on as it came"); + REQUIRE(has("llMessageLinked(LINK_SET, 0, \"\", NULL_KEY)")); + UNSCOPED_INFO("a string's stays"); + REQUIRE(has("llOwnerSay(NULL_KEY)")); + UNSCOPED_INFO("a function of the script's has its own"); + REQUIRE_FALSE(has("say(\"\")")); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text, target != ALLSLOptimizer::Target::LSO)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } } - template<> template<> - void allsloptimizer_object::test<43>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: unreachable functions and states and write-only variables are removed", "[allsloptimizer]") { - set_test_name("functions and states nothing that runs reaches go, cycles too; a variable set and never read goes, what it was set to still run"); const std::string source = "integer count;\ninteger shown;\nlist l;\n" "ping()\n{\n pong();\n}\npong()\n{\n ping();\n}\n" "leave()\n{\n if (llFrand(1.0) > 0.5)\n state away;\n}\n" @@ -1728,33 +2175,61 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); const auto has = [&r](const std::string& what) { return r.text.find(what) != std::string::npos; }; - ensure("functions calling only each other: " + r.text, !has("ping") && !has("pong")); - ensure("one nothing calls, and the states only it and they enter", !has("leave") && !has("state away") && !has("state back")); - ensure("a state entered stays", has("state on")); - ensure("a listen's handle never read: the call stays: " + r.text, has("llListen(0, \"\", \"\", \"\");") && !has("integer h")); - ensure("a global only set, and a read of the clock that set it", !has("count") && !has("llGetUnixTime();\n integer")); - ensure("a loop's counter never read: " + r.text, !has("i = 0") && !has("i++") && !has("integer i") && has("for (; ") && has(" > 5; )")); - ensure("a global read stays", has("shown = llGetUnixTime();")); - ensure("a comparison left empty turned around the other way: " + r.text, has("if (llGetObjectName() != \"on\")")); - ensure("anything else, what it negates bracketed; Luau's odd test a remainder", - has(target == ALLSLOptimizer::Target::Luau ? "if (!(llGetUnixTime() % 2))" : "if (!(llGetUnixTime() & 1))")); - ensure("said, as set and never read", std::any_of(r.problems.begin(), r.problems.end(), [](const ALScriptProblem& p) { + { + INFO("functions calling only each other: " + r.text); + REQUIRE_FALSE(has("ping")); + REQUIRE_FALSE(has("pong")); + } + { + INFO("one nothing calls, and the states only it and they enter"); + REQUIRE_FALSE(has("leave")); + REQUIRE_FALSE(has("state away")); + REQUIRE_FALSE(has("state back")); + } + UNSCOPED_INFO("a state entered stays"); + REQUIRE(has("state on")); + { + INFO("a listen's handle never read: the call stays: " + r.text); + REQUIRE(has("llListen(0, \"\", \"\", \"\");")); + REQUIRE_FALSE(has("integer h")); + } + { + INFO("a global only set, and a read of the clock that set it"); + REQUIRE_FALSE(has("count")); + REQUIRE_FALSE(has("llGetUnixTime();\n integer")); + } + { + INFO("a loop's counter never read: " + r.text); + REQUIRE_FALSE(has("i = 0")); + REQUIRE_FALSE(has("i++")); + REQUIRE_FALSE(has("integer i")); + REQUIRE(has("for (; ")); + REQUIRE(has(" > 5; )")); + } + UNSCOPED_INFO("a global read stays"); + REQUIRE(has("shown = llGetUnixTime();")); + UNSCOPED_INFO("a comparison left empty turned around the other way: " + r.text); + REQUIRE(has("if (llGetObjectName() != \"on\")")); + UNSCOPED_INFO("anything else, what it negates bracketed; Luau's odd test a remainder"); + REQUIRE(has(target == ALLSLOptimizer::Target::Luau ? "if (!(llGetUnixTime() % 2))" : "if (!(llGetUnixTime() & 1))")); + UNSCOPED_INFO("said, as set and never read"); + REQUIRE(std::any_of(r.problems.begin(), r.problems.end(), [](const ALScriptProblem& p) { return p.key == "OptimizerRemovedWriteOnly" && p.args.size() == 1 && p.args[0] == "count"; })); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text, target != ALLSLOptimizer::Target::LSO)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } } - template<> template<> - void allsloptimizer_object::test<44>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: comparisons and their negations take each target's smaller form", "[allsloptimizer]") { - set_test_name("comparisons as each target has them smaller: the ! of one, != and || where only truth counts, an if on ==, and inclusive against a constant"); const std::string source = wrap("", " list l = llParseString2List(llGetObjectName(), [\" \"], []);\n list m = llParseString2List(llGetObjectDesc(), [\" \"], []);\n" " integer a = (integer)llFrand(9);\n integer b = (integer)llFrand(9);\n" @@ -1776,33 +2251,51 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); const auto has = [&r](const std::string& what) { return r.text.find(what) != std::string::npos; }; const bool lso = target == ALLSLOptimizer::Target::LSO; const bool mono = target == ALLSLOptimizer::Target::Mono; - ensure("the ! of a comparison of integers: " + r.text, has("a >= b") && !has("!(a < b)")); - ensure("of strings, ==: " + r.text, has("s != \"x\"") && !has("!(s == \"x\")")); - ensure("not of lists, whose != is a length", has("!(l == m)")); - ensure("not of floats, which may be no number", has("!(f < g)")); - ensure("!= where only truth counts, a difference on Luau", has(lso || mono ? "if (a ^ b)" : "if (a - b)")); - ensure("an if on ==, its branches swapped", - lso || mono ? has("if (a ^ b)\n llOwnerSay(\"ne\");\n else\n llOwnerSay(\"eq\");") : has("if (!(a - b))")); - ensure("inclusive against a constant", has(mono ? "if (a > 4)" : "if (a >= 5)") && has(mono ? "if (b < 6)" : "if (b <= 5)")); - ensure("not past the largest integer", has("if (b <= 2147483647)")); - ensure("|| where only truth counts", has(mono ? "while (a | b)" : "while (a || b)")); - ensure("a string's length where only truth counts: " + r.text, has(lso ? "if (s != \"\")" : "if (llStringLength(s))")); + { + INFO("the ! of a comparison of integers: " + r.text); + REQUIRE(has("a >= b")); + REQUIRE_FALSE(has("!(a < b)")); + } + { + INFO("of strings, ==: " + r.text); + REQUIRE(has("s != \"x\"")); + REQUIRE_FALSE(has("!(s == \"x\")")); + } + UNSCOPED_INFO("not of lists, whose != is a length"); + REQUIRE(has("!(l == m)")); + UNSCOPED_INFO("not of floats, which may be no number"); + REQUIRE(has("!(f < g)")); + UNSCOPED_INFO("!= where only truth counts, a difference on Luau"); + REQUIRE(has(lso || mono ? "if (a ^ b)" : "if (a - b)")); + UNSCOPED_INFO("an if on ==, its branches swapped"); + REQUIRE((lso || mono ? has("if (a ^ b)\n llOwnerSay(\"ne\");\n else\n llOwnerSay(\"eq\");") : has("if (!(a - b))"))); + { + INFO("inclusive against a constant"); + REQUIRE(has(mono ? "if (a > 4)" : "if (a >= 5)")); + REQUIRE(has(mono ? "if (b < 6)" : "if (b <= 5)")); + } + UNSCOPED_INFO("not past the largest integer"); + REQUIRE(has("if (b <= 2147483647)")); + UNSCOPED_INFO("|| where only truth counts"); + REQUIRE(has(mono ? "while (a | b)" : "while (a || b)")); + UNSCOPED_INFO("a string's length where only truth counts: " + r.text); + REQUIRE(has(lso ? "if (s != \"\")" : "if (llStringLength(s))")); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text, !lso)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } } - template<> template<> - void allsloptimizer_object::test<45>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: list dumps and lone object details become casts as do Mono's whole floats", "[allsloptimizer]") { - set_test_name("a list dumped with nothing between as a cast, one object detail read as a string as a cast, and a whole float as the cast of an integer on Mono"); const std::string source = wrap("list L = [2.0];\n", " list l = llParseString2List(llGetObjectName(), [\" \"], []);\n" " string d = llDumpList2String(l, \"\");\n" @@ -1817,33 +2310,39 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); const auto has = [&r](const std::string& what) { return r.text.find(what) != std::string::npos; }; const bool lso = target == ALLSLOptimizer::Target::LSO; // On LSO and Mono; on Luau a place saves nothing. - ensure("dumped with nothing between: " + r.text, - target == ALLSLOptimizer::Target::Luau ? has("llDumpList2String(l, \"\")") : has("(string)l") && !has("llDumpList2String(l, \"\")")); - ensure("with something between, a dump", has("llDumpList2String(l, \", \")")); + UNSCOPED_INFO("dumped with nothing between: " + r.text); + REQUIRE((target == ALLSLOptimizer::Target::Luau ? has("llDumpList2String(l, \"\")") : has("(string)l") && !has("llDumpList2String(l, \"\")"))); + UNSCOPED_INFO("with something between, a dump"); + REQUIRE(has("llDumpList2String(l, \", \")")); // On LSO; on Mono a byte a place, against a helper the script may lack. - ensure("one detail: " + r.text, lso == (has("(string)llGetObjectDetails(llGetOwner(), [OBJECT_NAME])") || + UNSCOPED_INFO("one detail: " + r.text); + REQUIRE(lso == (has("(string)llGetObjectDetails(llGetOwner(), [OBJECT_NAME])") || has("(string)llGetObjectDetails(llGetOwner(), (list)OBJECT_NAME)"))); - ensure("two, a read", has("llList2String(llGetObjectDetails(llGetOwner(), ")); - ensure("the second, a read", has("OBJECT_NAME]), 1)") || has("(list)OBJECT_NAME), 1)")); + UNSCOPED_INFO("two, a read"); + REQUIRE(has("llList2String(llGetObjectDetails(llGetOwner(), ")); + UNSCOPED_INFO("the second, a read"); + REQUIRE((has("OBJECT_NAME]), 1)") || has("(list)OBJECT_NAME), 1)"))); const bool mono = target == ALLSLOptimizer::Target::Mono; - ensure("a whole float in a list: " + r.text, has(mono ? "((float)-3)" : "-3.0")); - ensure("not in a global's value", has("list L = [2.0];")); + UNSCOPED_INFO("a whole float in a list: " + r.text); + REQUIRE(has(mono ? "((float)-3)" : "-3.0")); + UNSCOPED_INFO("not in a global's value"); + REQUIRE(has("list L = [2.0];")); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text, !lso)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } } - template<> template<> - void allsloptimizer_object::test<46>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: code after a reset or any dead end goes and so do harmless empty handlers", "[allsloptimizer]") { - set_test_name("what follows a reset, an if none of whose ways goes on, or a loop that never ends never runs; an empty handler goes where having it changes nothing"); const std::string source = "integer a;\n" "both()\n{\n if (a)\n {\n llOwnerSay(\"a\");\n return;\n }\n else\n {\n llOwnerSay(\"b\");\n return;\n }\n" " llOwnerSay(\"after both\");\n}\n" @@ -1856,26 +2355,36 @@ namespace tut " collision_start(integer n)\n {\n }\n}\n" "state idle\n{\n timer()\n {\n }\n}\n"; ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); const auto has = [&r](const std::string& what) { return r.text.find(what) != std::string::npos; }; - ensure("after an if none of whose ways goes on: " + r.text, !has("after both")); - ensure("after a loop that never ends", !has("after forever")); - ensure("after one jumped out of, it runs", has("after out")); - ensure("after a reset", !has("after reset") && has("llResetScript();")); - ensure("an empty state_entry goes", !has("state_entry")); - ensure("an empty collision stays, which having makes the object collide so", has("collision_start(integer n)")); - ensure("a state keeps its one handler", !has("state idle") || has("timer()")); + UNSCOPED_INFO("after an if none of whose ways goes on: " + r.text); + CHECK_FALSE(has("after both")); + UNSCOPED_INFO("after a loop that never ends"); + CHECK_FALSE(has("after forever")); + UNSCOPED_INFO("after one jumped out of, it runs"); + CHECK(has("after out")); + { + INFO("after a reset"); + CHECK_FALSE(has("after reset")); + CHECK(has("llResetScript();")); + } + UNSCOPED_INFO("an empty state_entry goes"); + CHECK_FALSE(has("state_entry")); + UNSCOPED_INFO("an empty collision stays, which having makes the object collide so"); + CHECK(has("collision_start(integer n)")); + UNSCOPED_INFO("a state keeps its one handler"); + CHECK((!has("state idle") || has("timer()"))); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } - template<> template<> - void allsloptimizer_object::test<47>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: llRound and llAcos and list dumps and finds fold where the answer is known", "[allsloptimizer]") { - set_test_name("more of the library folded where its answer is known: llRound, llAcos, llAsin, llDumpList2String, llListFindList, llMD5String"); const std::string source = wrap("", " llOwnerSay((string)[llRound(2.5), llRound(-2.5), llRound(0.49999997), llAcos(1.0), llAsin(2.0)]);\n" " llOwnerSay(llDumpList2String([1, 2.5, \"a\"], \"-\"));\n" @@ -1884,25 +2393,40 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = ALLSLOptimizer::Target::LSO; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); const auto has = [&r](const std::string& what) { return r.text.find(what) != std::string::npos; }; - ensure("llRound half up: " + r.text, !has("llRound(2.5)") && !has("llRound(-2.5)")); - ensure("llRound where single and double would differ, left", has("llRound(")); - ensure("llAcos folded, llAsin out of its range left", !has("llAcos") && has("llAsin(2.0)")); - ensure("a dump folded", has("\"1-2.500000-a\"")); - ensure("a find folded, and one not found", !has("llListFindList([1, \"a\", 2]") && !has("[3])")); - ensure("an empty list to find left, and a string no integer", has("llListFindList((list)1 + 2, [])") || has("llListFindList([1, 2], [])")); + { + INFO("llRound half up: " + r.text); + CHECK_FALSE(has("llRound(2.5)")); + CHECK_FALSE(has("llRound(-2.5)")); + } + UNSCOPED_INFO("llRound where single and double would differ, left"); + CHECK(has("llRound(")); + { + INFO("llAcos folded, llAsin out of its range left"); + CHECK_FALSE(has("llAcos")); + CHECK(has("llAsin(2.0)")); + } + UNSCOPED_INFO("a dump folded"); + CHECK(has("\"1-2.500000-a\"")); + { + INFO("a find folded, and one not found"); + CHECK_FALSE(has("llListFindList([1, \"a\", 2]")); + CHECK_FALSE(has("[3])")); + } + UNSCOPED_INFO("an empty list to find left, and a string no integer"); + CHECK((has("llListFindList((list)1 + 2, [])") || has("llListFindList([1, 2], [])"))); ALLSLService service; for (const ALScriptProblem& p : service.check(r.text, false)) { - ensure("what was written compiles: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("what was written compiles: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } } - template<> template<> - void allsloptimizer_object::test<48>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a call answering a longer string folds only where the answer is smaller", "[allsloptimizer]") { - set_test_name("a call answered with a string longer than it was given is folded where the answer is the smaller"); using Target = ALLSLOptimizer::Target; const auto weigh = [](Target target, const std::string& text) { return target == Target::LSO ? ALScriptWeigh::lso(text) : target == Target::Mono ? ALScriptWeigh::mono(text) : ALScriptWeigh::lslLuau(text); @@ -1913,31 +2437,47 @@ namespace tut o.target = target; const std::string source = allsloptimizer_data::wrap("", body); const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + allsloptimizer_data::notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + allsloptimizer_data::notes(r)); + REQUIRE(r.optimized); const ALScriptWeight before = weigh(target, source); const ALScriptWeight after = weigh(target, r.text); - ensure("no larger: " + r.text, before.compiled && after.compiled && after.total <= before.total); + { + INFO("no larger: " + r.text); + REQUIRE(before.compiled); + REQUIRE(after.compiled); + REQUIRE(after.total <= before.total); + } return r.text.find("llMD5String") == std::string::npos; }; const std::string hashOfX = "\"70c6ab518c36e18d9925c3c27c048b26\""; for (const Target target : { Target::LSO, Target::Mono, Target::Luau }) { - ensure("a hash of one character is smaller as the call", !folded(target, " llOwnerSay(llMD5String(\"x\", 0));\n")); + INFO("a hash of one character is smaller as the call"); + REQUIRE_FALSE(folded(target, " llOwnerSay(llMD5String(\"x\", 0));\n")); } // LSO writes the argument where it is used, as it would the answer. - ensure("a hash of a longer string is smaller as its answer on LSO", - folded(Target::LSO, " llOwnerSay(llMD5String(\"Hello, Avatar!\", 0));\n")); + { + INFO("a hash of a longer string is smaller as its answer on LSO"); + CHECK(folded(Target::LSO, " llOwnerSay(llMD5String(\"Hello, Avatar!\", 0));\n")); + } // Mono and Luau hold a string once: where a function holds the // answer already, it costs a load. const std::string held = " llOwnerSay(llMD5String(\"x\", 0));\n llOwnerSay(" + hashOfX + ");\n"; - ensure("held already on Mono", folded(Target::Mono, held)); - ensure("held already on Luau", folded(Target::Luau, held)); - ensure("LSO writes it again", !folded(Target::LSO, held)); + { + INFO("held already on Mono"); + CHECK(folded(Target::Mono, held)); + } + { + INFO("held already on Luau"); + CHECK(folded(Target::Luau, held)); + } + { + INFO("LSO writes it again"); + CHECK_FALSE(folded(Target::LSO, held)); + } } - template<> template<> - void allsloptimizer_object::test<49>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: same-named labels in a handler keep one short name when names shrink", "[allsloptimizer]") { - set_test_name("labels of one name in one handler keep one name when names are shortened, as a jump finds its label by name"); const std::string source = "default\n{\n state_entry()\n {\n" " if (1)\n {\n jump there;\n @there;\n }\n" " if (0)\n {\n jump there;\n @there;\n llOwnerSay(\"reached\");\n }\n" @@ -1949,9 +2489,12 @@ namespace tut o.target = target; o.shrinknames = true; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized, not refused: " + notes(r), r.optimized); - ensure("the label jumped to from the first block is still there: " + r.text, r.text.find("reached") != std::string::npos); - ensure("renamed: " + r.text, r.text.find("there") == std::string::npos); + UNSCOPED_INFO("optimized, not refused: " + notes(r)); + REQUIRE(r.optimized); + UNSCOPED_INFO("the label jumped to from the first block is still there: " + r.text); + REQUIRE(r.text.find("reached") != std::string::npos); + UNSCOPED_INFO("renamed: " + r.text); + REQUIRE(r.text.find("there") == std::string::npos); // In state_entry, every jump and every label one name. const size_t entry = r.text.find("state_entry"); const size_t touch = r.text.find("touch_start"); @@ -1966,13 +2509,12 @@ namespace tut names.insert(body.substr(from, body.find(';', from) - from)); } } - ensure_equals("one name in state_entry: " + r.text, names.size(), size_t(1)); + UNSCOPED_INFO("one name in state_entry: " + r.text); + REQUIRE(names.size() == size_t(1)); } } - template<> template<> - void allsloptimizer_object::test<50>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: names the target keeps shrink first and Mono names a state for a held string", "[allsloptimizer]") { - set_test_name("names shortened first where the target's code keeps them, and a state named for a string the script holds on Mono"); const std::string source = "integer counted;\n" "default\n{\n touch_start(integer n)\n {\n" " integer busy = n;\n busy = busy * busy + busy;\n busy = busy - busy / 3;\n" @@ -1983,7 +2525,8 @@ namespace tut o.target = target; o.shrinknames = true; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + allsloptimizer_data::notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + allsloptimizer_data::notes(r)); + REQUIRE(r.optimized); return r; }; const auto nameOf = [](const ALLSLOptimizer::Result& r, const std::string& name) { @@ -1993,26 +2536,35 @@ namespace tut // Mono keeps a global's name in its field and where it is used; a // local's nowhere. const ALLSLOptimizer::Result mono = run(ALLSLOptimizer::Target::Mono); - ensure("the state takes the string it can share: " + mono.text, nameOf(mono, "waiting") == "Q"); - ensure("the global before the busier local: " + mono.text, nameOf(mono, "counted").size() == 1 && nameOf(mono, "counted") < nameOf(mono, "busy")); + UNSCOPED_INFO("the state takes the string it can share: " + mono.text); + CHECK(nameOf(mono, "waiting") == "Q"); + { + INFO("the global before the busier local: " + mono.text); + REQUIRE(nameOf(mono, "counted").size() == 1); + CHECK((nameOf(mono, "counted") < nameOf(mono, "busy"))); + } // LSO keeps no names: the most used is named first, the text's // shortest. const ALLSLOptimizer::Result lso = run(ALLSLOptimizer::Target::LSO); - ensure("on LSO the busiest first: " + lso.text, nameOf(lso, "busy") == "a"); - ensure("and no string taken for a state: " + lso.text, nameOf(lso, "waiting") != "Q"); + UNSCOPED_INFO("on LSO the busiest first: " + lso.text); + CHECK(nameOf(lso, "busy") == "a"); + UNSCOPED_INFO("and no string taken for a state: " + lso.text); + CHECK(nameOf(lso, "waiting") != "Q"); const ALScriptWeight named = ALScriptWeigh::mono(mono.text); ALLSLOptimizer::Options plain = options(); plain.shrinknames = true; plain.target = ALLSLOptimizer::Target::LSO; const ALScriptWeight asLso = ALScriptWeigh::mono(ALLSLOptimizer::run(source, plain).text); - ensure("smaller on Mono than named for LSO", named.compiled && asLso.compiled && named.total < asLso.total); + { + INFO("smaller on Mono than named for LSO"); + CHECK(named.compiled); + CHECK(asLso.compiled); + CHECK(named.total < asLso.total); + } } - template<> template<> - void allsloptimizer_object::test<51>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a repeated pure call is held in a local where no larger and safe (L12)", "[allsloptimizer]") { - set_test_name("a pure call made again, given what nothing between changes, is kept in a local where the target has that no larger (L12); " - "not across a write or a label, nor begun where its statement may not come to it"); ALLSLOptimizer::Options lso = options(); lso.target = ALLSLOptimizer::Target::LSO; const std::string head = "default\n{\n touch_start(integer n)\n {\n string s = llDetectedName(0);\n"; @@ -2024,43 +2576,53 @@ namespace tut const std::string three = head + " llOwnerSay(llToUpper(s));\n llSay(0, llToUpper(s));\n llWhisper(0, llToUpper(s));\n" + tail; ALLSLOptimizer::Result r = ALLSLOptimizer::run(three, lso); - ensure("optimized: " + notes(r), r.optimized); - ensure_equals("kept", body(r), " string toUpper = llToUpper(s);\n llOwnerSay(toUpper);\n llSay(0, toUpper);\n llWhisper(0, toUpper);\n" + tail); - ensure("noted: " + notes(r), has(r, "kept llToUpper(s) in the local toUpper, which its 3 places read")); + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); + UNSCOPED_INFO("kept"); + CHECK(body(r) == " string toUpper = llToUpper(s);\n llOwnerSay(toUpper);\n llSay(0, toUpper);\n llWhisper(0, toUpper);\n" + tail); + UNSCOPED_INFO("noted: " + notes(r)); + CHECK(has(r, "kept llToUpper(s) in the local toUpper, which its 3 places read")); // On Mono a call given one variable is smaller than a local. r = ALLSLOptimizer::run(three, options()); - ensure("not on Mono: " + r.text, r.text.find("toUpper") == std::string::npos); + UNSCOPED_INFO("not on Mono: " + r.text); + REQUIRE(r.text.find("toUpper") == std::string::npos); // A place in a loop after one its statement always comes to. r = ALLSLOptimizer::run(head + " llSay(0, llToUpper(s));\n integer i;\n for (i = 0; i < n; ++i)\n llSay(i, llToUpper(s));\n" + tail, lso); - ensure("into the loop: " + r.text, r.text.find("string toUpper = llToUpper(s);\n llSay(0, toUpper);") != std::string::npos && - r.text.find("llSay(i, toUpper);") != std::string::npos); + { + INFO("into the loop: " + r.text); + REQUIRE(r.text.find("string toUpper = llToUpper(s);\n llSay(0, toUpper);") != std::string::npos); + REQUIRE(r.text.find("llSay(i, toUpper);") != std::string::npos); + } // The outermost call, and the call it is given with it. r = ALLSLOptimizer::run(head + " llSay(0, llGetSubString(llToUpper(s), 0, 2));\n llSay(1, llGetSubString(llToUpper(s), 0, 2));\n" + tail, lso); - ensure_equals("outermost", body(r), + UNSCOPED_INFO("outermost"); + CHECK(body(r) == " string getSubString = llGetSubString(llToUpper(s), 0, 2);\n llSay(0, getSubString);\n llSay(1, getSubString);\n" + tail); // A name the script has is not taken again. r = ALLSLOptimizer::run(head + " string toUpper = llDetectedName(1);\n llSay(0, toUpper);\n llSay(0, llToUpper(s));\n llSay(1, llToUpper(s));\n" + tail, lso); - ensure("numbered: " + r.text, r.text.find("string toUpper2 = llToUpper(s);") != std::string::npos); + UNSCOPED_INFO("numbered: " + r.text); + REQUIRE(r.text.find("string toUpper2 = llToUpper(s);") != std::string::npos); // What it is given written between: two places, each alone. const std::string written = head + " llSay(0, llToUpper(s));\n s = llDetectedName(1);\n llSay(0, llToUpper(s));\n llSay(0, s);\n" + tail; r = ALLSLOptimizer::run(written, lso); - ensure("not across a write: " + r.text, r.text.find("toUpper") == std::string::npos); + UNSCOPED_INFO("not across a write: " + r.text); + REQUIRE(r.text.find("toUpper") == std::string::npos); // A label between, which a jump could bring the code in by. r = ALLSLOptimizer::run(head + " llSay(0, llToUpper(s));\n @again;\n llSay(0, llToUpper(s));\n if (llFrand(1) < 0.5)\n jump again;\n" + tail, lso); - ensure("not across a label: " + r.text, r.text.find("toUpper") == std::string::npos); + UNSCOPED_INFO("not across a label: " + r.text); + REQUIRE(r.text.find("toUpper") == std::string::npos); // The first place in a branch: kept, it would run where it did not. r = ALLSLOptimizer::run(head + " if (n)\n llSay(0, llToUpper(s));\n llSay(0, llToUpper(s));\n" + tail, lso); - ensure("not begun in a branch: " + r.text, r.text.find("toUpper") == std::string::npos); + UNSCOPED_INFO("not begun in a branch: " + r.text); + REQUIRE(r.text.find("toUpper") == std::string::npos); } - template<> template<> - void allsloptimizer_object::test<52>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: dot and cross products of constant vectors fold as the VM computes them", "[allsloptimizer]") { - set_test_name("two vectors' dot and cross products fold to what the VM gives: each part by the same part, and a vector by itself crossed to nothing"); // Each may be set again before it is read, so that its fold shows // in its declaration. const std::string source = wrap("", " float d = <1, 3, -5> * <4, -2, -1>;\n vector c = <1.0, 2.0, 3.0> % <4.0, 5.0, 6.0>;\n" @@ -2072,66 +2634,81 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); - ensure("dot: " + r.text, about(numbers(r.text, "float d"), { 3.f })); - ensure("cross: " + r.text, about(numbers(r.text, "vector c"), { -3.f, 6.f, -3.f })); - ensure("crossed by itself: " + r.text, about(numbers(r.text, "vector z"), { 0.f, 0.f, 0.f })); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); + UNSCOPED_INFO("dot: " + r.text); + REQUIRE(about(numbers(r.text, "float d"), { 3.f })); + UNSCOPED_INFO("cross: " + r.text); + REQUIRE(about(numbers(r.text, "vector c"), { -3.f, 6.f, -3.f })); + UNSCOPED_INFO("crossed by itself: " + r.text); + REQUIRE(about(numbers(r.text, "vector z"), { 0.f, 0.f, 0.f })); } } - template<> template<> - void allsloptimizer_object::test<53>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a local's constant propagates to reads every path agrees on (L15)", "[allsloptimizer]") { - set_test_name("a local set to a constant is that constant where it is read before it is set again, on every way there (L15); " - "not where the ways disagree, in a loop that sets it, past a label, nor after a write in the same statement"); const std::string head = "default\n{\n touch_start(integer n)\n {\n"; const std::string tail = " }\n}\n"; ALLSLOptimizer::Result r = ALLSLOptimizer::run(head + " integer x = 5;\n llSay(0, (string)x);\n x = 7;\n llSay(0, (string)x);\n n = 3;\n llSay(n, \"n\");\n" + tail, options()); - ensure("optimized: " + notes(r), r.optimized); - ensure_equals("each read its value, and the variable gone", r.text, + UNSCOPED_INFO("optimized: " + notes(r)); + CHECK(r.optimized); + UNSCOPED_INFO("each read its value, and the variable gone"); + CHECK(r.text == head + " llSay(0, \"5\");\n llSay(0, \"7\");\n llSay(3, \"n\");\n" + tail); - ensure("noted: " + notes(r), has(r, "folded (string)x to \"7\"") && has(r, "inlined n to 3") && has(r, "removed x, which is set and never read")); + { + INFO("noted: " + notes(r)); + CHECK(has(r, "folded (string)x to \"7\"")); + CHECK(has(r, "inlined n to 3")); + CHECK(has(r, "removed x, which is set and never read")); + } // The ways of an if: what both say goes on, what they do not stops. r = ALLSLOptimizer::run(head + " integer x = 1;\n integer y = 1;\n if (n)\n {\n x = 2;\n llSay(0, \"a\");\n }\n" " else\n {\n llSay(0, \"b\");\n }\n llSay(x, (string)y);\n x = y = 0;\n" + tail, options()); - ensure("agreed: " + r.text, r.text.find("llSay(x, \"1\");") != std::string::npos); + UNSCOPED_INFO("agreed: " + r.text); + REQUIRE(r.text.find("llSay(x, \"1\");") != std::string::npos); // A way that returns says nothing of what comes after. r = ALLSLOptimizer::run(head + " integer x = 1;\n if (n)\n {\n x = 2;\n return;\n }\n llSay(x, \"c\");\n x = 0;\n" + tail, options()); - ensure("a way that ends: " + r.text, r.text.find("llSay(1, \"c\");") != std::string::npos); + UNSCOPED_INFO("a way that ends: " + r.text); + REQUIRE(r.text.find("llSay(1, \"c\");") != std::string::npos); // A loop that sets it: not inside, nor after. r = ALLSLOptimizer::run(head + " integer i = 0;\n while (i < n)\n {\n llSay(i, \"d\");\n ++i;\n }\n llSay(i, \"e\");\n" + tail, options()); - ensure("in a loop: " + r.text, r.text.find("llSay(i, \"d\");") != std::string::npos && r.text.find("llSay(i, \"e\");") != std::string::npos); + { + INFO("in a loop: " + r.text); + REQUIRE(r.text.find("llSay(i, \"d\");") != std::string::npos); + REQUIRE(r.text.find("llSay(i, \"e\");") != std::string::npos); + } // Past a label, which a jump may come to with anything. r = ALLSLOptimizer::run(head + " integer x = 1;\n @top;\n llSay(x, \"f\");\n x = 2;\n if (llFrand(1) < 0.5)\n jump top;\n" + tail, options()); - ensure("past a label: " + r.text, r.text.find("llSay(x, \"f\");") != std::string::npos); + UNSCOPED_INFO("past a label: " + r.text); + REQUIRE(r.text.find("llSay(x, \"f\");") != std::string::npos); // A loop holding a label forgets everything as it begins. r = ALLSLOptimizer::run(head + " integer x = 1;\n while (llFrand(1) < 0.5)\n {\n @in;\n llSay(x, \"g\");\n }\n" " x = 2;\n if (llFrand(1) < 0.5)\n jump in;\n" + tail, options()); - ensure("a loop with a label: " + r.text, r.text.find("llSay(x, \"g\");") != std::string::npos); + UNSCOPED_INFO("a loop with a label: " + r.text); + REQUIRE(r.text.find("llSay(x, \"g\");") != std::string::npos); // LSL runs a binary operator's right side first: the read on the // left comes after the write. r = ALLSLOptimizer::run(head + " integer x = 1;\n llSay(0, (string)(x + (x = 3)));\n llSay(x, \"h\");\n" + tail, options()); - ensure("after a write in the statement: " + r.text, r.text.find("(string)(x + (x = 3))") != std::string::npos); + UNSCOPED_INFO("after a write in the statement: " + r.text); + REQUIRE(r.text.find("(string)(x + (x = 3))") != std::string::npos); // A read left where another read of the variable is not known: a // literal in place of a read is no smaller, and the variable stays. r = ALLSLOptimizer::run(head + " string s = \"a long string of text\";\n llSay(0, s);\n if (n)\n s = llDetectedName(0);\n llSay(1, s);\n" + tail, options()); - ensure("not one read alone: " + r.text, r.text.find("llSay(0, s);") != std::string::npos); + UNSCOPED_INFO("not one read alone: " + r.text); + REQUIRE(r.text.find("llSay(0, s);") != std::string::npos); } - template<> template<> - void allsloptimizer_object::test<54>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: comparisons settle by the ranges of their sides (L16)", "[allsloptimizer]") { - set_test_name("a comparison settled by what each side can be (L16): a count never below nought, a find never below -1, llFrand of a positive " - "magnitude, a truth 1 or 0, a type within its constants; not where the ranges meet, nor where a side changes something"); const std::string source = "integer said() { llOwnerSay(\"said\"); return 1; }\n" "default\n{\n touch_start(integer n)\n {\n" " list l = llParseString2List(llDetectedName(0), [\" \"], []);\n" @@ -2151,24 +2728,40 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); const auto said = [&r](const char* what) { return r.text.find(std::string("llSay(0, \"") + what + "\")") != std::string::npos; }; - ensure("a count below nought never: " + r.text, !said("a")); - ensure("a count's local nought or more always: " + r.text, said("b") && r.text.find("count") == std::string::npos); - ensure("llFrand of ten never below nought: " + r.text, !said("c")); - ensure("a find above -2 always: " + r.text, said("d") && r.text.find("llListFindList") == std::string::npos); - ensure("a type past TYPE_ROTATION never: " + r.text, !said("e")); - ensure("a truth never 2: " + r.text, !said("f")); - ensure("where the ranges meet, kept: " + r.text, said("g") && said("h")); - ensure("where a side changes something, kept: " + r.text, said("i")); - ensure("noted: " + notes(r), has(r, "settled llGetListLength(l) < 0 to 0") || has(r, "settled (l != []) < 0 to 0") || has(r, "settled l != [] < 0 to 0")); - } - } - template<> template<> - void allsloptimizer_object::test<55>() - { - set_test_name("the folding arithmetic is IEEE's whatever the build's float flags: llVecNorm folds where dividing by the length and " - "multiplying by its reciprocal agree, as LLVector3 does, and not where they differ; a negative zero keeps its sign"); + UNSCOPED_INFO("a count below nought never: " + r.text); + REQUIRE_FALSE(said("a")); + { + INFO("a count's local nought or more always: " + r.text); + REQUIRE(said("b")); + REQUIRE(r.text.find("count") == std::string::npos); + } + UNSCOPED_INFO("llFrand of ten never below nought: " + r.text); + REQUIRE_FALSE(said("c")); + { + INFO("a find above -2 always: " + r.text); + REQUIRE(said("d")); + REQUIRE(r.text.find("llListFindList") == std::string::npos); + } + UNSCOPED_INFO("a type past TYPE_ROTATION never: " + r.text); + REQUIRE_FALSE(said("e")); + UNSCOPED_INFO("a truth never 2: " + r.text); + REQUIRE_FALSE(said("f")); + { + INFO("where the ranges meet, kept: " + r.text); + REQUIRE(said("g")); + REQUIRE(said("h")); + } + UNSCOPED_INFO("where a side changes something, kept: " + r.text); + REQUIRE(said("i")); + UNSCOPED_INFO("noted: " + notes(r)); + REQUIRE((has(r, "settled llGetListLength(l) < 0 to 0") || has(r, "settled (l != []) < 0 to 0") || has(r, "settled l != [] < 0 to 0"))); + } + } + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: folding is IEEE's: llVecNorm only where both ways agree and -0 keeps its sign", "[allsloptimizer]") + { // Each may be set again before it is read, so that its fold shows // in its declaration. const std::string source = wrap("", " vector a = llVecNorm(<0.75, 3, 1>);\n vector b = llVecNorm(<0, 3, 4>);\n" @@ -2180,25 +2773,34 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); // 0.75 / 3.25 is 0.230769232 as a single, 0.75 * (1 / 3.25) // 0.230769247: which the VM gives is not known, so it is left. // Built with the viewer's float flags, the compiler made the // division a product with the reciprocal, and this folded. - ensure("left where the two differ: " + r.text, r.text.find("vector a = llVecNorm(<0.75, ") != std::string::npos); + UNSCOPED_INFO("left where the two differ: " + r.text); + REQUIRE(r.text.find("vector a = llVecNorm(<0.75, ") != std::string::npos); const std::vector b = numbers(r.text, "vector b"); - ensure("folded where they agree: " + r.text, b.size() == 3 && b[0] == 0.0f && b[1] == 3.0f / 5.0f && b[2] == 4.0f / 5.0f); - ensure("a length both ways the same: " + r.text, numbers(r.text, "float m") == std::vector{ 5.0f }); + { + INFO("folded where they agree: " + r.text); + REQUIRE(b.size() == 3); + REQUIRE(b[0] == 0.0f); + REQUIRE(b[1] == 3.0f / 5.0f); + REQUIRE(b[2] == 4.0f / 5.0f); + } + UNSCOPED_INFO("a length both ways the same: " + r.text); + REQUIRE(numbers(r.text, "float m") == std::vector{ 5.0f }); const size_t at = r.text.find("list l = "); - ensure("a negative zero keeps its sign: " + r.text, - at != std::string::npos && r.text.substr(at, r.text.find('\n', at) - at).find("-0.0") != std::string::npos); + { + INFO("a negative zero keeps its sign: " + r.text); + REQUIRE(at != std::string::npos); + REQUIRE(r.text.substr(at, r.text.find('\n', at) - at).find("-0.0") != std::string::npos); + } } } - template<> template<> - void allsloptimizer_object::test<56>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: one-bit tests of one value merge into one mask test (L18)", "[allsloptimizer]") { - set_test_name("bit tests of one value merged (L18): one-bit tests all asked as !~(x | ~mask), all clear as !(x & mask), an | of them as " - "x & mask, an || of them so where only truth counts; not of two values, of a test of more than one bit, nor where x changes"); const std::string source = "integer said() { llOwnerSay(\"said\"); return 4; }\n" "default\n{\n touch_start(integer n)\n {\n" " integer m = llGetLinkNumber();\n" @@ -2217,26 +2819,42 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); const auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; // -13 as its cast on Mono (ALLSLCosts::castForNegative). - ensure("all set: " + r.text, has("if (!~(n | -13))") || has("if (!~(n | ((integer)-13)))")); + UNSCOPED_INFO("all set: " + r.text); + REQUIRE((has("if (!~(n | -13))") || has("if (!~(n | ((integer)-13)))"))); // v and w are read once, and their values go where they are read. - ensure("three, a value: " + r.text, has("llSay(!~(n | -14), ") || has("llSay(!~(n | ((integer)-14)), ")); - ensure("all clear: " + r.text, has("if (!(n & 12))")); - ensure("any, where only truth counts: " + r.text, has("if (n & 20)")); - ensure("an | of them: " + r.text, has("(string)(n & 15)")); - ensure("two values left: " + r.text, has("(n & 4)") && has("(m & 8)")); - ensure("more than one bit left: " + r.text, has("(n & 6)") && has("(n & 8)")); - ensure("what changes left: " + r.text, has("(said() & 4)") && has("(said() & 8)")); - ensure("noted: " + notes(r), allsloptimizer_data::has(r, "wrote (n & 4) && (n & 8) as !~(n | -13)")); + UNSCOPED_INFO("three, a value: " + r.text); + REQUIRE((has("llSay(!~(n | -14), ") || has("llSay(!~(n | ((integer)-14)), "))); + UNSCOPED_INFO("all clear: " + r.text); + REQUIRE(has("if (!(n & 12))")); + UNSCOPED_INFO("any, where only truth counts: " + r.text); + REQUIRE(has("if (n & 20)")); + UNSCOPED_INFO("an | of them: " + r.text); + REQUIRE(has("(string)(n & 15)")); + { + INFO("two values left: " + r.text); + REQUIRE(has("(n & 4)")); + REQUIRE(has("(m & 8)")); + } + { + INFO("more than one bit left: " + r.text); + REQUIRE(has("(n & 6)")); + REQUIRE(has("(n & 8)")); + } + { + INFO("what changes left: " + r.text); + REQUIRE(has("(said() & 4)")); + REQUIRE(has("(said() & 8)")); + } + UNSCOPED_INFO("noted: " + notes(r)); + REQUIRE(allsloptimizer_data::has(r, "wrote (n & 4) && (n & 8) as !~(n | -13)")); } } - template<> template<> - void allsloptimizer_object::test<57>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: parsing and CSV and sorting and rotations fold where both VMs agree (L17)", "[allsloptimizer]") { - set_test_name("more of the library folded where the two VMs cannot part (L17): parsing, CSV and sorting where no edge is in play; a " - "rotation between vectors, the angle between rotations and a rotation from axes where every way of working it out agrees"); // Each may be set again before it is read, so that its fold shows // in its declaration. const std::string source = wrap("", " list p = llParseString2List(\"a,b;;c\", [\",\"], [\";\"]);\n" @@ -2262,34 +2880,47 @@ namespace tut ALLSLOptimizer::Options o = options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + notes(r)); + REQUIRE(r.optimized); const auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; - ensure("split, spacers kept: " + r.text, has("list p = [\"a\", \"b\", \";\", \";\", \"c\"];")); - ensure("split, empties kept: " + r.text, has("list k = [\"a\", \"\", \"b\", \"\"];")); - ensure("a separator the start of another: left: " + r.text, has("list x = llParseString2List(")); - ensure("CSV: " + r.text, has("list v = [\"a\", \"b\", \"c\"];")); - ensure("a value starting with a space: left: " + r.text, has("list w = llCSV2List(")); - ensure("sorted by stride: " + r.text, has("list s = [1, \"a\", 2, \"b\", 3, \"c\"];")); - ensure("sorted down: " + r.text, has("list t = [\"pear\", \"fig\", \"apple\"];")); - ensure("two keys the same: left: " + r.text, has("list u = llListSort(")); - ensure("keys of two cases: left: " + r.text, has("list m = llListSort(")); + UNSCOPED_INFO("split, spacers kept: " + r.text); + REQUIRE(has("list p = [\"a\", \"b\", \";\", \";\", \"c\"];")); + UNSCOPED_INFO("split, empties kept: " + r.text); + REQUIRE(has("list k = [\"a\", \"\", \"b\", \"\"];")); + UNSCOPED_INFO("a separator the start of another: left: " + r.text); + REQUIRE(has("list x = llParseString2List(")); + UNSCOPED_INFO("CSV: " + r.text); + REQUIRE(has("list v = [\"a\", \"b\", \"c\"];")); + UNSCOPED_INFO("a value starting with a space: left: " + r.text); + REQUIRE(has("list w = llCSV2List(")); + UNSCOPED_INFO("sorted by stride: " + r.text); + REQUIRE(has("list s = [1, \"a\", 2, \"b\", 3, \"c\"];")); + UNSCOPED_INFO("sorted down: " + r.text); + REQUIRE(has("list t = [\"pear\", \"fig\", \"apple\"];")); + UNSCOPED_INFO("two keys the same: left: " + r.text); + REQUIRE(has("list u = llListSort(")); + UNSCOPED_INFO("keys of two cases: left: " + r.text); + REQUIRE(has("list m = llListSort(")); const float half = static_cast(std::sqrt(0.5)); - ensure("a quarter turn between: " + r.text, numbers(r.text, "rotation r") == std::vector{ 0.f, 0.f, half, half }); - ensure("parallel: left: " + r.text, has("rotation q = llRotBetween(")); - ensure("a quarter turn the other way: " + r.text, numbers(r.text, "rotation n") == std::vector{ 0.f, -half, 0.f, half }); - ensure("no angle between the same: " + r.text, numbers(r.text, "float h") == std::vector{ 0.f }); - ensure("the axes themselves: " + r.text, numbers(r.text, "rotation z") == std::vector{ 0.f, 0.f, 0.f, 1.f }); + UNSCOPED_INFO("a quarter turn between: " + r.text); + REQUIRE(numbers(r.text, "rotation r") == std::vector{ 0.f, 0.f, half, half }); + UNSCOPED_INFO("parallel: left: " + r.text); + REQUIRE(has("rotation q = llRotBetween(")); + UNSCOPED_INFO("a quarter turn the other way: " + r.text); + REQUIRE(numbers(r.text, "rotation n") == std::vector{ 0.f, -half, 0.f, half }); + UNSCOPED_INFO("no angle between the same: " + r.text); + REQUIRE(numbers(r.text, "float h") == std::vector{ 0.f }); + UNSCOPED_INFO("the axes themselves: " + r.text); + REQUIRE(numbers(r.text, "rotation z") == std::vector{ 0.f, 0.f, 0.f, 1.f }); // Made unit, as LLQuaternion does today, a quarter turn is a bit // larger in its last place than left as it was, as in 2007. - ensure("ways that part by a bit: left: " + r.text, has("rotation e = llAxes2Rot(")); + UNSCOPED_INFO("ways that part by a bit: left: " + r.text); + REQUIRE(has("rotation e = llAxes2Rot(")); } } - template<> template<> - void allsloptimizer_object::test<58>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: increments and left-out defaults and negative casts where each is smaller", "[allsloptimizer]") { - set_test_name("Mono's ++x for x = x + 1, a default left out, and a negative number as its cast; each where its target has it smaller, and what " - "must stay stays"); const std::string source = "integer gz = 0;\n" "integer gn = -5;\n" "default\n" @@ -2331,52 +2962,89 @@ namespace tut ALLSLOptimizer::Options o = allsloptimizer_data::options(); o.target = target; const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); - ensure("optimized: " + allsloptimizer_data::notes(r), r.optimized); + UNSCOPED_INFO("optimized: " + allsloptimizer_data::notes(r)); + REQUIRE(r.optimized); ALLSLService service; const ALScriptProblems said = service.check(r.text, target != ALLSLOptimizer::Target::LSO); for (const ALScriptProblem& p : said) { - ensure("no error in what was written: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + UNSCOPED_INFO("no error in what was written: " + p.message + "\n" + r.text); + REQUIRE(p.severity != ALScriptProblem::Severity::Error); } return r.text; }; const auto has = [](const std::string& text, const std::string& what) { return text.find(what) != std::string::npos; }; const std::string mono = run(ALLSLOptimizer::Target::Mono); - ensure("Mono: x = x + 1 as ++x: " + mono, has(mono, "++i;")); - ensure("Mono: x -= 1 as --x", has(mono, "--j;")); - ensure("Mono: as a value, 1 + x", has(mono, "j = ++i;")); - ensure("Mono: a float's stays", !has(mono, "++f") && !has(mono, "f;")); - ensure("Mono: a vector's part stays", !has(mono, "++v.x")); - ensure("Mono: an integer's default left out", has(mono, "integer z;")); - ensure("Mono: an integer global's too", has(mono, "integer gz;")); - ensure("Mono: a float's is not, there", !has(mono, "float y;")); - ensure("Mono: a negative number as its cast", has(mono, "llSay(((integer)-5), ")); - ensure("Mono: a vector's negative part", has(mono, "<((float)-1.5), 0, 0>")); - ensure("Mono: never in a global's value", has(mono, "integer gn = -5;")); + UNSCOPED_INFO("Mono: x = x + 1 as ++x: " + mono); + CHECK(has(mono, "++i;")); + UNSCOPED_INFO("Mono: x -= 1 as --x"); + CHECK(has(mono, "--j;")); + UNSCOPED_INFO("Mono: as a value, 1 + x"); + CHECK(has(mono, "j = ++i;")); + { + INFO("Mono: a float's stays"); + CHECK_FALSE(has(mono, "++f")); + CHECK_FALSE(has(mono, "f;")); + } + UNSCOPED_INFO("Mono: a vector's part stays"); + CHECK_FALSE(has(mono, "++v.x")); + UNSCOPED_INFO("Mono: an integer's default left out"); + CHECK(has(mono, "integer z;")); + UNSCOPED_INFO("Mono: an integer global's too"); + CHECK(has(mono, "integer gz;")); + UNSCOPED_INFO("Mono: a float's is not, there"); + CHECK_FALSE(has(mono, "float y;")); + UNSCOPED_INFO("Mono: a negative number as its cast"); + CHECK(has(mono, "llSay(((integer)-5), ")); + UNSCOPED_INFO("Mono: a vector's negative part"); + CHECK(has(mono, "<((float)-1.5), 0, 0>")); + UNSCOPED_INFO("Mono: never in a global's value"); + CHECK(has(mono, "integer gn = -5;")); const std::string lso = run(ALLSLOptimizer::Target::LSO); - ensure("LSO: x = x + 1 stays as LSO has it smaller: " + lso, !has(lso, "++i;") && !has(lso, "--j;")); - ensure("LSO: a float's default left out", has(lso, "float y;")); - ensure("LSO: -0.0 is not the default", !has(lso, "float n;")); - ensure("LSO: a key's default, \"\"", has(lso, "key k;")); - ensure("LSO: NULL_KEY is not a key's default", !has(lso, "key nk;")); - ensure("LSO: a string's never", !has(lso, "string s;")); - ensure("LSO: a vector's and a rotation's", has(lso, "vector w;") && has(lso, "rotation r;")); - ensure("LSO: an integer's is not, there", !has(lso, "integer z;")); - ensure("LSO: no casts of negative numbers", !has(lso, "((integer)-") && !has(lso, "((float)-")); + { + INFO("LSO: x = x + 1 stays as LSO has it smaller: " + lso); + CHECK_FALSE(has(lso, "++i;")); + CHECK_FALSE(has(lso, "--j;")); + } + UNSCOPED_INFO("LSO: a float's default left out"); + CHECK(has(lso, "float y;")); + UNSCOPED_INFO("LSO: -0.0 is not the default"); + CHECK_FALSE(has(lso, "float n;")); + UNSCOPED_INFO("LSO: a key's default, \"\""); + CHECK(has(lso, "key k;")); + UNSCOPED_INFO("LSO: NULL_KEY is not a key's default"); + CHECK_FALSE(has(lso, "key nk;")); + UNSCOPED_INFO("LSO: a string's never"); + CHECK_FALSE(has(lso, "string s;")); + { + INFO("LSO: a vector's and a rotation's"); + CHECK(has(lso, "vector w;")); + CHECK(has(lso, "rotation r;")); + } + UNSCOPED_INFO("LSO: an integer's is not, there"); + CHECK_FALSE(has(lso, "integer z;")); + { + INFO("LSO: no casts of negative numbers"); + CHECK_FALSE(has(lso, "((integer)-")); + CHECK_FALSE(has(lso, "((float)-")); + } const std::string luau = run(ALLSLOptimizer::Target::Luau); - ensure("Luau: a key's default left out: " + luau, has(luau, "key k;")); - ensure("Luau: nothing else", !has(luau, "integer z;") && !has(luau, "++i;") && !has(luau, "((integer)-")); + UNSCOPED_INFO("Luau: a key's default left out: " + luau); + CHECK(has(luau, "key k;")); + { + INFO("Luau: nothing else"); + CHECK_FALSE(has(luau, "integer z;")); + CHECK_FALSE(has(luau, "++i;")); + CHECK_FALSE(has(luau, "((integer)-")); + } } - template<> template<> - void allsloptimizer_object::test<59>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: operators take each target's smaller form with Luau's bit tests as arithmetic", "[allsloptimizer]") { - set_test_name("operators as each target has them smaller: && of truths as &, a - b as a + -b, -1 - x as ~x, a shift left as a product, a " - "doubling as a sum, a float halved as a product, x * -1 as -x; Luau's bitwise tests as arithmetic"); const std::string source = kept(wrap("integer g;\nfloat h;\n", " integer a = (integer)llFrand(9);\n integer b = (integer)llFrand(9);\n" " integer c = llStringLength(llGetObjectName());\n float f = llFrand(9);\n float e = llFrand(9);\n" @@ -2395,40 +3063,75 @@ namespace tut " g = c & 7;\n" " g = a >> 2;\n" " llOwnerSay((string)[a, b, c, f, e, g, h]);\n"), { "g", "h" }); - const std::string mono = optimized(source, ALLSLOptimizer::Target::Mono); - ensure("Mono: a - b as a + -b: " + mono, in(mono, "g = a + -b;")); - ensure("Mono: -= as += -", in(mono, "g += -b;")); - ensure("Mono: of floats", in(mono, "h = f + -e;")); - ensure("Mono: a shift as a product", in(mono, "g = a * 8;")); - ensure("Mono: -1 - x as ~x", in(mono, "g = ~b;")); - ensure("Mono: a doubling as a sum", in(mono, "g = a + a;")); - ensure("Mono: a float by a power of two as a product", in(mono, "h = f * 0.25;")); - ensure("Mono: x * -1 as -x", in(mono, "g = -b;")); - ensure("Mono: && of truths as &", in(mono, "if (a < b & b > 2)")); - ensure("Mono: the bitwise stay", in(mono, "if (a & 1)") && in(mono, "g = c >> 2;") && in(mono, "g = c & 7;")); - - const std::string lso = optimized(source, ALLSLOptimizer::Target::LSO); - ensure("LSO: a - b stays: " + lso, in(lso, "g = a - b;") && in(lso, "g -= b;") && in(lso, "h = f - e;")); - ensure("LSO: -1 - x as ~x", in(lso, "g = ~b;")); - ensure("LSO: x * -1 as -x", in(lso, "g = -b;")); - ensure("LSO: the rest stays", in(lso, "g = a << 3;") && in(lso, "g = a * 2;") && in(lso, "h = f / 4;") && in(lso, "if (a < b && b > 2)")); - - const std::string luau = optimized(source, ALLSLOptimizer::Target::Luau); - ensure("Luau: a shift as a product: " + luau, in(luau, "g = a * 8;")); - ensure("Luau: a doubling as a sum", in(luau, "g = a + a;")); - ensure("Luau: -1 - x stays, ~ a call there", in(luau, "g = -1 - b;")); - ensure("Luau: an odd test as a remainder", in(luau, "if (a % 2)")); - ensure("Luau: == as the ! of a difference", in(luau, "g = !(a - b);")); - ensure("Luau: a shift right and a mask of a count as a quotient and a remainder", in(luau, "g = c / 4;") && in(luau, "g = c % 8;")); - ensure("Luau: not of what may be below nought", in(luau, "g = a >> 2;")); - ensure("Luau: a - b stays", in(luau, "g = a - b;")); - } - - template<> template<> - void allsloptimizer_object::test<60>() - { - set_test_name("branches in fewer jumps: a truth returned or set, a select as arithmetic, a value set before an if, ways the same once, an " - "else after a return dropped, a jump over statements as an else, an empty if gone, nested ifs merged"); + const std::string mono = require_optimized(source, ALLSLOptimizer::Target::Mono); + UNSCOPED_INFO("Mono: a - b as a + -b: " + mono); + CHECK(in(mono, "g = a + -b;")); + UNSCOPED_INFO("Mono: -= as += -"); + CHECK(in(mono, "g += -b;")); + UNSCOPED_INFO("Mono: of floats"); + CHECK(in(mono, "h = f + -e;")); + UNSCOPED_INFO("Mono: a shift as a product"); + CHECK(in(mono, "g = a * 8;")); + UNSCOPED_INFO("Mono: -1 - x as ~x"); + CHECK(in(mono, "g = ~b;")); + UNSCOPED_INFO("Mono: a doubling as a sum"); + CHECK(in(mono, "g = a + a;")); + UNSCOPED_INFO("Mono: a float by a power of two as a product"); + CHECK(in(mono, "h = f * 0.25;")); + UNSCOPED_INFO("Mono: x * -1 as -x"); + CHECK(in(mono, "g = -b;")); + UNSCOPED_INFO("Mono: && of truths as &"); + CHECK(in(mono, "if (a < b & b > 2)")); + { + INFO("Mono: the bitwise stay"); + CHECK(in(mono, "if (a & 1)")); + CHECK(in(mono, "g = c >> 2;")); + CHECK(in(mono, "g = c & 7;")); + } + + const std::string lso = require_optimized(source, ALLSLOptimizer::Target::LSO); + { + INFO("LSO: a - b stays: " + lso); + CHECK(in(lso, "g = a - b;")); + CHECK(in(lso, "g -= b;")); + CHECK(in(lso, "h = f - e;")); + } + UNSCOPED_INFO("LSO: -1 - x as ~x"); + CHECK(in(lso, "g = ~b;")); + UNSCOPED_INFO("LSO: x * -1 as -x"); + CHECK(in(lso, "g = -b;")); + { + INFO("LSO: the rest stays"); + CHECK(in(lso, "g = a << 3;")); + CHECK(in(lso, "g = a * 2;")); + CHECK(in(lso, "h = f / 4;")); + CHECK(in(lso, "if (a < b && b > 2)")); + } + + const std::string luau = require_optimized(source, ALLSLOptimizer::Target::Luau); + UNSCOPED_INFO("Luau: a shift as a product: " + luau); + CHECK(in(luau, "g = a * 8;")); + UNSCOPED_INFO("Luau: a doubling as a sum"); + CHECK(in(luau, "g = a + a;")); + UNSCOPED_INFO("Luau: -1 - x stays, ~ a call there"); + CHECK(in(luau, "g = -1 - b;")); + UNSCOPED_INFO("Luau: an odd test as a remainder"); + CHECK(in(luau, "if (a % 2)")); + UNSCOPED_INFO("Luau: == as the ! of a difference"); + CHECK(in(luau, "g = !(a - b);")); + { + INFO("Luau: a shift right and a mask of a count as a quotient and a remainder"); + CHECK(in(luau, "g = c / 4;")); + CHECK(in(luau, "g = c % 8;")); + } + UNSCOPED_INFO("Luau: not of what may be below nought"); + CHECK(in(luau, "g = a >> 2;")); + UNSCOPED_INFO("Luau: a - b stays"); + CHECK(in(luau, "g = a - b;")); + } + + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: branches are written with fewer jumps on every target", "[allsloptimizer]") + { const std::string source = "integer g;\nstring t;\n" "integer big(integer a)\n{\n if (a > 3)\n return TRUE;\n return FALSE;\n}\n" "integer none(integer a)\n{\n if (a)\n return FALSE;\n else\n return TRUE;\n}\n" @@ -2454,32 +3157,59 @@ namespace tut " touch_start(integer n)\n {\n llOwnerSay((string)g + t);\n }\n}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) { - const std::string text = optimized(source, target); + const std::string text = require_optimized(source, target); const bool lso = target == ALLSLOptimizer::Target::LSO; const bool mono = target == ALLSLOptimizer::Target::Mono; - ensure("a truth returned: " + text, in(text, "return a > 3;")); - ensure("its ! the other way", in(text, "return !a;")); - ensure("a truth set", in(text, "g = a > 2;")); - ensure("a select of a truth as arithmetic, on every target: " + text, in(text, "g = (b > 2) * ") && in(text, " + 7;")); - ensure("of anything else, on LSO and Mono", lso || mono ? in(text, "g = !!b * ") : !in(text, "g = !!b")); - ensure("a local's value set before the if: " + text, in(text, "u = \"none\";\n if (a > 4)")); - ensure("not a global's, which a call in the check could read", in(text, "t = \"global\";\n") && !in(text, "t = \"global\";\n if")); - ensure("ways the same written once", !in(text, "else\n llOwnerSay(\"same\")") && in(text, "llOwnerSay(\"same\");")); - ensure("what both ways end with once, after", in(text, "llOwnerSay(\"end\");") && text.find("llOwnerSay(\"end\")") == text.rfind("llOwnerSay(\"end\")")); - ensure("an else after a return dropped", !in(text, "else\n {\n llOwnerSay(\"other\")")); - ensure("a jump over statements as an else", !in(text, "jump past") && !in(text, "@past")); - ensure("an empty if gone", !in(text, "a < 3")); - ensure("nested ifs of truths merged on LSO and Mono: " + text, lso || mono ? in(text, "if ((a > 1) & (b < 4))") : in(text, "if (a > 1)\n if (b < 4)")); - ensure("nested ifs merged with && on LSO", lso ? in(text, "if (a && b)") : !in(text, "if (a && b)")); - ensure("not where the inner may stop the script", in(text, "100 / b")); - } - } - - template<> template<> - void allsloptimizer_object::test<61>() - { - set_test_name("loops: a for or a while whose first check passes run as a do on LSO and Mono, the step in its check; a counter read nowhere else " - "counted down; not where the check is not known, nor the counter read"); + UNSCOPED_INFO("a truth returned: " + text); + REQUIRE(in(text, "return a > 3;")); + UNSCOPED_INFO("its ! the other way"); + REQUIRE(in(text, "return !a;")); + UNSCOPED_INFO("a truth set"); + REQUIRE(in(text, "g = a > 2;")); + { + INFO("a select of a truth as arithmetic, on every target: " + text); + REQUIRE(in(text, "g = (b > 2) * ")); + REQUIRE(in(text, " + 7;")); + } + UNSCOPED_INFO("of anything else, on LSO and Mono"); + REQUIRE((lso || mono ? in(text, "g = !!b * ") : !in(text, "g = !!b"))); + UNSCOPED_INFO("a local's value set before the if: " + text); + REQUIRE(in(text, "u = \"none\";\n if (a > 4)")); + { + INFO("not a global's, which a call in the check could read"); + REQUIRE(in(text, "t = \"global\";\n")); + REQUIRE_FALSE(in(text, "t = \"global\";\n if")); + } + { + INFO("ways the same written once"); + REQUIRE_FALSE(in(text, "else\n llOwnerSay(\"same\")")); + REQUIRE(in(text, "llOwnerSay(\"same\");")); + } + { + INFO("what both ways end with once, after"); + REQUIRE(in(text, "llOwnerSay(\"end\");")); + REQUIRE(text.find("llOwnerSay(\"end\")") == text.rfind("llOwnerSay(\"end\")")); + } + UNSCOPED_INFO("an else after a return dropped"); + REQUIRE_FALSE(in(text, "else\n {\n llOwnerSay(\"other\")")); + { + INFO("a jump over statements as an else"); + REQUIRE_FALSE(in(text, "jump past")); + REQUIRE_FALSE(in(text, "@past")); + } + UNSCOPED_INFO("an empty if gone"); + REQUIRE_FALSE(in(text, "a < 3")); + UNSCOPED_INFO("nested ifs of truths merged on LSO and Mono: " + text); + REQUIRE((lso || mono ? in(text, "if ((a > 1) & (b < 4))") : in(text, "if (a > 1)\n if (b < 4)"))); + UNSCOPED_INFO("nested ifs merged with && on LSO"); + REQUIRE((lso ? in(text, "if (a && b)") : !in(text, "if (a && b)"))); + UNSCOPED_INFO("not where the inner may stop the script"); + REQUIRE(in(text, "100 / b")); + } + } + + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: loops known to run become dos and unread counters count down", "[allsloptimizer]") + { const std::string source = "default\n{\n state_entry()\n {\n" " list l = llParseString2List(llGetObjectName(), [\" \"], []);\n" " integer i;\n integer j;\n integer k;\n integer m;\n integer n = (integer)llFrand(9);\n" @@ -2493,22 +3223,34 @@ namespace tut " }\n}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) { - const std::string text = optimized(source, target); + const std::string text = require_optimized(source, target); const bool luau = target == ALLSLOptimizer::Target::Luau; - ensure("a for known to run as a do, its step in the check: " + text, luau ? in(text, "for (i = 0; i < 10; ++i)") : in(text, "while(++i < 10);")); - ensure("a counter read nowhere else counted down", in(text, "j = 3;") && in(text, "while(--j);")); - ensure("to a count never below nought: " + text, (in(text, "integer k = llGetListLength(l);") || in(text, "integer k = (l != []);")) && in(text, "while (k--)")); - ensure("not where the first check is not known", in(text, "for (m = n; m < 10; ++m)")); - ensure("a while known to run as a do", luau ? in(text, "while (w < 5)") : in(text, "while(w < 5);")); - ensure("a loop known never to run gone, its first part kept: " + text, !in(text, "never") && in(text, "llOwnerSay(\"5\");")); + UNSCOPED_INFO("a for known to run as a do, its step in the check: " + text); + REQUIRE((luau ? in(text, "for (i = 0; i < 10; ++i)") : in(text, "while(++i < 10);"))); + { + INFO("a counter read nowhere else counted down"); + REQUIRE(in(text, "j = 3;")); + REQUIRE(in(text, "while(--j);")); + } + { + INFO("to a count never below nought: " + text); + REQUIRE((in(text, "integer k = llGetListLength(l);") || in(text, "integer k = (l != []);"))); + REQUIRE(in(text, "while (k--)")); + } + UNSCOPED_INFO("not where the first check is not known"); + REQUIRE(in(text, "for (m = n; m < 10; ++m)")); + UNSCOPED_INFO("a while known to run as a do"); + REQUIRE((luau ? in(text, "while (w < 5)") : in(text, "while(w < 5);"))); + { + INFO("a loop known never to run gone, its first part kept: " + text); + REQUIRE_FALSE(in(text, "never")); + REQUIRE(in(text, "llOwnerSay(\"5\");")); + } } } - template<> template<> - void allsloptimizer_object::test<62>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: a key compared with NULL_KEY is tested as itself unless it may be any string", "[allsloptimizer]") { - set_test_name("a key that is a key found or NULL_KEY tested as itself: k != NULL_KEY as k, k == NULL_KEY as k with its ways turned round; " - "not of a key that may be any string"); const std::string source = "default\n{\n" " touch_start(integer d)\n {\n" " key owner = llGetOwner();\n" @@ -2526,22 +3268,27 @@ namespace tut "}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) { - const std::string text = optimized(source, target); - ensure("a local set to the owner: " + text, in(text, "if (owner)")); - ensure("a detected key, its ways turned round", in(text, "if (llDetectedKey(0))\n llOwnerSay(\"somebody\");\n else\n llOwnerSay(\"nobody\");")); - ensure("a lone == turned round", in(text, "if (sitter)\n ;\n else\n llOwnerSay(\"empty\");")); - ensure("a listen's speaker: " + text, in(text, "if (id)\n llOwnerSay(m);")); - ensure("not a key cast from a string", in(text, "if (given != ")); - ensure("not a link message's key, which is any string", in(text, "if (id != ") && in(text, "llOwnerSay(str);")); + const std::string text = require_optimized(source, target); + UNSCOPED_INFO("a local set to the owner: " + text); + REQUIRE(in(text, "if (owner)")); + UNSCOPED_INFO("a detected key, its ways turned round"); + REQUIRE(in(text, "if (llDetectedKey(0))\n llOwnerSay(\"somebody\");\n else\n llOwnerSay(\"nobody\");")); + UNSCOPED_INFO("a lone == turned round"); + REQUIRE(in(text, "if (sitter)\n ;\n else\n llOwnerSay(\"empty\");")); + UNSCOPED_INFO("a listen's speaker: " + text); + REQUIRE(in(text, "if (id)\n llOwnerSay(m);")); + UNSCOPED_INFO("not a key cast from a string"); + REQUIRE(in(text, "if (given != ")); + { + INFO("not a link message's key, which is any string"); + REQUIRE(in(text, "if (id != ")); + REQUIRE(in(text, "llOwnerSay(str);")); + } } } - template<> template<> - void allsloptimizer_object::test<63>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: unread parameters and shared constant arguments and unread returns go", "[allsloptimizer]") { - set_test_name("functions given and giving what their calls need: a parameter never read taken away with what each call gives it, one every " - "call gives the same constant made a local, a value no call reads no longer returned; not where a call's argument does " - "something, the calls differ, the parameter is set, or a call reads the value"); const std::string source = "integer got;\n" "say(integer channel, string message, integer unused)\n{\n llSay(channel, message);\n}\n" "tell(integer quiet, string message)\n{\n llOwnerSay(message);\n}\n" @@ -2558,23 +3305,38 @@ namespace tut " touch_start(integer t)\n {\n llOwnerSay((string)got);\n }\n}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) { - const std::string text = optimized(source, target); - ensure("a parameter never read gone, and its arguments: " + text, in(text, "say(string message)") && in(text, "say(\"a\");")); - ensure("one every call gives the same constant made its value", in(text, "llSay(0, message);")); - ensure("not where a call's argument does something", in(text, "tell(integer quiet, string message)") && in(text, "llListen(5")); - ensure("a value no call reads no longer returned", in(text, "\nnoisy(integer n)") && !in(text, "integer noisy")); - ensure("what it returned still worked out where it does something, or gone", !in(text, "return n * 2") || in(text, "n * 2;")); - ensure("not where a call reads it", in(text, "integer read(integer n)")); - ensure("not a parameter that is set", in(text, "set(integer n)") && in(text, "set(7);")); + const std::string text = require_optimized(source, target); + { + INFO("a parameter never read gone, and its arguments: " + text); + REQUIRE(in(text, "say(string message)")); + REQUIRE(in(text, "say(\"a\");")); + } + UNSCOPED_INFO("one every call gives the same constant made its value"); + REQUIRE(in(text, "llSay(0, message);")); + { + INFO("not where a call's argument does something"); + REQUIRE(in(text, "tell(integer quiet, string message)")); + REQUIRE(in(text, "llListen(5")); + } + { + INFO("a value no call reads no longer returned"); + REQUIRE(in(text, "\nnoisy(integer n)")); + REQUIRE_FALSE(in(text, "integer noisy")); + } + UNSCOPED_INFO("what it returned still worked out where it does something, or gone"); + REQUIRE((!in(text, "return n * 2") || in(text, "n * 2;"))); + UNSCOPED_INFO("not where a call reads it"); + REQUIRE(in(text, "integer read(integer n)")); + { + INFO("not a parameter that is set"); + REQUIRE(in(text, "set(integer n)")); + REQUIRE(in(text, "set(7);")); + } } } - template<> template<> - void allsloptimizer_object::test<64>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: dead stores go and globals each handler sets first become its locals", "[allsloptimizer]") { - set_test_name("a value set again before it is read is not set; a global every handler sets before it reads made a local of each -- on " - "Mono, whose locals each cost a frame, only where that is less than the global; not one read before it is set, nor one a " - "function also sets; a || 1 and a && 0 settled"); const std::string source = "integer scratch;\n" "integer twice;\n" "integer kept = 3;\n" @@ -2594,26 +3356,35 @@ namespace tut " kept = t;\n }\n}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) { - const std::string text = optimized(source, target); + const std::string text = require_optimized(source, target); // A read of the clock changes nothing, and goes with it. - ensure("a value set again before it is read not set: " + text, in(text, "integer i = (integer)llFrand(9);")); - ensure("a for's first part sets again", !in(text, "integer j = 4;")); - ensure("a global one handler sets before it reads made a local", in(text, "integer scratch = llGetUnixTime();") && !in(text, "integer scratch;\n")); - ensure("one two handlers set before they read, a local of each but on Mono", - target == ALLSLOptimizer::Target::Mono ? in(text, "integer twice;\n") : in(text, "integer twice = llGetUnixTime();") && in(text, "llOwnerSay((string)t + ")); - ensure("not one read before it is set", in(text, "integer kept")); - ensure("not one a function sets too", in(text, "integer shared")); - ensure("a || 1 settled true", in(text, "llOwnerSay(\"always\")") && !in(text, "|| 1")); - ensure("a && 0 settled false", !in(text, "never")); + UNSCOPED_INFO("a value set again before it is read not set: " + text); + REQUIRE(in(text, "integer i = (integer)llFrand(9);")); + UNSCOPED_INFO("a for's first part sets again"); + REQUIRE_FALSE(in(text, "integer j = 4;")); + { + INFO("a global one handler sets before it reads made a local"); + REQUIRE(in(text, "integer scratch = llGetUnixTime();")); + REQUIRE_FALSE(in(text, "integer scratch;\n")); + } + UNSCOPED_INFO("one two handlers set before they read, a local of each but on Mono"); + REQUIRE((target == ALLSLOptimizer::Target::Mono ? in(text, "integer twice;\n") : in(text, "integer twice = llGetUnixTime();") && in(text, "llOwnerSay((string)t + "))); + UNSCOPED_INFO("not one read before it is set"); + REQUIRE(in(text, "integer kept")); + UNSCOPED_INFO("not one a function sets too"); + REQUIRE(in(text, "integer shared")); + { + INFO("a || 1 settled true"); + REQUIRE(in(text, "llOwnerSay(\"always\")")); + REQUIRE_FALSE(in(text, "|| 1")); + } + UNSCOPED_INFO("a && 0 settled false"); + REQUIRE_FALSE(in(text, "never")); } } - template<> template<> - void allsloptimizer_object::test<65>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: repeated global reads and repeated constants are held once where cheaper", "[allsloptimizer]") { - set_test_name("a global the script sets read again and again read once into a local, where nothing between sets it; a constant written at " - "many places kept once, in a global where the target holds one smaller so, else in a local; each on its target, Mono's " - "locals paying for their frames"); const std::string source = "integer channel;\n" "integer never = 7;\n" "default\n{\n" @@ -2627,29 +3398,41 @@ namespace tut " llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n" " llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n" " }\n}\n"; - const std::string lso = optimized(source, ALLSLOptimizer::Target::LSO); - ensure("LSO: a long string at three places kept in a global: " + lso, in(lso, "string s = \"a sentence long enough to be worth holding\";") && - in(lso, "llOwnerSay(s);")); - ensure("LSO: a vector at four places, in a global", in(lso, "vector v = <1.5, 2.5, 3.5>;")); - ensure("LSO: a global's reads stay, as large as a local's there", in(lso, "llSay(channel, \"one\");")); + const std::string lso = require_optimized(source, ALLSLOptimizer::Target::LSO); + { + INFO("LSO: a long string at three places kept in a global: " + lso); + CHECK(in(lso, "string s = \"a sentence long enough to be worth holding\";")); + CHECK(in(lso, "llOwnerSay(s);")); + } + UNSCOPED_INFO("LSO: a vector at four places, in a global"); + CHECK(in(lso, "vector v = <1.5, 2.5, 3.5>;")); + UNSCOPED_INFO("LSO: a global's reads stay, as large as a local's there"); + CHECK(in(lso, "llSay(channel, \"one\");")); - const std::string luau = optimized(source, ALLSLOptimizer::Target::Luau); - ensure("Luau: a global read three times read once: " + luau, in(luau, "integer localChannel = channel;") && in(luau, "llSay(localChannel, \"one\");")); - ensure("Luau: not across where it is set", in(luau, "channel = n;")); - ensure("Luau: never one the script never sets, which is the folder's", !in(luau, "localNever")); - ensure("Luau: constants held once in a function already", !in(luau, "string s = ")); + const std::string luau = require_optimized(source, ALLSLOptimizer::Target::Luau); + { + INFO("Luau: a global read three times read once: " + luau); + CHECK(in(luau, "integer localChannel = channel;")); + CHECK(in(luau, "llSay(localChannel, \"one\");")); + } + UNSCOPED_INFO("Luau: not across where it is set"); + CHECK(in(luau, "channel = n;")); + UNSCOPED_INFO("Luau: never one the script never sets, which is the folder's"); + CHECK_FALSE(in(luau, "localNever")); + UNSCOPED_INFO("Luau: constants held once in a function already"); + CHECK_FALSE(in(luau, "string s = ")); - const std::string mono = optimized(source, ALLSLOptimizer::Target::Mono); - ensure("Mono: a vector at four places kept in a local, its frame paid for: " + mono, in(mono, "vector v = <1.5, 2.5, 3.5>;")); - ensure("Mono: three reads of a global not worth a local's frame", !in(mono, "localChannel")); - ensure("Mono: a string held once already", !in(mono, "string s = ")); + const std::string mono = require_optimized(source, ALLSLOptimizer::Target::Mono); + UNSCOPED_INFO("Mono: a vector at four places kept in a local, its frame paid for: " + mono); + CHECK(in(mono, "vector v = <1.5, 2.5, 3.5>;")); + UNSCOPED_INFO("Mono: three reads of a global not worth a local's frame"); + CHECK_FALSE(in(mono, "localChannel")); + UNSCOPED_INFO("Mono: a string held once already"); + CHECK_FALSE(in(mono, "string s = ")); } - template<> template<> - void allsloptimizer_object::test<66>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: string literals join on LSO or when asked and endless loops become jumps", "[allsloptimizer]") { - set_test_name("two string literals joined on LSO, which writes each where it is used, and where the option says elsewhere; a loop that " - "never ends run by a jump back on LSO and Mono"); const std::string source = "default\n{\n touch_start(integer n)\n {\n" " llOwnerSay(\"Hello, \" + \"world\");\n" " while (TRUE)\n {\n llOwnerSay((string)llFrand(1));\n" @@ -2657,24 +3440,28 @@ namespace tut " @out;\n llOwnerSay(\"done\");\n }\n}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) { - const std::string text = optimized(source, target); + const std::string text = require_optimized(source, target); const bool lso = target == ALLSLOptimizer::Target::LSO; const bool luau = target == ALLSLOptimizer::Target::Luau; - ensure("two literals joined on LSO alone, by default: " + text, lso ? in(text, "\"Hello, world\"") : in(text, "\"Hello, \" + \"world\"")); - ensure("a loop that never ends by a jump back, not on Luau", luau ? in(text, "while (TRUE)") : in(text, "@loop;") && in(text, "jump loop;")); + UNSCOPED_INFO("two literals joined on LSO alone, by default: " + text); + REQUIRE((lso ? in(text, "\"Hello, world\"") : in(text, "\"Hello, \" + \"world\""))); + UNSCOPED_INFO("a loop that never ends by a jump back, not on Luau"); + REQUIRE((luau ? in(text, "while (TRUE)") : in(text, "@loop;") && in(text, "jump loop;"))); // Its jump out and back made one jump back where the check fails. - ensure("what follows it still reached: " + text, in(text, "llOwnerSay(\"done\");") && (luau || in(text, "jump loop;\n llOwnerSay(\"done\");"))); + { + INFO("what follows it still reached: " + text); + REQUIRE(in(text, "llOwnerSay(\"done\");")); + REQUIRE((luau || in(text, "jump loop;\n llOwnerSay(\"done\");"))); + } } ALLSLOptimizer::Options o = options(); o.addstrings = true; - ensure("joined on Mono where the option asks", in(ALLSLOptimizer::run(source, o).text, "\"Hello, world\"")); + UNSCOPED_INFO("joined on Mono where the option asks"); + CHECK(in(ALLSLOptimizer::run(source, o).text, "\"Hello, world\"")); } - template<> template<> - void allsloptimizer_object::test<67>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: string to integer casts fold as 32-bit hosts do and big JSON numbers are left", "[allsloptimizer]") { - set_test_name("a string cast to an integer folds as the grid's 32-bit hosts cast one, past 0xFFFFFFFF -1; a JSON number past 32 bits " - "is left to the simulator"); const std::string source = "default\n{\n touch_start(integer n)\n {\n" " llOwnerSay((string)((integer)\"4294967296\"));\n" " llOwnerSay((string)((integer)\"-4294967296\"));\n" @@ -2688,27 +3475,34 @@ namespace tut " }\n}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) { - const std::string text = optimized(source, target); + const std::string text = require_optimized(source, target); size_t minus_ones = 0; for (size_t at = text.find("llOwnerSay(\"-1\");"); at != std::string::npos; at = text.find("llOwnerSay(\"-1\");", at + 1)) { ++minus_ones; } - ensure("past 0xFFFFFFFF, -1, its minus or not: " + text, minus_ones == 4); - ensure("within it, wrapped below nought: " + text, in(text, "llOwnerSay(\"-1294967296\");")); - ensure("spaces and what follows the number, hexadecimal only from a leading 0x: " + text, - in(text, "llOwnerSay(\"12\");") && in(text, "llOwnerSay(\"16\");") && in(text, "llOwnerSay(\"0\");")); - ensure("a JSON number past 32 bits left: " + text, in(text, "llJson2List(\"[9999999999]\")")); - ensure("one within them folded: " + text, in(text, "2147483647") && !in(text, "llJson2List(\"[2147483647")); + UNSCOPED_INFO("past 0xFFFFFFFF, -1, its minus or not: " + text); + REQUIRE(minus_ones == 4); + UNSCOPED_INFO("within it, wrapped below nought: " + text); + REQUIRE(in(text, "llOwnerSay(\"-1294967296\");")); + { + INFO("spaces and what follows the number, hexadecimal only from a leading 0x: " + text); + REQUIRE(in(text, "llOwnerSay(\"12\");")); + REQUIRE(in(text, "llOwnerSay(\"16\");")); + REQUIRE(in(text, "llOwnerSay(\"0\");")); + } + UNSCOPED_INFO("a JSON number past 32 bits left: " + text); + REQUIRE(in(text, "llJson2List(\"[9999999999]\")")); + { + INFO("one within them folded: " + text); + REQUIRE(in(text, "2147483647")); + REQUIRE_FALSE(in(text, "llJson2List(\"[2147483647")); + } } } - template<> template<> - void allsloptimizer_object::test<68>() + TEST_CASE_METHOD(allsloptimizer_data, "allsloptimizer: float library functions fold where double and single agree and -0 becomes 0", "[allsloptimizer]") { - set_test_name("a float function of the library folds where in double rounded once and in single precision it agrees, to a single on " - "every target, as each VM answers; a pow or an atan2 of -0 is 0; on Luau, sin of no single and a length no single " - "is are left"); const std::string source = "default\n{\n touch_start(integer n)\n {\n" " llOwnerSay((string)llPow(2.0, 10.0));\n" " llOwnerSay((string)llSqrt(2.0));\n" @@ -2721,17 +3515,36 @@ namespace tut " }\n}\n"; for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono }) { - const std::string text = optimized(source, target); - ensure("each folded as the VM's single: " + text, - in(text, "llOwnerSay(\"1024.000000\");") && in(text, "llOwnerSay(\"1.414214\");") && in(text, "llOwnerSay(\"0.841471\");") && - in(text, "llOwnerSay(\"0.295520\");") && in(text, "llOwnerSay(\"5.000000\");") && !in(text, "llSin") && !in(text, "llVecMag")); - ensure("-0 made 0, not -0: " + text, !in(text, "-0.000000")); + const std::string text = require_optimized(source, target); + { + INFO("each folded as the VM's single: " + text); + REQUIRE(in(text, "llOwnerSay(\"1024.000000\");")); + REQUIRE(in(text, "llOwnerSay(\"1.414214\");")); + REQUIRE(in(text, "llOwnerSay(\"0.841471\");")); + REQUIRE(in(text, "llOwnerSay(\"0.295520\");")); + REQUIRE(in(text, "llOwnerSay(\"5.000000\");")); + REQUIRE_FALSE(in(text, "llSin")); + REQUIRE_FALSE(in(text, "llVecMag")); + } + UNSCOPED_INFO("-0 made 0, not -0: " + text); + REQUIRE_FALSE(in(text, "-0.000000")); } - const std::string luau = optimized(source, ALLSLOptimizer::Target::Luau); - ensure("a single, not the double: " + luau, in(luau, "(string)0.8414709568023682") && in(luau, "(string)1.4142135381698608")); - ensure("exact, folded: " + luau, in(luau, "(string)1024.0") && in(luau, "(string)5.0")); - ensure("-0 made 0: " + luau, !in(luau, "-0.0")); - ensure("sin of what is no single, which SLua takes as it is, left: " + luau, in(luau, "llSin(0.30000000447034836)")); - ensure("a length no single is, which the simulator gives by a way not known, left: " + luau, in(luau, "llVecMag(")); - } -} // namespace tut + const std::string luau = require_optimized(source, ALLSLOptimizer::Target::Luau); + { + INFO("a single, not the double: " + luau); + CHECK(in(luau, "(string)0.8414709568023682")); + CHECK(in(luau, "(string)1.4142135381698608")); + } + { + INFO("exact, folded: " + luau); + CHECK(in(luau, "(string)1024.0")); + CHECK(in(luau, "(string)5.0")); + } + UNSCOPED_INFO("-0 made 0: " + luau); + CHECK_FALSE(in(luau, "-0.0")); + UNSCOPED_INFO("sin of what is no single, which SLua takes as it is, left: " + luau); + CHECK(in(luau, "llSin(0.30000000447034836)")); + UNSCOPED_INFO("a length no single is, which the simulator gives by a way not known, left: " + luau); + CHECK(in(luau, "llVecMag(")); + } +} // anonymous namespace diff --git a/indra/alscript/tests/allslservice_test.cpp b/indra/alscript/tests/allslservice_test.cpp index bfff29bfd8..667f12bdf2 100644 --- a/indra/alscript/tests/allslservice_test.cpp +++ b/indra/alscript/tests/allslservice_test.cpp @@ -27,7 +27,9 @@ #include "../lsl/allslservice.h" #include "../lint/alscriptfixes.h" -#include "../test/lltut.h" +#include "alteststateguard.h" + +#include #include "llsdserialize.h" #include "lluuid.h" @@ -35,7 +37,7 @@ #include #include -namespace tut +namespace { struct allslservice_data { @@ -81,23 +83,30 @@ namespace tut } }; - typedef test_group allslservice_group; - typedef allslservice_group::object allslservice_object; - allslservice_group allslservice_instance("allslservice"); + TEST_CASE_METHOD(allslservice_data, "allslservice: the grid's builtins load", "[allslservice]") + { + UNSCOPED_INFO("builtins load: " + error); + CHECK(loaded); + CHECK(service.hasBuiltins()); + } - template<> template<> - void allslservice_object::test<1>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a second service loading the same builtins has them too", "[allslservice]") { - set_test_name("the grid's builtins load"); - ensure("builtins load: " + error, loaded); - ensure("service says so", service.hasBuiltins()); + // Every name it declares is in Tailslide's table already, so none + // is loaded again; the service has them all the same. + REQUIRE(loaded); + ALLSLService again; + std::string why; + const bool read = again.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", why); + CAPTURE(why); + CHECK(read); + CHECK(again.hasBuiltins()); } - template<> template<> - void allslservice_object::test<2>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a script that is right checks clean", "[allslservice]") { - set_test_name("a script that is right checks clean"); - ensure("builtins load: " + error, loaded); + UNSCOPED_INFO("builtins load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check( "default\n" "{\n" @@ -106,14 +115,14 @@ namespace tut " llSay(0, \"Hello, \" + (string)llGetOwner());\n" " }\n" "}\n"); - ensure_equals("nothing said: " + said(problems), problems.size(), 0); + UNSCOPED_INFO("nothing said: " + said(problems)); + REQUIRE(problems.size() == 0); } - template<> template<> - void allslservice_object::test<3>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a wrong argument type is an error on its line", "[allslservice]") { - set_test_name("a wrong argument type is an error on its line"); - ensure("builtins load: " + error, loaded); + UNSCOPED_INFO("builtins load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check( "default\n" "{\n" @@ -124,25 +133,30 @@ namespace tut "}\n"); // E_ARGUMENT_WRONG_TYPE const ALScriptProblem* problem = coded(problems, "10011"); - ensure("the wrong-type error: " + said(problems), problem != nullptr); - ensure("an error", problem->severity == ALScriptProblem::Severity::Error); - ensure("from the checks", problem->source == ALScriptProblem::Source::Types); - ensure_equals("on the call's line, zero-based", problem->line, 4); + UNSCOPED_INFO("the wrong-type error: " + said(problems)); + REQUIRE(problem != nullptr); + CHECK(problem->severity == ALScriptProblem::Severity::Error); + CHECK(problem->source == ALScriptProblem::Source::Types); + UNSCOPED_INFO("on the call's line, zero-based"); + CHECK(problem->line == 4); // Taken apart by its code, for another language to say: the // words in the order the marks have them. - ensure_equals("the key", problem->key, std::string("LSLArgumentWrongType")); - ensure_equals("five words", problem->args.size(), size_t(5)); - ensure_equals("the type passed", problem->args[0], std::string("string")); - ensure_equals("the argument's number", problem->args[1], std::string("1")); - ensure_equals("the function", problem->args[2], std::string("llSay")); - ensure_equals("put back, the same words", ALScriptProblem::fill("Passing [1] as argument [2] of `[3]' which is declared as `[4] [5]'.", problem->args), problem->message); + CHECK(problem->key == std::string("LSLArgumentWrongType")); + REQUIRE(problem->args.size() == size_t(5)); + UNSCOPED_INFO("the type passed"); + CHECK(problem->args[0] == std::string("string")); + UNSCOPED_INFO("the argument's number"); + CHECK(problem->args[1] == std::string("1")); + UNSCOPED_INFO("the function"); + CHECK(problem->args[2] == std::string("llSay")); + UNSCOPED_INFO("put back, the same words"); + CHECK(ALScriptProblem::fill("Passing [1] as argument [2] of `[3]' which is declared as `[4] [5]'.", problem->args) == problem->message); } - template<> template<> - void allslservice_object::test<4>() + TEST_CASE_METHOD(allslservice_data, "allslservice: an unused global is a warning", "[allslservice]") { - set_test_name("an unused global is a warning"); - ensure("builtins load: " + error, loaded); + UNSCOPED_INFO("builtins load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check( "integer unused = 1;\n" "default\n" @@ -153,18 +167,19 @@ namespace tut "}\n"); // W_DECLARED_BUT_NOT_USED const ALScriptProblem* problem = coded(problems, "20009"); - ensure("the unused warning: " + said(problems), problem != nullptr); - ensure("a warning", problem->severity == ALScriptProblem::Severity::Warning); - ensure("from the lint", problem->source == ALScriptProblem::Source::Lint); - ensure_equals("on the declaration's line", problem->line, 0); - ensure_equals("no errors", errors(problems), 0); + UNSCOPED_INFO("the unused warning: " + said(problems)); + REQUIRE(problem != nullptr); + CHECK(problem->severity == ALScriptProblem::Severity::Warning); + CHECK(problem->source == ALScriptProblem::Source::Lint); + UNSCOPED_INFO("on the declaration's line"); + CHECK(problem->line == 0); + CHECK(errors(problems) == 0); } - template<> template<> - void allslservice_object::test<5>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a syntax error is the parser's", "[allslservice]") { - set_test_name("a syntax error is the parser's"); - ensure("builtins load: " + error, loaded); + UNSCOPED_INFO("builtins load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check( "default\n" "{\n" @@ -172,27 +187,25 @@ namespace tut " {\n" " }\n" "}\n"); - ensure("an error: " + said(problems), errors(problems) > 0); - ensure("from the parser", problems.front().source == ALScriptProblem::Source::Parser); - ensure("on a line past the first", problems.front().line > 0); + UNSCOPED_INFO("an error: " + said(problems)); + REQUIRE(errors(problems) > 0); + CHECK(problems.front().source == ALScriptProblem::Source::Parser); + CHECK(problems.front().line > 0); } - template<> template<> - void allslservice_object::test<6>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a builtins file that cannot be opened is refused and is not fatal", "[allslservice]") { - set_test_name("a builtins file that cannot be opened is refused, not fatal"); ALLSLService bare; std::string why; - ensure("refused", !bare.loadBuiltins("/nonexistent/builtins.txt", why)); - ensure("with a reason", !why.empty()); - ensure("nothing loaded", !bare.hasBuiltins()); + REQUIRE_FALSE(bare.loadBuiltins("/nonexistent/builtins.txt", why)); + REQUIRE_FALSE(why.empty()); + CHECK_FALSE(bare.hasBuiltins()); } - template<> template<> - void allslservice_object::test<7>() + TEST_CASE_METHOD(allslservice_data, "allslservice: symbols in scope are the script's own and only locals declared before", "[allslservice]") { - set_test_name("the script's symbols in scope: globals, functions, states, and locals declared before the position"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "integer count = 0;\n" "float half(integer n) { return n / 2.0; }\n" @@ -218,26 +231,25 @@ namespace tut } return false; }; - ensure("the global", has("count", ALScriptSymbolKind::Variable, "integer count")); - ensure("the function", has("half", ALScriptSymbolKind::Function, "float half(integer n)")); - ensure("the state", has("other", ALScriptSymbolKind::State, "state other")); - ensure("the event's parameter", has("total", ALScriptSymbolKind::Parameter, "integer total")); - ensure("the local before", has("before", ALScriptSymbolKind::Variable, "string before")); + CHECK(has("count", ALScriptSymbolKind::Variable, "integer count")); + CHECK(has("half", ALScriptSymbolKind::Function, "float half(integer n)")); + CHECK(has("other", ALScriptSymbolKind::State, "state other")); + CHECK(has("total", ALScriptSymbolKind::Parameter, "integer total")); + CHECK(has("before", ALScriptSymbolKind::Variable, "string before")); bool after = false, builtin = false; for (const ALScriptCompletion& c : found) { after = after || c.text == "after"; builtin = builtin || c.text == "llSay"; } - ensure("not the local after", !after); - ensure("not the builtins", !builtin); + CHECK_FALSE(after); + CHECK_FALSE(builtin); } - template<> template<> - void allslservice_object::test<8>() + TEST_CASE_METHOD(allslservice_data, "allslservice: hover shows a declaration and where it is; signature says which argument", "[allslservice]") { - set_test_name("hover reads a symbol's declaration and points at it; a signature says which argument"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "float half(integer n) { return n / 2.0; }\n" "default\n" @@ -248,30 +260,35 @@ namespace tut " }\n" "}\n"; ALScriptHover hover = service.hover(script, 5, 26); // on `half` - ensure("found", hover.found); - ensure_equals("as declared", hover.label, std::string("float half(integer n)")); - ensure("declared on the first line", hover.hasDefinition && hover.definitionLine == 0); + REQUIRE(hover.found); + CHECK(hover.label == std::string("float half(integer n)")); + CHECK(hover.hasDefinition); + CHECK(hover.definitionLine == 0); hover = service.hover(script, 5, 10); // on `llSay` - ensure("a builtin is found", hover.found); - ensure("as declared: " + hover.label, hover.label.find("llSay(") != std::string::npos); - ensure("but not in the script", !hover.hasDefinition); + REQUIRE(hover.found); + UNSCOPED_INFO("as declared: " + hover.label); + REQUIRE(hover.label.find("llSay(") != std::string::npos); + UNSCOPED_INFO("but not in the script"); + CHECK_FALSE(hover.hasDefinition); ALScriptSignature sig = service.signature(script, 5, 15); // in llSay's first argument - ensure("in the call", sig.found); - ensure("the builtin's signature: " + sig.label, sig.label.find("llSay(") == 0 || sig.label.find(" llSay(") != std::string::npos); - ensure_equals("two parameters", sig.parameters.size(), size_t(2)); - ensure_equals("the first", sig.active, 0); + REQUIRE(sig.found); + UNSCOPED_INFO("the builtin's signature: " + sig.label); + REQUIRE((sig.label.find("llSay(") == 0 || sig.label.find(" llSay(") != std::string::npos)); + REQUIRE(sig.parameters.size() == size_t(2)); + CHECK(sig.active == 0); sig = service.signature(script, 5, 30); // just inside half( - ensure("the inner call", sig.found && sig.label == "float half(integer n)"); - ensure_equals("its only parameter", sig.active, 0); - ensure("none at the top", !service.signature(script, 0, 0).found); + REQUIRE(sig.found); + CHECK(sig.label == "float half(integer n)"); + CHECK(sig.active == 0); + UNSCOPED_INFO("none at the top"); + REQUIRE_FALSE(service.signature(script, 0, 0).found); } - template<> template<> - void allslservice_object::test<9>() + TEST_CASE_METHOD(allslservice_data, "allslservice: references find a declaration and its uses; only the script's own rename", "[allslservice]") { - set_test_name("references find a symbol's declaration and every use; builtins, events and default are not the script's to rename"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "integer count = 0;\n" "default\n" @@ -288,40 +305,60 @@ namespace tut " state_entry() { state default; }\n" "}\n"; ALScriptReferences refs = service.references(script, 5, 16); // on the second `count` - ensure("found", refs.found && refs.name == "count"); - ensure("a variable", refs.kind == ALScriptSymbolKind::Variable); - ensure(llformat("declared on the first line, not %d:%d-%d (%d)", refs.definition.line, refs.definition.column, refs.definition.endColumn, (int)refs.hasDefinition), - refs.hasDefinition && refs.definition.line == 0 && refs.definition.column == 8 && refs.definition.endColumn == 13); - ensure_equals("four places", refs.references.size(), size_t(4)); - ensure("the declaration first", refs.references[0] == refs.definition); - ensure("the cast's operand last", refs.references[3].line == 6 && refs.references[3].column == 25); - ensure("renamable", refs.renamable); + REQUIRE(refs.found); + CHECK(refs.name == "count"); + CHECK(refs.kind == ALScriptSymbolKind::Variable); + CHECK(refs.hasDefinition); + CHECK(refs.definition.line == 0); + CHECK(refs.definition.column == 8); + CHECK(refs.definition.endColumn == 13); + REQUIRE(refs.references.size() == size_t(4)); + CHECK(refs.references[0] == refs.definition); + { + INFO("the cast's operand last"); + CHECK(refs.references[3].line == 6); + CHECK(refs.references[3].column == 25); + } + CHECK(refs.renamable); refs = service.references(script, 6, 10); // on llSay - ensure("a builtin is found", refs.found && refs.name == "llSay"); - ensure("used once, declared nowhere here", refs.references.size() == 1 && !refs.hasDefinition && !refs.renamable); + REQUIRE(refs.found); + CHECK(refs.name == "llSay"); + { + INFO("used once, declared nowhere here"); + REQUIRE(refs.references.size() == 1); + CHECK_FALSE(refs.hasDefinition); + CHECK_FALSE(refs.renamable); + } refs = service.references(script, 7, 15); // on `other` in `state other;` - ensure("a state is found", refs.found && refs.kind == ALScriptSymbolKind::State); - ensure("declared where it is", refs.hasDefinition && refs.definition.line == 10 && refs.definition.column == 6); - ensure_equals("the declaration and the change", refs.references.size(), size_t(2)); - ensure("renamable", refs.renamable); + REQUIRE(refs.found); + CHECK(refs.kind == ALScriptSymbolKind::State); + CHECK(refs.hasDefinition); + CHECK(refs.definition.line == 10); + CHECK(refs.definition.column == 6); + UNSCOPED_INFO("the declaration and the change"); + REQUIRE(refs.references.size() == size_t(2)); + CHECK(refs.renamable); refs = service.references(script, 12, 26); // on `default` in `state default;` - ensure("default is found", refs.found && refs.name == "default"); - ensure("but is not the script's to rename", !refs.renamable); + REQUIRE(refs.found); + CHECK(refs.name == "default"); + UNSCOPED_INFO("but is not the script's to rename"); + CHECK_FALSE(refs.renamable); refs = service.references(script, 3, 6); // on state_entry - ensure("an event is found", refs.found && refs.kind == ALScriptSymbolKind::Event); - ensure(llformat("in both states, not %d places", (int)refs.references.size()), refs.references.size() == 2); - ensure("not renamable", !refs.renamable); + REQUIRE(refs.found); + CHECK(refs.kind == ALScriptSymbolKind::Event); + UNSCOPED_INFO("in both states"); + REQUIRE(refs.references.size() == 2); + CHECK_FALSE(refs.renamable); } - template<> template<> - void allslservice_object::test<10>() + TEST_CASE_METHOD(allslservice_data, "allslservice: the outline lists globals and functions and states; events one deeper", "[allslservice]") { - set_test_name("the outline lists the globals, the functions and the states with their events one deeper"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "integer count = 0;\n" "float half(integer n) { return n / 2.0; }\n" @@ -336,22 +373,38 @@ namespace tut std::vector outline = service.outline(script); std::string names; for (const ALScriptOutlineEntry& e : outline) names += " " + e.name + llformat("@%d", e.depth); - ensure_equals("six entries:" + names, outline.size(), size_t(6)); - ensure("the global", outline[0].name == "count" && outline[0].kind == ALScriptSymbolKind::Variable && outline[0].detail == "integer count" && outline[0].depth == 0); - ensure("the function, as declared", outline[1].name == "half" && outline[1].kind == ALScriptSymbolKind::Function && outline[1].detail == "float half(integer n)"); - ensure("its name where it is", outline[1].nameSpan.line == 1 && outline[1].nameSpan.column == 6 && outline[1].nameSpan.endColumn == 10); - ensure("default, a state", outline[2].name == "default" && outline[2].kind == ALScriptSymbolKind::State && outline[2].depth == 0); - ensure("spanning its lines", outline[2].span.line == 2 && outline[2].span.endLine == 8); - ensure("state_entry, one deeper", outline[3].name == "state_entry" && outline[3].kind == ALScriptSymbolKind::Event && outline[3].depth == 1); - ensure("touch_start with its parameter", outline[4].name == "touch_start" && outline[4].detail == "touch_start(integer total)" && outline[4].span.line == 5 && outline[4].span.endLine == 7); - ensure("the other state", outline[5].name == "other" && outline[5].kind == ALScriptSymbolKind::State); + UNSCOPED_INFO("six entries:" + names); + REQUIRE(outline.size() == size_t(6)); + CHECK(outline[0].name == "count"); + CHECK(outline[0].kind == ALScriptSymbolKind::Variable); + CHECK(outline[0].detail == "integer count"); + CHECK(outline[0].depth == 0); + CHECK(outline[1].name == "half"); + CHECK(outline[1].kind == ALScriptSymbolKind::Function); + CHECK(outline[1].detail == "float half(integer n)"); + CHECK(outline[1].nameSpan.line == 1); + CHECK(outline[1].nameSpan.column == 6); + CHECK(outline[1].nameSpan.endColumn == 10); + CHECK(outline[2].name == "default"); + CHECK(outline[2].kind == ALScriptSymbolKind::State); + CHECK(outline[2].depth == 0); + CHECK(outline[2].span.line == 2); + CHECK(outline[2].span.endLine == 8); + CHECK(outline[3].name == "state_entry"); + CHECK(outline[3].kind == ALScriptSymbolKind::Event); + CHECK(outline[3].depth == 1); + CHECK(outline[4].name == "touch_start"); + CHECK(outline[4].detail == "touch_start(integer total)"); + CHECK(outline[4].span.line == 5); + CHECK(outline[4].span.endLine == 7); + CHECK(outline[5].name == "other"); + CHECK(outline[5].kind == ALScriptSymbolKind::State); } - template<> template<> - void allslservice_object::test<11>() + TEST_CASE_METHOD(allslservice_data, "allslservice: semantic tokens tell every name by what it is", "[allslservice]") { - set_test_name("every name is told by what it is: global, parameter, local, function, builtin, state, event, label"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "integer count = 0;\n" "float half(integer n) { return n / 2.0; }\n" @@ -378,38 +431,93 @@ namespace tut std::string listed; for (const ALScriptSemanticToken& t : tokens) listed += llformat(" %d:%d/%d+%d", t.span.line, t.span.column, (int)t.kind, (int)t.modifiers); const ALScriptSemanticToken* count = at(0, 8); - ensure("count, a global declared:" + listed, count && count->kind == ALScriptSymbolKind::Variable && (count->modifiers & ALScriptSemanticToken::Global) && (count->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("count, a global declared:" + listed); + REQUIRE(count); + REQUIRE(count->kind == ALScriptSymbolKind::Variable); + REQUIRE(count->modifiers & ALScriptSemanticToken::Global); + CHECK(count->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* half = at(1, 6); - ensure("half, a function declared:" + listed, half && half->kind == ALScriptSymbolKind::Function && (half->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("half, a function declared:" + listed); + REQUIRE(half); + REQUIRE(half->kind == ALScriptSymbolKind::Function); + CHECK(half->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* n = at(1, 19); - ensure("n, a parameter declared:" + listed, n && n->kind == ALScriptSymbolKind::Parameter && (n->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("n, a parameter declared:" + listed); + REQUIRE(n); + REQUIRE(n->kind == ALScriptSymbolKind::Parameter); + CHECK(n->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* touch = at(4, 4); - ensure("touch_start, an event:" + listed, touch && touch->kind == ALScriptSymbolKind::Event); + { + INFO("touch_start, an event:" + listed); + REQUIRE(touch); + CHECK(touch->kind == ALScriptSymbolKind::Event); + } const ALScriptSemanticToken* h = at(6, 14); - ensure("h, a local declared:" + listed, h && h->kind == ALScriptSymbolKind::Variable && !(h->modifiers & ALScriptSemanticToken::Global) && (h->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("h, a local declared:" + listed); + REQUIRE(h); + REQUIRE(h->kind == ALScriptSymbolKind::Variable); + REQUIRE_FALSE(h->modifiers & ALScriptSemanticToken::Global); + CHECK(h->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* total = at(6, 23); - ensure("total used, a parameter:" + listed, total && total->kind == ALScriptSymbolKind::Parameter && !(total->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("total used, a parameter:" + listed); + REQUIRE(total); + REQUIRE(total->kind == ALScriptSymbolKind::Parameter); + CHECK_FALSE(total->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* say = at(7, 8); - ensure("llSay, a builtin function:" + listed, say && say->kind == ALScriptSymbolKind::Function && (say->modifiers & ALScriptSemanticToken::Builtin)); + { + INFO("llSay, a builtin function:" + listed); + REQUIRE(say); + REQUIRE(say->kind == ALScriptSymbolKind::Function); + CHECK(say->modifiers & ALScriptSemanticToken::Builtin); + } const ALScriptSemanticToken* channel = at(7, 14); - ensure("PUBLIC_CHANNEL, a builtin constant:" + listed, channel && channel->kind == ALScriptSymbolKind::Constant && (channel->modifiers & ALScriptSemanticToken::ReadOnly)); + { + INFO("PUBLIC_CHANNEL, a builtin constant:" + listed); + REQUIRE(channel); + REQUIRE(channel->kind == ALScriptSymbolKind::Constant); + CHECK(channel->modifiers & ALScriptSemanticToken::ReadOnly); + } const ALScriptSemanticToken* label = at(8, 9); - ensure("again, a label declared:" + listed, label && label->kind == ALScriptSymbolKind::Label && (label->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("again, a label declared:" + listed); + REQUIRE(label); + REQUIRE(label->kind == ALScriptSymbolKind::Label); + CHECK(label->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* jump = at(9, 24); - ensure("jumped to, not declared:" + listed, jump && jump->kind == ALScriptSymbolKind::Label && !(jump->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("jumped to, not declared:" + listed); + REQUIRE(jump); + REQUIRE(jump->kind == ALScriptSymbolKind::Label); + CHECK_FALSE(jump->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* other = at(10, 14); - ensure("other, a state:" + listed, other && other->kind == ALScriptSymbolKind::State); + { + INFO("other, a state:" + listed); + REQUIRE(other); + CHECK(other->kind == ALScriptSymbolKind::State); + } for (size_t i = 1; i < tokens.size(); ++i) { - ensure("in order, each once", tokens[i - 1].span < tokens[i].span); + UNSCOPED_INFO("in order, each once"); + REQUIRE(tokens[i - 1].span < tokens[i].span); } } - template<> template<> - void allslservice_object::test<12>() + TEST_CASE_METHOD(allslservice_data, "allslservice: inlay hints name each argument's parameter in builtin and own calls", "[allslservice]") { - set_test_name("inlay hints name each argument's parameter, builtin or the script's own"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "float half(integer n) { return n / 2.0; }\n" "default\n" @@ -431,18 +539,22 @@ namespace tut } return false; }; - ensure("the channel's name before the 0:" + listed, has(6, 14, "channel:")); - ensure("the message's before the cast:" + listed, has(6, 17, "msg:")); - ensure("nothing before n, which is the name:" + listed, !has(6, 30, "n:")); - ensure("n: before the 2:" + listed, has(7, 29, "n:")); - ensure("nothing when not asked", service.inlayHints(script, false).empty()); + UNSCOPED_INFO("the channel's name before the 0:" + listed); + CHECK(has(6, 14, "channel:")); + UNSCOPED_INFO("the message's before the cast:" + listed); + CHECK(has(6, 17, "msg:")); + UNSCOPED_INFO("nothing before n, which is the name:" + listed); + CHECK_FALSE(has(6, 30, "n:")); + UNSCOPED_INFO("n: before the 2:" + listed); + CHECK(has(7, 29, "n:")); + UNSCOPED_INFO("nothing when not asked"); + REQUIRE(service.inlayHints(script, false).empty()); } - template<> template<> - void allslservice_object::test<13>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a script mid-edit is answered about the rest of it", "[allslservice]") { - set_test_name("a script mid-edit is answered about the rest of it: the scope, the call being typed, what every name is"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string head = "integer count = 0;\n" "float half(integer n) { return n / 2.0; }\n" @@ -464,63 +576,81 @@ namespace tut // A call not yet closed, the caret after an argument half typed. std::string script = head + " llSay(0, be" + tail; ALScriptSignature sig = service.signature(script, 7, 19); - ensure("the call is found", sig.found && sig.label.find("llSay(") != std::string::npos); - ensure_equals("at its second parameter", sig.active, 1); - ensure("the text does not parse", !service.parsed()); - ensure("but was understood", service.understood()); + REQUIRE(sig.found); + REQUIRE(sig.label.find("llSay(") != std::string::npos); + CHECK(sig.active == 1); + CHECK_FALSE(service.parsed()); + CHECK(service.understood()); const std::string in_scope = names(script, 7, 17); - ensure("the local before, in scope:" + in_scope, in_scope.find(" before ") != std::string::npos); - ensure("the parameter:" + in_scope, in_scope.find(" total ") != std::string::npos); - ensure("the global:" + in_scope, in_scope.find(" count ") != std::string::npos); + UNSCOPED_INFO("the local before, in scope:" + in_scope); + REQUIRE(in_scope.find(" before ") != std::string::npos); + UNSCOPED_INFO("the parameter:" + in_scope); + REQUIRE(in_scope.find(" total ") != std::string::npos); + UNSCOPED_INFO("the global:" + in_scope); + REQUIRE(in_scope.find(" count ") != std::string::npos); // The bracket just typed, and a comma just typed. sig = service.signature(head + " llSay(" + tail, 7, 14); - ensure("just opened", sig.found); - ensure_equals("at the first parameter", sig.active, 0); + REQUIRE(sig.found); + CHECK(sig.active == 0); sig = service.signature(head + " llSay(0," + tail, 7, 16); - ensure("after the comma", sig.found); - ensure_equals("at the second parameter", sig.active, 1); + REQUIRE(sig.found); + CHECK(sig.active == 1); sig = service.signature(head + " llSay(0, \"hel" + tail, 7, 20); - ensure("inside a string not yet closed", sig.found && sig.active == 1); + { + INFO("inside a string not yet closed"); + REQUIRE(sig.found); + CHECK(sig.active == 1); + } sig = service.signature(head + " half(" + tail, 7, 13); - ensure("the script's own function", sig.found && sig.label == "float half(integer n)"); + REQUIRE(sig.found); + CHECK(sig.label == "float half(integer n)"); // A name alone, and a statement without its semicolon. - ensure("a word alone", names(head + " bef" + tail, 7, 8).find(" before ") != std::string::npos); - ensure("no semicolon", names(head + " llSay(0, before)" + tail, 7, 17).find(" before ") != std::string::npos); + REQUIRE(names(head + " bef" + tail, 7, 8).find(" before ") != std::string::npos); + REQUIRE(names(head + " llSay(0, before)" + tail, 7, 17).find(" before ") != std::string::npos); // What the check's other questions answer of a text that does not // parse: the rest of it, rather than nothing. script = head + " llSay(0, be" + tail; - ensure("the outline", service.outline(script).size() == 4); + REQUIRE(service.outline(script).size() == 4); const std::vector tokens = service.semanticTokens(script); bool before_named = false; for (const ALScriptSemanticToken& token : tokens) { before_named = before_named || (token.span.line == 6 && token.kind == ALScriptSymbolKind::Variable); } - ensure("the names before the break", before_named); - ensure("the hints", !service.inlayHints(head + " llSay(0, \"x\");\n llOwnerSay(" + tail, true).empty()); + UNSCOPED_INFO("the names before the break"); + CHECK(before_named); + REQUIRE_FALSE(service.inlayHints(head + " llSay(0, \"x\");\n llOwnerSay(" + tail, true).empty()); // A function being declared at the top, before the states. script = "integer count = 0;\nfoo(\ndefault\n{\n state_entry()\n {\n llSay(0, (string)count);\n }\n}\n"; const ALScriptHover hover = service.hover(script, 6, 26); - ensure("a name below a broken declaration", hover.found && hover.label == "integer count"); - ensure("its references", service.references(script, 6, 26).references.size() == 2); + { + INFO("a name below a broken declaration"); + REQUIRE(hover.found); + CHECK(hover.label == "integer count"); + } + REQUIRE(service.references(script, 6, 26).references.size() == 2); // A block left open at the end. script = head + " llSay(0, before);\n"; - ensure("closed at the end", names(script, 7, 17).find(" before ") != std::string::npos); + UNSCOPED_INFO("closed at the end"); + REQUIRE(names(script, 7, 17).find(" before ") != std::string::npos); // Nothing to mend from. - ensure("nonsense is not understood", service.outline("}}} ((( ;;; \"").empty() && !service.understood()); + { + INFO("nonsense is not understood"); + REQUIRE(service.outline("}}} ((( ;;; \"").empty()); + CHECK_FALSE(service.understood()); + } } - template<> template<> - void allslservice_object::test<14>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a syntax error says what is missing and where or what was unexpected", "[allslservice]") { - set_test_name("a syntax error says what is missing where it is missing, or what was unexpected, keyed"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string head = "default\n{\n state_entry()\n {\n"; auto first_error = [&](const std::string& script) { for (const ALScriptProblem& problem : service.check(script)) @@ -534,44 +664,67 @@ namespace tut }; // A call not closed, the block closed on the next line. ALScriptProblem p = first_error(head + " llSay(0, \"hi\"\n }\n}\n"); - ensure_equals("missing the bracket: " + p.message, p.message, std::string("Missing ')'.")); - ensure("keyed", p.key == "LSLSyntaxMissing" && p.args.size() == 1 && p.args[0] == "')'"); - ensure("on the string's closing quote, not the next line's brace", p.line == 4 && p.column == 20 && p.endColumn == 21); + CHECK(p.message == std::string("Missing ')'.")); + CHECK(p.key == "LSLSyntaxMissing"); + REQUIRE(p.args.size() == 1); + CHECK(p.args[0] == "')'"); + { + INFO("on the string's closing quote, not the next line's brace"); + CHECK(p.line == 4); + CHECK(p.column == 20); + CHECK(p.endColumn == 21); + } // A statement not ended. p = first_error(head + " llSay(0, \"hi\")\n llOwnerSay(\"x\");\n }\n}\n"); - ensure_equals("missing the semicolon: " + p.message, p.message, std::string("Missing ';'.")); - ensure("after the call", p.line == 4 && p.column == 21); + CHECK(p.message == std::string("Missing ';'.")); + { + INFO("after the call"); + CHECK(p.line == 4); + CHECK(p.column == 21); + } p = first_error(head + " integer x = 5 // five\n }\n}\n"); - ensure_equals("past a comment: " + p.message, p.message, std::string("Missing ';'.")); - ensure("on the number", p.line == 4 && p.column == 20); + UNSCOPED_INFO("past a comment"); + CHECK(p.message == std::string("Missing ';'.")); + { + INFO("on the number"); + CHECK(p.line == 4); + CHECK(p.column == 20); + } // Something that is no statement's next word. p = first_error("integer x = 1;\nfoo(\ndefault\n{\n state_entry() { }\n}\n"); - ensure("unexpected, by what it is: " + p.message, p.key == "LSLSyntaxUnexpected" || p.key == "LSLSyntaxUnexpectedWanted"); - ensure("said as written: " + p.message, p.message.find("'default'") != std::string::npos); - ensure("no bison left in it: " + p.message, p.message.find("syntax error") == std::string::npos && p.message.find("STATE_DEFAULT") == std::string::npos); + UNSCOPED_INFO("unexpected, by what it is: " + p.message); + CHECK((p.key == "LSLSyntaxUnexpected" || p.key == "LSLSyntaxUnexpectedWanted")); + UNSCOPED_INFO("said as written: " + p.message); + REQUIRE(p.message.find("'default'") != std::string::npos); + { + INFO("no bison left in it: " + p.message); + REQUIRE(p.message.find("syntax error") == std::string::npos); + REQUIRE(p.message.find("STATE_DEFAULT") == std::string::npos); + } } - template<> template<> - void allslservice_object::test<15>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a text checked again says the same after other questions or targets", "[allslservice]") { - set_test_name("the same text checked again says what it says once, whatever was asked between, for either target"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "integer unused;\nlist g = [1, 2];\ninteger h = g;\ndefault\n{\n state_entry()\n {\n integer never;\n }\n}\n"; const ALScriptProblems first = service.check(script, true); - ensure("something to say: " + said(first), !first.empty()); + REQUIRE_FALSE(first.empty()); service.outline(script); service.hover(script, 0, 9); const ALScriptProblems again = service.check(script, true); - ensure_equals("said once, the second time too", said(again), said(first)); + UNSCOPED_INFO("said once, the second time too"); + CHECK(said(again) == said(first)); const ALScriptProblems lso = service.check(script, false); const ALScriptProblems back = service.check(script, true); - ensure_equals("and after the other target, nothing of it left", said(back), said(first)); - ensure_equals("the other target, asked twice, the same", said(service.check(script, false)), said(lso)); + UNSCOPED_INFO("and after the other target, nothing of it left"); + CHECK(said(back) == said(first)); + UNSCOPED_INFO("the other target, asked twice, the same"); + CHECK(said(service.check(script, false)) == said(lso)); } - template<> template<> - void allslservice_object::test<16>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a parameter without its type is named; the unexpected is said plainly", "[allslservice]") { - set_test_name("a parameter written without its type says so by name, a function's or an event's, and what was not expected is said plainly"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); auto first_error = [&](const std::string& script) { for (const ALScriptProblem& problem : service.check(script)) { @@ -585,23 +738,44 @@ namespace tut const std::string tail = "default\n{\n state_entry() { }\n}\n"; // As Nexii's linkset library has it. ALScriptProblem p = first_error("list ObjectLinksetSittingAvatars(object) {\n return [];\n}\n" + tail); - ensure_equals("the first: " + p.message, p.message, + CHECK(p.message == std::string("The parameter 'object' needs its type before it: integer, float, string, key, vector, rotation or list.")); - ensure("keyed", p.key == "LSLParameterUntyped" && p.args.size() == 1 && p.args[0] == "object"); - ensure("on the name, whole", p.line == 0 && p.column == 33 && p.endLine == 0 && p.endColumn == 39); + CHECK(p.key == "LSLParameterUntyped"); + REQUIRE(p.args.size() == 1); + CHECK(p.args[0] == "object"); + { + INFO("on the name, whole"); + CHECK(p.line == 0); + CHECK(p.column == 33); + CHECK(p.endLine == 0); + CHECK(p.endColumn == 39); + } p = first_error("f(integer a, b) { }\n" + tail); - ensure("after a comma: " + p.message, p.key == "LSLParameterUntyped" && p.args[0] == "b" && p.column == 13); + { + INFO("after a comma: " + p.message); + REQUIRE(p.key == "LSLParameterUntyped"); + CHECK(p.args[0] == "b"); + CHECK(p.column == 13); + } p = first_error("default\n{\n touch_start(total)\n {\n }\n}\n"); - ensure("an event's: " + p.message, p.key == "LSLParameterUntyped" && p.args[0] == "total" && p.line == 2); + { + INFO("an event's: " + p.message); + REQUIRE(p.key == "LSLParameterUntyped"); + CHECK(p.args[0] == "total"); + CHECK(p.line == 2); + } // A name where none can go, not after a bracket. p = first_error("default\n{\n state_entry()\n {\n llSay(0, \"x\") y;\n }\n}\n"); - ensure("not a parameter: " + p.message, p.key != "LSLParameterUntyped"); - ensure("said plainly: " + p.message, p.message.find("Unexpected a") == std::string::npos && p.message.find("syntax error") == std::string::npos); + UNSCOPED_INFO("not a parameter: " + p.message); + CHECK(p.key != "LSLParameterUntyped"); + { + INFO("said plainly: " + p.message); + REQUIRE(p.message.find("Unexpected a") == std::string::npos); + REQUIRE(p.message.find("syntax error") == std::string::npos); + } } - template<> template<> - void allslservice_object::test<17>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a definitions file with lines the engine cannot read loads the rest", "[allslservice]") { - set_test_name("a definitions file with lines the engine cannot read loads the rest, and the process goes on"); // What a grid newer than this viewer may send: a type the engine does // not know, a constant it cannot read, blank lines of CR LF and of // blanks -- each of which ended the process before. @@ -620,28 +794,33 @@ namespace tut } std::string why; const bool read = service.loadBuiltins(path, why); + // Tailslide's table is the process's and only ever added to: what + // the file declares stays for every case after this one. + ll_test::StateGuard::accept("Tailslide's builtins"); std::error_code gone; std::filesystem::remove(fsyspath(path), gone); - ensure("loaded: " + why, read); + UNSCOPED_INFO("loaded: " + why); + CHECK(read); const std::string head = "default\n{\n state_entry()\n {\n"; const std::string tail = " }\n}\n"; const ALScriptProblems kept = service.check(head + " llOwnerSay((string)(llAlchemyProbe(1) + llAlchemyAfter(\"x\")));\n" + tail, true); - ensure_equals("the lines it could read are known: " + said(kept), errors(kept), size_t(0)); + UNSCOPED_INFO("the lines it could read are known: " + said(kept)); + CHECK(errors(kept) == size_t(0)); const ALScriptProblems left = service.check(head + " llAlchemyBadReturn();\n" + tail, true); - ensure("a line it could not read is not: " + said(left), errors(left) > 0); + UNSCOPED_INFO("a line it could not read is not: " + said(left)); + CHECK(errors(left) > 0); } - template<> template<> - void allslservice_object::test<18>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a definitions file under a folder named outside the ANSI code page loads", "[allslservice]") { - set_test_name("a definitions file under a folder named outside the ANSI code page loads"); // The grid's definitions are kept in the cache, under the user's // profile, whose name may be in any script: the path is UTF-8, and // on Windows the engine opened it as the ANSI code page read it. const fsyspath dir = std::filesystem::temp_directory_path() / fsyspath("al_builtins_\xc3\xa9\xe6\x97\xa5_" + LLUUID::generateNewID().asString()); std::error_code made; std::filesystem::create_directories(dir, made); - ensure("folder made: " + made.message(), !made); + UNSCOPED_INFO("folder made: " + made.message()); + REQUIRE_FALSE(made); const std::string path = fsyspath(dir / "builtins.txt").string(); { llofstream out(path, std::ios::binary); @@ -649,17 +828,20 @@ namespace tut } std::string why; const bool read = service.loadBuiltins(path, why); + // Tailslide's table is the process's and only ever added to: what + // the file declares stays for every case after this one. + ll_test::StateGuard::accept("Tailslide's builtins"); std::error_code gone; std::filesystem::remove_all(dir, gone); - ensure("loaded: " + why, read); + UNSCOPED_INFO("loaded: " + why); + CHECK(read); const ALScriptProblems kept = service.check("default\n{\n state_entry()\n {\n llOwnerSay((string)llAlchemyWide(1));\n }\n}\n", true); - ensure_equals("what it defines is known: " + said(kept), errors(kept), size_t(0)); + UNSCOPED_INFO("what it defines is known: " + said(kept)); + CHECK(errors(kept) == size_t(0)); } - template<> template<> - void allslservice_object::test<19>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a broken text is mended once whole and once at the caret and no more", "[allslservice]") { - set_test_name("a text that does not parse is mended once whole and once at the caret, however the questions take turns"); const std::string broken = "integer count;\ndefault {\n state_entry() {\n llSay(0, (string)count\n }\n}\n"; const size_t before = service.mendings(); // What the check asks after it, whole; a call's parameters, at the @@ -667,7 +849,8 @@ namespace tut service.outline(broken); service.signature(broken, 3, 20); const size_t twice = service.mendings(); - ensure_equals("one each", twice - before, size_t(2)); + UNSCOPED_INFO("one each"); + CHECK(twice - before == size_t(2)); for (int turn = 0; turn < 3; ++turn) { service.outline(broken); @@ -675,15 +858,15 @@ namespace tut service.hover(broken, 3, 20); service.symbols(broken, 3, 20); } - ensure_equals("and no more, turn and turn about", service.mendings(), twice); + UNSCOPED_INFO("and no more, turn and turn about"); + CHECK(service.mendings() == twice); service.signature(broken, 3, 12); - ensure_equals("another place is mended again", service.mendings(), twice + 1); + UNSCOPED_INFO("another place is mended again"); + CHECK(service.mendings() == twice + 1); } - template<> template<> - void allslservice_object::test<20>() + TEST_CASE_METHOD(allslservice_data, "allslservice: lines passed over get no semantic tokens and their problems no fixes", "[allslservice]") { - set_test_name("lines nobody reads -- an include's -- are passed over by the names and the hints, and their problems offered no fixes"); // An include's function, unused, then the script's own. const std::string text = "helper(integer count)\n{\n llOwnerSay((string)count);\n}\n" "default\n{\n state_entry()\n {\n integer unused = 1;\n llOwnerSay(\"x\");\n }\n}\n"; @@ -702,38 +885,43 @@ namespace tut own_fixed = !problem.fixes.empty(); } } - ensure("the include's unused function said", helper_said); - ensure("but offered nothing", !helper_fixed); - ensure("the script's own offered its fix", own_fixed); + UNSCOPED_INFO("the include's unused function said"); + CHECK(helper_said); + UNSCOPED_INFO("but offered nothing"); + CHECK_FALSE(helper_fixed); + UNSCOPED_INFO("the script's own offered its fix"); + CHECK(own_fixed); bool in_include = false; for (const ALScriptSemanticToken& token : service.semanticTokens(text)) { in_include |= token.span.line <= 3; } - ensure("no names coloured in the include", !in_include); + UNSCOPED_INFO("no names coloured in the include"); + CHECK_FALSE(in_include); service.setPassedOver({}); bool coloured = false; for (const ALScriptSemanticToken& token : service.semanticTokens(text)) { coloured |= token.span.line <= 3; } - ensure("with nothing passed over, coloured", coloured); + UNSCOPED_INFO("with nothing passed over, coloured"); + CHECK(coloured); } - template<> template<> - void allslservice_object::test<21>() + TEST_CASE_METHOD(allslservice_data, "allslservice: another region's builtins: new ones known after and none declared twice", "[allslservice]") { - set_test_name("another region's builtins: what it adds is known after, what both have stays as first loaded, and nothing is said twice"); const std::string base = std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt"; std::string error; - ensure("the first region's: " + error, service.loadBuiltins(base, error)); + UNSCOPED_INFO("the first region's: " + error); + REQUIRE(service.loadBuiltins(base, error)); const std::string script = "default\n{\n state_entry()\n {\n integer n = llBrandNewThing(1);\n llOwnerSay((string)n);\n }\n}\n"; bool unknown = false; for (const ALScriptProblem& problem : service.check(script)) { unknown |= problem.message.find("llBrandNewThing") != std::string::npos; } - ensure("not yet known", unknown); + UNSCOPED_INFO("not yet known"); + CHECK(unknown); // A region with one function more. std::string text; { @@ -750,43 +938,53 @@ namespace tut out << text << "integer llBrandNewThing( integer Value )\n"; } const bool loaded = service.loadBuiltins(path, error); + // Tailslide's table is the process's and only ever added to: what + // the file declares stays for every case after this one. + ll_test::StateGuard::accept("Tailslide's builtins"); LLFile::remove(path); - ensure("loaded: " + error, loaded); + UNSCOPED_INFO("loaded: " + error); + CHECK(loaded); unknown = false; for (const ALScriptProblem& problem : service.check(script)) { unknown |= problem.message.find("llBrandNewThing") != std::string::npos; } - ensure("known now", !unknown); + UNSCOPED_INFO("known now"); + CHECK_FALSE(unknown); // Nothing twice: an old one still one declaration. const ALScriptSignature said = service.signature("default { state_entry() { llAbs( } }\n", 0, 32); - ensure("an old function as it was", said.found && said.label.find("llAbs") != std::string::npos); + { + INFO("an old function as it was"); + REQUIRE(said.found); + REQUIRE(said.label.find("llAbs") != std::string::npos); + } } - template<> template<> - void allslservice_object::test<22>() + TEST_CASE_METHOD(allslservice_data, "allslservice: every template for a new LSL script checks without an error", "[allslservice]") { - set_test_name("every template Script Studio offers for a new LSL script checks without an error"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); llifstream in(std::string(AL_SCRIPT_TEMPLATES_DIR) + "/lsl.xml", std::ios::in | std::ios::binary); LLSD templates; - ensure("read", in.is_open() && LLSDSerialize::fromXML(templates, in) != LLSDParser::PARSE_FAILURE && templates.isArray()); - ensure("some", templates.size() > 0); + CHECK(in.is_open()); + CHECK(LLSDSerialize::fromXML(templates, in) != LLSDParser::PARSE_FAILURE); + REQUIRE(templates.isArray()); + REQUIRE(templates.size() > 0); for (LLSD::array_const_iterator it = templates.beginArray(); it != templates.endArray(); ++it) { const std::string name = (*it)["name"].asString(); const ALScriptProblems problems = service.check((*it)["body"].asString()); - ensure(name + " parses", service.parsed()); - ensure_equals(name + ": " + said(problems), errors(problems), size_t(0)); + CAPTURE(name); + REQUIRE(service.parsed()); + INFO(said(problems)); + REQUIRE(errors(problems) == size_t(0)); } } - template<> template<> - void allslservice_object::test<23>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a handler or state begun in an open block is a missing brace before it", "[allslservice]") { - set_test_name("a handler or a state begun inside a block left open is a missing brace, after the last code before it, not what the " - "parser wanted of the head it misread"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); auto first_error = [&](const std::string& script) { for (const ALScriptProblem& problem : service.check(script)) { @@ -800,27 +998,47 @@ namespace tut const std::string open = "default\n{\n state_entry()\n {\n llSay(0, \"Hello, Avatar!\");\n\n"; // A handler with parameters, read as a call. ALScriptProblem p = first_error(open + " touch_start(integer total_number)\n {\n llSay(0, \"Touched.\");\n }\n}\n"); - ensure_equals("the brace: " + p.message, p.message, std::string("Missing '}'.")); - ensure("keyed", p.key == "LSLSyntaxMissing" && p.args.size() == 1 && p.args[0] == "'}'"); - ensure("on the semicolon before it", p.line == 4 && p.column == 34 && p.endColumn == 35); + CHECK(p.message == std::string("Missing '}'.")); + CHECK(p.key == "LSLSyntaxMissing"); + REQUIRE(p.args.size() == 1); + CHECK(p.args[0] == "'}'"); + { + INFO("on the semicolon before it"); + CHECK(p.line == 4); + CHECK(p.column == 34); + CHECK(p.endColumn == 35); + } // One without, its brace on the next line. p = first_error(open + " touch_start()\n {\n }\n}\n"); - ensure_equals("the brace, a head without parameters: " + p.message, p.message, std::string("Missing '}'.")); - ensure("on the same semicolon", p.line == 4 && p.column == 34); + UNSCOPED_INFO("a head without parameters"); + CHECK(p.message == std::string("Missing '}'.")); + { + INFO("on the same semicolon"); + CHECK(p.line == 4); + CHECK(p.column == 34); + } // A state after one not closed. p = first_error("default\n{\n state_entry()\n {\n }\n\nstate other\n{\n state_entry()\n {\n }\n}\n"); - ensure_equals("the state's brace: " + p.message, p.message, std::string("Missing '}'.")); - ensure("after the handler's", p.line == 4 && p.column == 4); + UNSCOPED_INFO("a state after one not closed"); + CHECK(p.message == std::string("Missing '}'.")); + { + INFO("after the handler's"); + CHECK(p.line == 4); + CHECK(p.column == 4); + } // A handler's own head wrong where handlers go is not one. p = first_error("default\n{\n touch_start(integer)\n {\n }\n}\n"); - ensure("a head wrong in its state's braces: not a brace", p.severity == ALScriptProblem::Severity::Error && p.message != "Missing '}'."); + { + INFO("a head wrong in its state's braces: not a brace"); + CHECK(p.severity == ALScriptProblem::Severity::Error); + CHECK(p.message != "Missing '}'."); + } } - template<> template<> - void allslservice_object::test<24>() + TEST_CASE_METHOD(allslservice_data, "allslservice: SlLoopInvariantCall notes a loop check's invariant call; NOLINT by name", "[allslservice]") { - set_test_name("the studio's own LSL lints beside Tailslide's: a pure call in a loop's check whose arguments the loop leaves be, noted once, and answering a NOLINT by its name"); - ensure("builtins load: " + error, loaded); + UNSCOPED_INFO("builtins load: " + error); + CHECK(loaded); const auto found = [&](const std::string& text) { ALScriptProblems out; for (const ALScriptProblem& p : service.check(text)) @@ -839,14 +1057,23 @@ namespace tut " while (i < llStringLength(s)) ++i;\n" " do ++i; while (i < llGetListLength(llParseString2List(s, [\",\"], [])));\n" "} }\n"); - ensure_equals("three: each loop's once", said.size(), size_t(3)); - ensure("a note keyed as Tailslide's, by the function: " + said[0].message, - said[0].key == "LSLSlLoopInvariantCall" && said[0].severity == ALScriptProblem::Severity::Note && said[0].line == 3 && - said[0].args == std::vector{ "llGetListLength", "integer", "3", "4" } && - said[0].source == ALScriptProblem::Source::Lint); - ensure("the outer call of two: " + said[2].message, said[2].line == 5 && said[2].args[0] == "llGetListLength"); - ensure("a NOLINT answers to the rule's name", - ALScriptFixes::suppressed(said[0], " for (i = 0; i < llGetListLength(gL); ++i) // NOLINT(SlLoopInvariantCall)", "", false)); + UNSCOPED_INFO("three: each loop's once"); + REQUIRE(said.size() == size_t(3)); + { + INFO("a note keyed as Tailslide's, by the function: " + said[0].message); + CHECK(said[0].key == "LSLSlLoopInvariantCall"); + CHECK(said[0].severity == ALScriptProblem::Severity::Note); + CHECK(said[0].line == 3); + CHECK(said[0].args == std::vector{ "llGetListLength", "integer", "3", "4" }); + CHECK(said[0].source == ALScriptProblem::Source::Lint); + } + { + INFO("the outer call of two: " + said[2].message); + CHECK(said[2].line == 5); + CHECK(said[2].args[0] == "llGetListLength"); + } + UNSCOPED_INFO("a NOLINT answers to the rule's name"); + CHECK(ALScriptFixes::suppressed(said[0], " for (i = 0; i < llGetListLength(gL); ++i) // NOLINT(SlLoopInvariantCall)", "", false)); said = found("list gL = [1, 2];\n" "grow() { gL += [0]; }\n" @@ -856,14 +1083,14 @@ namespace tut " for (i = 0; i < llGetListLength(gL) && i < 10; ++i) grow();\n" " while (llGetUnixTime() < 5) ++i;\n" "} }\n"); - ensure("not where the loop changes what it is given, itself or by a function, nor for a call that reads the world", said.empty()); + UNSCOPED_INFO("not where the loop changes what it is given, itself or by a function, nor for a call that reads the world"); + REQUIRE(said.empty()); } - template<> template<> - void allslservice_object::test<25>() + TEST_CASE_METHOD(allslservice_data, "allslservice: a shape-only outline drops details and mends apart from the kept tree", "[allslservice]") { - set_test_name("the shape alone, from a parse: the outline but for the words of each declaration, mended where it must be, the tree kept left be"); - ensure("builtins loaded: " + error, loaded); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(loaded); const std::string script = "integer count = 0;\n" "float half(integer n) { return n / 2.0; }\n" @@ -875,19 +1102,32 @@ namespace tut "}\n"; const std::vector whole = service.outline(script); const std::vector shape = service.outline(script, false); - ensure_equals("as many", shape.size(), whole.size()); + REQUIRE(shape.size() == whole.size()); for (size_t i = 0; i < shape.size(); ++i) { - ensure("the same " + whole[i].name, shape[i].name == whole[i].name && shape[i].kind == whole[i].kind && shape[i].depth == whole[i].depth && - shape[i].span == whole[i].span && shape[i].nameSpan == whole[i].nameSpan); - ensure("no words of its declaration: " + shape[i].detail, shape[i].detail.empty()); + CAPTURE(whole[i].name); + REQUIRE(shape[i].name == whole[i].name); + REQUIRE(shape[i].kind == whole[i].kind); + REQUIRE(shape[i].depth == whole[i].depth); + REQUIRE(shape[i].span == whole[i].span); + REQUIRE(shape[i].nameSpan == whole[i].nameSpan); + UNSCOPED_INFO("no words of its declaration: " + shape[i].detail); + REQUIRE(shape[i].detail.empty()); } // Another text's shape, broken: mended apart from the tree kept. const std::string broken = "integer count;\ndefault {\n state_entry() {\n llSay(0, (string)count\n }\n}\n"; const size_t before = service.mendings(); const std::vector mended = service.outline(broken, false); - ensure("mended to its shape", mended.size() == 3 && mended[0].name == "count" && mended[2].name == "state_entry" && mended[2].depth == 1); - ensure_equals("the tree kept mended nothing", service.mendings(), before); - ensure("and still answers for its own text", service.parsed()); + { + INFO("mended to its shape"); + REQUIRE(mended.size() == 3); + CHECK(mended[0].name == "count"); + CHECK(mended[2].name == "state_entry"); + CHECK(mended[2].depth == 1); + } + UNSCOPED_INFO("the tree kept mended nothing"); + CHECK(service.mendings() == before); + UNSCOPED_INFO("and still answers for its own text"); + CHECK(service.parsed()); } } diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 487713d896..c85486f357 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -31,7 +31,7 @@ #include "../lsl/allsltraits.h" #include "../luau/alluauservice.h" -#include "../test/lltut.h" +#include #include "Luau/Allocator.h" #include "Luau/Lexer.h" @@ -42,7 +42,7 @@ #include #include -namespace tut +namespace { struct allsltoslua_data { @@ -62,7 +62,7 @@ namespace tut loaded = lsl.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error); if (!loaded) { - fail("the builtins did not load: " + error); + FAIL("the builtins did not load: " + error); } } llifstream in(std::string(AL_LSL_DEFINITIONS_DIR) + "/secondlife.d.luau", std::ios::binary); @@ -80,7 +80,7 @@ namespace tut } // Written again, and it must have been. - ALLSLToSLua::Result convert(const std::string& lsl) + ALLSLToSLua::Result require_converted(const std::string& lsl) { ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl); std::string why; @@ -88,7 +88,8 @@ namespace tut { why += p.message + "\n"; } - ensure("converted: " + why, r.converted); + CAPTURE(why); + REQUIRE(r.converted); return r; } @@ -110,9 +111,9 @@ namespace tut } // What SLua's check says of it: nothing unwanted. - void checksClean(const ALLSLToSLua::Result& r) + void require_checks_clean(const ALLSLToSLua::Result& r) { - ensure("the definitions", definitions); + REQUIRE(definitions); std::string said; for (const ALScriptProblem& p : service.check(r.text)) { @@ -121,7 +122,8 @@ namespace tut said += llformat("[%d:%d] %s\n", p.line + 1, p.column + 1, p.message.c_str()); } } - ensure("checks as SLua:\n" + said + "---\n" + r.text, said.empty()); + CAPTURE(said, r.text); + REQUIRE(said.empty()); } // The comment Luau's own lexer reads beginning at `at` in the text, @@ -156,19 +158,21 @@ namespace tut } return {}; } - static std::string commentAt(const std::string& slua, const std::string& begins) + static std::string require_comment_at(const std::string& slua, const std::string& begins) { const size_t at = slua.find(begins); - ensure("written: " + begins + "\n" + slua, at != std::string::npos); + CAPTURE(begins, slua); + REQUIRE(at != std::string::npos); return commentAt(slua, at); } // A toggle turned the other way in SLua: a dash taken out of its // opener's ---, or put in a block comment's --; the comment that // begins there then. - static std::string toggled(const std::string& slua, const std::string& opener, bool off = true) + static std::string require_toggled(const std::string& slua, const std::string& opener, bool off = true) { const size_t at = slua.find(opener); - ensure("written: " + opener + "\n" + slua, at != std::string::npos); + CAPTURE(opener, slua); + REQUIRE(at != std::string::npos); std::string other = slua; if (off) { @@ -218,168 +222,245 @@ namespace tut } }; - typedef test_group allsltoslua_group; - typedef allsltoslua_group::object allsltoslua_object; - tut::allsltoslua_group allsltoslua_test("ALLSLToSLua"); - - template<> template<> - void allsltoslua_object::test<1>() - { - set_test_name("a script of one state: globals and functions as locals, handlers on LLEvents, the detected count from the table, state_entry run last"); - const ALLSLToSLua::Result r = convert("integer gCount = 2;\n" - "string greet(string who) { return \"Hello, \" + who; }\n" - "default {\n" - " state_entry() { llSay(0, greet(\"world\")); }\n" - " touch_start(integer total_number) {\n" - " gCount += total_number;\n" - " llSay(0, greet(llDetectedName(0)) + \" \" + (string)gCount);\n" - " }\n" - "}\n"); - ensure("the global: " + r.text, has(r, "local gCount = 2")); - ensure("the function: " + r.text, has(r, "local function greet(who)") && has(r, "return \"Hello, \" .. who")); - ensure("the handler: " + r.text, has(r, "LLEvents:on(\"touch_start\", function(detected)") && has(r, "local total_number = #detected")); - ensure("the step: " + r.text, has(r, "gCount += total_number")); - ensure("what was detected from the table: " + r.text, has(r, "detected[1]:getName()") && noted(r, "SluaDetectedTable")); - ensure("an integer joined to text, interpolated: " + r.text, has(r, "ll.Say(0, `{greet(detected[1]:getName())} {gCount}`)")); - ensure("state_entry last: " + r.text, r.text.find("-- state_entry") > r.text.find("LLEvents:on") && has(r, "ll.Say(0, greet(\"world\"))")); - ensure("the note over its line: " + r.text, has(r, "-- LSL: detected[n] is what LSL read")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<2>() - { - set_test_name("expressions: conditions as LSL reads them, comparisons as 1 or 0 where they are numbers, integer division and remainder toward nought, lists grown and measured"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " integer a = 7; integer b = 2; float f = 1.5; string s = \"x\"; list l = [1, 2];\n" - " integer same = a == b;\n" - " if (a) llOwnerSay(\"a\");\n" - " if (s) llOwnerSay(\"said\");\n" - " if (a > 1 && llGetListLength(l) > 1) llOwnerSay(\"both\");\n" - " integer q = a / b; integer m = a % b;\n" - " l += 3; l = l + [4] + \"five\";\n" - " integer t = TRUE; if (FALSE) t = 0;\n" - " llOwnerSay((string)f + (string)((integer)\"12abc\"));\n" - " if (l == [1]) llOwnerSay(\"one long\");\n" - " integer flags = a & b | 4;\n" - " vector v = <1, 2, 3>; v.x = 4; v.z += 1;\n" - " llOwnerSay((string)v + (string)same + (string)q + (string)m + (string)t + (string)flags);\n" - "} }\n"); - ensure("a comparison as a number: " + r.text, has(r, "local same = if a == b then 1 else 0")); - ensure("an integer's truth: " + r.text, has(r, "if a ~= 0 then")); - ensure("a string's: " + r.text, has(r, "if s ~= \"\" then")); - ensure("and: " + r.text, has(r, "if a > 1 and #l > 1 then")); - ensure("integer division: " + r.text, has(r, "local q = bit32.s32(a / b)")); - ensure("remainder: " + r.text, has(r, "local m = math.fmod(a, b)")); - ensure("a list no other holds grown in place: " + r.text, has(r, "table.insert(l, 3)") && has(r, "table.append(l, 4, \"five\")") && - !has(r, "table.extend")); - ensure("TRUE and FALSE: " + r.text, has(r, "local t = 1") && has(r, "if false then")); - ensure("a float as LSL writes it: " + r.text, has(r, "string.format(\"%.6f\", f)")); - ensure("a string as an integer, as a list's item converts it: " + r.text, - has(r, "lslInteger(\"12abc\")") && has(r, "return llcompat.List2Integer({ s }, 0)")); - ensure("lists compared by length: " + r.text, has(r, "#l == #{ 1 }") && noted(r, "SluaListCompare")); + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a one-state script as locals and LLEvents handlers; state_entry last", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer gCount = 2;\n" + "string greet(string who) { return \"Hello, \" + who; }\n" + "default {\n" + " state_entry() { llSay(0, greet(\"world\")); }\n" + " touch_start(integer total_number) {\n" + " gCount += total_number;\n" + " llSay(0, greet(llDetectedName(0)) + \" \" + (string)gCount);\n" + " }\n" + "}\n"); + CAPTURE(r.text); + UNSCOPED_INFO("the global"); + CHECK(has(r, "local gCount = 2")); + { + INFO("the function"); + CHECK(has(r, "local function greet(who)")); + CHECK(has(r, "return \"Hello, \" .. who")); + } + { + INFO("the handler"); + CHECK(has(r, "LLEvents:on(\"touch_start\", function(detected)")); + CHECK(has(r, "local total_number = #detected")); + } + UNSCOPED_INFO("the step"); + CHECK(has(r, "gCount += total_number")); + { + INFO("what was detected from the table"); + CHECK(has(r, "detected[1]:getName()")); + CHECK(noted(r, "SluaDetectedTable")); + } + UNSCOPED_INFO("an integer joined to text, interpolated"); + CHECK(has(r, "ll.Say(0, `{greet(detected[1]:getName())} {gCount}`)")); + { + INFO("state_entry last"); + CHECK(r.text.find("-- state_entry") > r.text.find("LLEvents:on")); + CHECK(has(r, "ll.Say(0, greet(\"world\"))")); + } + UNSCOPED_INFO("the note over its line"); + CHECK(has(r, "-- LSL: detected[n] is what LSL read")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: expressions: truth tests; 1 or 0 comparisons; / and % toward nought; lists", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " integer a = 7; integer b = 2; float f = 1.5; string s = \"x\"; list l = [1, 2];\n" + " integer same = a == b;\n" + " if (a) llOwnerSay(\"a\");\n" + " if (s) llOwnerSay(\"said\");\n" + " if (a > 1 && llGetListLength(l) > 1) llOwnerSay(\"both\");\n" + " integer q = a / b; integer m = a % b;\n" + " l += 3; l = l + [4] + \"five\";\n" + " integer t = TRUE; if (FALSE) t = 0;\n" + " llOwnerSay((string)f + (string)((integer)\"12abc\"));\n" + " if (l == [1]) llOwnerSay(\"one long\");\n" + " integer flags = a & b | 4;\n" + " vector v = <1, 2, 3>; v.x = 4; v.z += 1;\n" + " llOwnerSay((string)v + (string)same + (string)q + (string)m + (string)t + (string)flags);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a comparison as a number"); + CHECK(has(r, "local same = if a == b then 1 else 0")); + UNSCOPED_INFO("an integer's truth"); + CHECK(has(r, "if a ~= 0 then")); + UNSCOPED_INFO("a string's"); + CHECK(has(r, "if s ~= \"\" then")); + CHECK(has(r, "if a > 1 and #l > 1 then")); + UNSCOPED_INFO("integer division"); + CHECK(has(r, "local q = bit32.s32(a / b)")); + UNSCOPED_INFO("remainder"); + CHECK(has(r, "local m = math.fmod(a, b)")); + { + INFO("a list no other holds grown in place"); + CHECK(has(r, "table.insert(l, 3)")); + CHECK(has(r, "table.append(l, 4, \"five\")")); + CHECK_FALSE(has(r, "table.extend")); + } + { + INFO("TRUE and FALSE"); + CHECK(has(r, "local t = 1")); + CHECK(has(r, "if false then")); + } + UNSCOPED_INFO("a float as LSL writes it"); + CHECK(has(r, "string.format(\"%.6f\", f)")); + { + INFO("a string as an integer, as a list's item converts it"); + CHECK(has(r, "lslInteger(\"12abc\")")); + CHECK(has(r, "return llcompat.List2Integer({ s }, 0)")); + } + { + INFO("lists compared by length"); + CHECK(has(r, "#l == #{ 1 }")); + CHECK(noted(r, "SluaListCompare")); + } // bit32 answers 0 to 4294967295: made LSL's signed integer, but where // bit32 takes it again. - ensure("bits: " + r.text, has(r, "bit32.s32(bit32.bor(bit32.band(a, b), 4))") && !has(r, "local function int32")); - ensure("a vector's part set: " + r.text, has(r, "v = vector(4, v.y, v.z)") && has(r, "v = vector(v.x, v.y, v.z + 1)")); - ensure("a vector as LSL writes it: " + r.text, has(r, "ll.DumpList2String({ v }, \"\")")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<3>() - { - set_test_name("loops: a counting for as Luau's numeric for, a jump out as break and to the next turn as continue, a do as repeat; another jump noted"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " integer i; integer n;\n" - " for (i = 0; i < 10; ++i) {\n" - " if (i == 3) jump next;\n" - " if (i == 8) jump done;\n" - " n += i;\n" - " @next;\n" - " }\n" - " @done;\n" - " do { n -= 1; } while (n > 5);\n" - " while (n) { n--; }\n" - " jump away;\n" - " n = 1;\n" - " @away;\n" - " llOwnerSay((string)n);\n" - "} }\n"); - ensure("a numeric for: " + r.text, has(r, "for i = 0, 9 do")); - ensure("its counter declared by it alone: " + r.text, !has(r, "local i = 0")); - ensure("continue, the step Luau's own: " + r.text, has(r, "then continue end")); - ensure("break: " + r.text, has(r, "break")); - ensure("repeat: " + r.text, has(r, "repeat") && has(r, "until not (n > 5)")); - ensure("a step: " + r.text, has(r, "n -= 1")); - ensure("another jump noted and kept as a mark: " + r.text, noted(r, "SluaJump") && has(r, "-- jump away") && has(r, "-- @away")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<4>() - { - set_test_name("states: each a table of handlers, entered by setState, a state change ending the event; the timer on LLTimers, calling the state's"); - const ALLSLToSLua::Result r = convert("default {\n" - " state_entry() { llSetTimerEvent(1.0); }\n" - " timer() { state running; }\n" - " state_exit() { llOwnerSay(\"leaving\"); }\n" - "}\n" - "state running {\n" - " state_entry() { llOwnerSay(\"running\"); }\n" - " touch_start(integer n) { if (n > 1) { state default; } llOwnerSay(\"touched\"); }\n" - "}\n"); - ensure("the tables: " + r.text, has(r, "states.default = {") && has(r, "states.running = {")); - ensure("setState: " + r.text, has(r, "local function setState(name: string)") && has(r, "setState(\"default\")")); - ensure("a change ends the event, where anything follows: " + r.text, - has(r, "setState(\"running\")\n end,") && has(r, "setState(\"default\")\n return\n end")); - ensure("the timer on LLTimers: " + r.text, has(r, "setTimer(1") && has(r, "LLTimers:every(seconds") && noted(r, "SluaTimers")); - ensure("calling the state's handler: " + r.text, has(r, "states[currentState].timer") && has(r, "event ~= \"timer\"")); - ensure("state_exit a handler of its state: " + r.text, has(r, "state_exit = function()")); - ensure("what is let go of noted: " + r.text, noted(r, "SluaStates")); + CHECK(has(r, "bit32.s32(bit32.bor(bit32.band(a, b), 4))")); + CHECK_FALSE(has(r, "local function int32")); + { + INFO("a vector's part set"); + CHECK(has(r, "v = vector(4, v.y, v.z)")); + CHECK(has(r, "v = vector(v.x, v.y, v.z + 1)")); + } + UNSCOPED_INFO("a vector as LSL writes it"); + CHECK(has(r, "ll.DumpList2String({ v }, \"\")")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: loops: numeric for; break and continue jumps; repeat; other jumps noted", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " integer i; integer n;\n" + " for (i = 0; i < 10; ++i) {\n" + " if (i == 3) jump next;\n" + " if (i == 8) jump done;\n" + " n += i;\n" + " @next;\n" + " }\n" + " @done;\n" + " do { n -= 1; } while (n > 5);\n" + " while (n) { n--; }\n" + " jump away;\n" + " n = 1;\n" + " @away;\n" + " llOwnerSay((string)n);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a numeric for"); + CHECK(has(r, "for i = 0, 9 do")); + UNSCOPED_INFO("its counter declared by it alone"); + CHECK_FALSE(has(r, "local i = 0")); + UNSCOPED_INFO("continue, the step Luau's own"); + CHECK(has(r, "then continue end")); + CHECK(has(r, "break")); + CHECK(has(r, "repeat")); + CHECK(has(r, "until not (n > 5)")); + UNSCOPED_INFO("a step"); + CHECK(has(r, "n -= 1")); + { + INFO("another jump noted and kept as a mark"); + CHECK(noted(r, "SluaJump")); + CHECK(has(r, "-- jump away")); + CHECK(has(r, "-- @away")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: states as handler tables entered by setState; the timer on LLTimers", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default {\n" + " state_entry() { llSetTimerEvent(1.0); }\n" + " timer() { state running; }\n" + " state_exit() { llOwnerSay(\"leaving\"); }\n" + "}\n" + "state running {\n" + " state_entry() { llOwnerSay(\"running\"); }\n" + " touch_start(integer n) { if (n > 1) { state default; } llOwnerSay(\"touched\"); }\n" + "}\n"); + CAPTURE(r.text); + { + INFO("the tables"); + CHECK(has(r, "states.default = {")); + CHECK(has(r, "states.running = {")); + } + CHECK(has(r, "local function setState(name: string)")); + CHECK(has(r, "setState(\"default\")")); + { + INFO("a change ends the event, where anything follows"); + CHECK(has(r, "setState(\"running\")\n end,")); + CHECK(has(r, "setState(\"default\")\n return\n end")); + } + { + INFO("the timer on LLTimers"); + CHECK(has(r, "setTimer(1")); + CHECK(has(r, "LLTimers:every(seconds")); + CHECK(noted(r, "SluaTimers")); + } + { + INFO("calling the state's handler"); + CHECK(has(r, "states[currentState].timer")); + CHECK(has(r, "event ~= \"timer\"")); + } + UNSCOPED_INFO("state_exit a handler of its state"); + CHECK(has(r, "state_exit = function()")); + UNSCOPED_INFO("what is let go of noted"); + CHECK(noted(r, "SluaStates")); // Tapple's: a name with no state, which only an edit could write, // said so at the call, before the state it is in is left. const size_t looked = r.text.find(" local entering = states[name]\n if not entering then\n error(\"unknown state: \" .. name, 2)\n end\n"); - ensure("a state not there said: " + r.text, looked != std::string::npos && looked < r.text.find("local leaving")); - checksClean(r); + { + INFO("a state not there said"); + REQUIRE(looked != std::string::npos); + CHECK(looked < r.text.find("local leaving")); + } + require_checks_clean(r); ALLSLToSLua::Options typed; typed.types = true; - checksClean(ALLSLToSLua::convert("default { touch_start(integer n) { state two; } }\nstate two { state_entry() { state default; } }\n", typed)); + require_checks_clean(ALLSLToSLua::convert("default { touch_start(integer n) { state two; } }\nstate two { state_entry() { state default; } }\n", typed)); } - template<> template<> - void allsltoslua_object::test<5>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: names: reserved ones renamed; forward declarations; inline assignments", "[ALLSLToSLua]") { - set_test_name("names: one Luau or SLua holds renamed, functions declared first where one calls another after it; assignments inside expressions made in place"); - const ALLSLToSLua::Result r = convert("integer table = 1;\n" - "integer first(integer end) { return second(end) + table; }\n" - "integer second(integer x) { return x * 2; }\n" - "default { state_entry() {\n" - " integer i; integer j = (i = 4) + i++;\n" - " llOwnerSay((string)first(j));\n" - "} }\n"); - ensure("renamed: " + r.text, has(r, "local table_ = 1") && has(r, "(end_)")); - ensure("only the one called before it is written declared first: " + r.text, - has(r, "local second\n") && has(r, "local function first(end_)") && has(r, "\nfunction second(x)")); - ensure("an assignment in place: " + r.text, has(r, "(function() i = 4 return i end)()") && noted(r, "SluaAssignInExpression")); - ensure("a step after, what it was: " + r.text, has(r, "(function() local was = i; i += 1 return was end)()")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted("integer table = 1;\n" + "integer first(integer end) { return second(end) + table; }\n" + "integer second(integer x) { return x * 2; }\n" + "default { state_entry() {\n" + " integer i; integer j = (i = 4) + i++;\n" + " llOwnerSay((string)first(j));\n" + "} }\n"); + CAPTURE(r.text); + CHECK(has(r, "local table_ = 1")); + CHECK(has(r, "(end_)")); + { + INFO("only the one called before it is written declared first"); + CHECK(has(r, "local second\n")); + CHECK(has(r, "local function first(end_)")); + CHECK(has(r, "\nfunction second(x)")); + } + { + INFO("an assignment in place"); + CHECK(has(r, "(function() i = 4 return i end)()")); + CHECK(noted(r, "SluaAssignInExpression")); + } + UNSCOPED_INFO("a step after, what it was"); + CHECK(has(r, "(function() local was = i; i += 1 return was end)()")); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<6>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: LSL that does not parse writes nothing and says why", "[ALLSLToSLua]") { - set_test_name("LSL that does not parse writes nothing, and says why"); const ALLSLToSLua::Result r = ALLSLToSLua::convert("default { state_entry() { integer = ; } }"); - ensure("not converted", !r.converted && r.text.empty()); - ensure("why", !r.problems.empty()); + CHECK_FALSE(r.converted); + CHECK(r.text.empty()); + CHECK_FALSE(r.problems.empty()); } - template<> template<> - void allsltoslua_object::test<7>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: close to LSL: llcompat's Detected and timer event noted; ll.OwnerSay kept", "[ALLSLToSLua]") { - set_test_name("close to LSL: llcompat's Detected functions and timer event, as LSL had them, noted with SLua's ways; ll's " - "OwnerSay, which only SLua's deprecating sets apart"); const ALLSLToSLua::Options close = ALLSLToSLua::Options::closeToLSL(); const ALLSLToSLua::Result r = ALLSLToSLua::convert("default {\n" " state_entry() { llSetTimerEvent(2.0); }\n" @@ -387,50 +468,82 @@ namespace tut " touch_start(integer n) { llOwnerSay(llDetectedName(0)); }\n" "}\n", close); - ensure("converted", r.converted); - ensure("llcompat's Detected: " + r.text, has(r, "llcompat.DetectedName(0)") && noted(r, "SluaDetected")); - ensure("the timer event: " + r.text, has(r, "llcompat.SetTimerEvent(2") && has(r, "LLEvents:on(\"timer\"") && noted(r, "SluaTimer")); - ensure("ll.OwnerSay, SLua deprecating it for print: " + r.text, - has(r, "ll.OwnerSay(\"tick\")") && !has(r, "llcompat.OwnerSay") && noted(r, "SluaDeprecatedFor", "OwnerSay")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<8>() - { - set_test_name("SLua's ll where it means the same -- a boolean, a constant index moved on, a find against nil -- and what SLua has in a call's stead"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " key k = llDetectedKey(0); string s = \"abcdef\"; list l = [\"a\", \"b\"]; integer i = 2;\n" - " if (llSameGroup(k)) llOwnerSay(\"group\");\n" - " integer same = llSameGroup(k);\n" - " string head = llGetSubString(s, 0, 2);\n" - " string tail = llGetSubString(s, i, -1);\n" - " if (llListFindList(l, [\"a\"]) != -1) llOwnerSay(\"found\");\n" - " if (~llSubStringIndex(s, \"c\")) llOwnerSay(\"has c\");\n" - " if (llSubStringIndex(s, \"z\") < 0) llOwnerSay(\"no z\");\n" - " integer at = llListFindList(l, [\"a\", \"b\"]);\n" - " float p = llPow(2.0, 3.0) + llFabs(-1.0) + llVecDist(<1,2,3>, ZERO_VECTOR);\n" - " integer r = llRound(2.5) + llGetUnixTime();\n" - " llOwnerSay(head + tail + (string)same + (string)at + (string)p + (string)r);\n" - "} }\n"); - ensure("a boolean in a condition: " + r.text, has(r, "if ll.SameGroup(k) then")); - ensure("and as a number: " + r.text, has(r, "local same = if ll.SameGroup(k) then 1 else 0")); - ensure("a constant index moved on: " + r.text, has(r, "ll.GetSubString(s, 1, 3)")); - ensure("one that is not, through llcompat: " + r.text, has(r, "llcompat.GetSubString(s, i, -1)") && noted(r, "SluaIndex", "GetSubString")); - ensure("one thing found in a list: " + r.text, has(r, "if table.find(l, \"a\") ~= nil then")); - ensure("~ of a find: " + r.text, has(r, "if string.find(s, \"c\", 1, true) ~= nil then")); - ensure("< 0 of one: " + r.text, has(r, "if string.find(s, \"z\", 1, true) == nil then")); - ensure("a find's index as a number, through llcompat: " + r.text, has(r, "llcompat.ListFindList(l, { \"a\", \"b\" })")); - ensure("^, math and vector: " + r.text, has(r, "2.0 ^ 3.0") && has(r, "math.abs(-1.0)") && has(r, "vector.magnitude(vector(1, 2, 3) - ZERO_VECTOR)")); - ensure("half up, as LSL rounds, not math.round: " + r.text, has(r, "math.floor(2.5 + 0.5)")); - ensure("os.time and print: " + r.text, has(r, "os.time()") && has(r, "print(")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<9>() - { - set_test_name("handlers assigned to LLEvents' fields, and Luau types on what LSL typed"); + CAPTURE(r.text); + CHECK(r.converted); + { + INFO("llcompat's Detected"); + CHECK(has(r, "llcompat.DetectedName(0)")); + CHECK(noted(r, "SluaDetected")); + } + { + INFO("the timer event"); + CHECK(has(r, "llcompat.SetTimerEvent(2")); + CHECK(has(r, "LLEvents:on(\"timer\"")); + CHECK(noted(r, "SluaTimer")); + } + { + INFO("ll.OwnerSay, SLua deprecating it for print"); + CHECK(has(r, "ll.OwnerSay(\"tick\")")); + CHECK_FALSE(has(r, "llcompat.OwnerSay")); + CHECK(noted(r, "SluaDeprecatedFor", "OwnerSay")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: SLua's ll where it means the same and SLua's own forms for some calls", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " key k = llDetectedKey(0); string s = \"abcdef\"; list l = [\"a\", \"b\"]; integer i = 2;\n" + " if (llSameGroup(k)) llOwnerSay(\"group\");\n" + " integer same = llSameGroup(k);\n" + " string head = llGetSubString(s, 0, 2);\n" + " string tail = llGetSubString(s, i, -1);\n" + " if (llListFindList(l, [\"a\"]) != -1) llOwnerSay(\"found\");\n" + " if (~llSubStringIndex(s, \"c\")) llOwnerSay(\"has c\");\n" + " if (llSubStringIndex(s, \"z\") < 0) llOwnerSay(\"no z\");\n" + " integer at = llListFindList(l, [\"a\", \"b\"]);\n" + " float p = llPow(2.0, 3.0) + llFabs(-1.0) + llVecDist(<1,2,3>, ZERO_VECTOR);\n" + " integer r = llRound(2.5) + llGetUnixTime();\n" + " llOwnerSay(head + tail + (string)same + (string)at + (string)p + (string)r);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a boolean in a condition"); + CHECK(has(r, "if ll.SameGroup(k) then")); + UNSCOPED_INFO("and as a number"); + CHECK(has(r, "local same = if ll.SameGroup(k) then 1 else 0")); + UNSCOPED_INFO("a constant index moved on"); + CHECK(has(r, "ll.GetSubString(s, 1, 3)")); + { + INFO("one that is not, through llcompat"); + CHECK(has(r, "llcompat.GetSubString(s, i, -1)")); + CHECK(noted(r, "SluaIndex", "GetSubString")); + } + UNSCOPED_INFO("one thing found in a list"); + CHECK(has(r, "if table.find(l, \"a\") ~= nil then")); + UNSCOPED_INFO("~ of a find"); + CHECK(has(r, "if string.find(s, \"c\", 1, true) ~= nil then")); + UNSCOPED_INFO("< 0 of one"); + CHECK(has(r, "if string.find(s, \"z\", 1, true) == nil then")); + UNSCOPED_INFO("a find's index as a number, through llcompat"); + CHECK(has(r, "llcompat.ListFindList(l, { \"a\", \"b\" })")); + { + INFO("^, math and vector"); + CHECK(has(r, "2.0 ^ 3.0")); + CHECK(has(r, "math.abs(-1.0)")); + CHECK(has(r, "vector.magnitude(vector(1, 2, 3) - ZERO_VECTOR)")); + } + UNSCOPED_INFO("half up, as LSL rounds, not math.round"); + CHECK(has(r, "math.floor(2.5 + 0.5)")); + { + INFO("os.time and print"); + CHECK(has(r, "os.time()")); + CHECK(has(r, "print(")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: handlers assigned to LLEvents' fields and Luau types on what LSL typed", "[ALLSLToSLua]") + { ALLSLToSLua::Options options; options.handlers = ALLSLToSLua::Options::Handlers::Field; options.types = true; @@ -438,13 +551,20 @@ namespace tut "string greet(string who) { return \"Hi \" + who; }\n" "default { touch_start(integer n) { string s = greet(\"x\"); gCount += n; llOwnerSay(s); } }\n", options); - ensure("converted", r.converted); - ensure("a field, as SLua's own scripts write one: " + r.text, - has(r, "function LLEvents.touch_start(detected: { DetectedEvent })") && !has(r, "LLEvents:on") && !has(r, "= function(")); - ensure("the count typed: " + r.text, has(r, "local n: number = #detected")); - ensure("typed: " + r.text, has(r, "local gCount: number = 2") && has(r, "local function greet(who: string): string") && - has(r, "local s: string = greet(\"x\")")); - checksClean(r); + CAPTURE(r.text); + CHECK(r.converted); + { + INFO("a field, as SLua's own scripts write one"); + CHECK(has(r, "function LLEvents.touch_start(detected: { DetectedEvent })")); + CHECK_FALSE(has(r, "LLEvents:on")); + CHECK_FALSE(has(r, "= function(")); + } + UNSCOPED_INFO("the count typed"); + CHECK(has(r, "local n: number = #detected")); + CHECK(has(r, "local gCount: number = 2")); + CHECK(has(r, "local function greet(who: string): string")); + CHECK(has(r, "local s: string = greet(\"x\")")); + require_checks_clean(r); // Each parameter as the definitions type SLua's handler: a key a // uuid, link_message's id the string SLua passes; none on a key the @@ -456,12 +576,16 @@ namespace tut " money(key giver, integer amount) { giver = \"someone\"; llOwnerSay((string)giver); }\n" "}\n"; const ALLSLToSLua::Result typed = ALLSLToSLua::convert(handlers, options); - ensure("an integer: " + typed.text, has(typed, "function LLEvents.on_rez(param: number)")); - ensure("a key a uuid: " + typed.text, has(typed, "function LLEvents.listen(c: number, n: string, id: uuid, m: string)")); - ensure("link_message's id a string: " + typed.text, has(typed, "function LLEvents.link_message(s: number, num: number, str: string, id: string)")); - ensure("a key the body takes as text, under a name of its own: " + typed.text, - has(typed, "function LLEvents.money(giverKey: uuid, amount: number)\n local giver: string = tostring(giverKey)\n giver = \"someone\"\n")); - checksClean(typed); + CAPTURE(typed.text); + UNSCOPED_INFO("an integer"); + CHECK(has(typed, "function LLEvents.on_rez(param: number)")); + UNSCOPED_INFO("a key a uuid"); + CHECK(has(typed, "function LLEvents.listen(c: number, n: string, id: uuid, m: string)")); + UNSCOPED_INFO("link_message's id a string"); + CHECK(has(typed, "function LLEvents.link_message(s: number, num: number, str: string, id: string)")); + UNSCOPED_INFO("a key the body takes as text, under a name of its own"); + CHECK(has(typed, "function LLEvents.money(giverKey: uuid, amount: number)\n local giver: string = tostring(giverKey)\n giver = \"someone\"\n")); + require_checks_clean(typed); // Strict as well, under the grid's solver: each handler as the // definitions type it, the text key made text. if (!newSolver) @@ -471,333 +595,499 @@ namespace tut { said += p.severity == ALScriptProblem::Severity::Error ? p.message + "\n" : std::string(); } - ensure("strict:\n" + said + "---\n" + typed.text, said.empty()); + CAPTURE(said); + CHECK(said.empty()); } ALLSLToSLua::Options on = options; on.handlers = ALLSLToSLua::Options::Handlers::On; - ensure("typed with LLEvents:on too", has(ALLSLToSLua::convert(handlers, on), "LLEvents:on(\"on_rez\", function(param: number)")); + UNSCOPED_INFO("typed with LLEvents:on too"); + CHECK(has(ALLSLToSLua::convert(handlers, on), "LLEvents:on(\"on_rez\", function(param: number)")); on.types = false; - ensure("and none where types are not written", has(ALLSLToSLua::convert(handlers, on), "LLEvents:on(\"on_rez\", function(param)")); + UNSCOPED_INFO("and none where types are not written"); + CHECK(has(ALLSLToSLua::convert(handlers, on), "LLEvents:on(\"on_rez\", function(param)")); const ALLSLToSLua::Result many = ALLSLToSLua::convert("default { touch_start(integer n) { state other; } }\n" "state other { touch_start(integer n) { state default; } }\n", options); + CAPTURE(many.text); // SLua stops the script at a field assigned nil: taken off by // LLEvents:off, as LLEvents:on's are. - ensure("setState sets the fields: " + many.text, has(many, "(LLEvents :: any)[event] = handler") && - has(many, "LLEvents:off(event :: any, handler)") && !has(many, "[event] = nil")); - ensure("a state's handlers typed in its table: " + many.text, has(many, "touch_start = function(detected: { DetectedEvent })")); - checksClean(many); + { + INFO("setState sets the fields"); + CHECK(has(many, "(LLEvents :: any)[event] = handler")); + CHECK(has(many, "LLEvents:off(event :: any, handler)")); + CHECK_FALSE(has(many, "[event] = nil")); + } + UNSCOPED_INFO("a state's handlers typed in its table"); + CHECK(has(many, "touch_start = function(detected: { DetectedEvent })")); + require_checks_clean(many); } - template<> template<> - void allsltoslua_object::test<10>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: no comments where they are not wanted: the notes are still said", "[ALLSLToSLua]") { - set_test_name("no comments where they are not wanted: the notes are still said"); ALLSLToSLua::Options options; options.comments = false; const ALLSLToSLua::Result r = ALLSLToSLua::convert("default { state_entry() { list l = [7]; integer a = l == [2]; llOwnerSay((string)a); } }\n", options); - ensure("converted", r.converted); - ensure("no comment: " + r.text, !has(r, "-- LSL:")); - ensure("but noted", noted(r, "SluaListCompare")); - } - - template<> template<> - void allsltoslua_object::test<11>() - { - set_test_name("numeric for where it counts as LSL's did: up, down, by a step, to a length; a while where the counter or the limit could change, or the counter is read after"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " list l = [1, 2, 3]; integer n = 4; integer total;\n" - " integer a; for (a = 0; a < llGetListLength(l); a++) total += llList2Integer(l, a);\n" - " integer b; for (b = 10; b >= 0; b -= 2) total += b;\n" - " integer c; for (c = 1; c <= n; ++c) total += c;\n" - " integer d; for (d = 0; d < n; ++d) { if (total > 100) d = n; }\n" - " integer e; for (e = 0; e < n; ++e) { n--; }\n" - " integer f; for (f = 0; f < 3; ++f) total += f;\n" - " llOwnerSay((string)(total + f));\n" - "} }\n"); - ensure("up to a length, walking its items: " + r.text, has(r, "for _, item in l do\n total += item")); - ensure("down by a step: " + r.text, has(r, "for b = 10, 0, -2 do")); - ensure("up to and with: " + r.text, has(r, "for c = 1, n do")); - ensure("a counter set in its body keeps the while: " + r.text, has(r, "while d < n do")); - ensure("a limit set in its body keeps the while: " + r.text, has(r, "while e < n do")); - ensure("a counter read after keeps the while: " + r.text, has(r, "while f < 3 do") && has(r, "local f = 0")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<12>() - { - set_test_name("integers only ever truth values are booleans, and functions that answer one; each use that reads a number keeps it one"); - const ALLSLToSLua::Result r = convert("integer gOn = FALSE;\n" - "integer gSeen;\n" - "integer isOwner(key k) { return k == llGetOwner(); }\n" - "integer anyOf(list l) { return llGetListLength(l); }\n" - "default { touch_start(integer n) {\n" - " gOn = !gOn;\n" - " if (gOn) llOwnerSay(\"on\"); else llOwnerSay(\"off\");\n" - " if (gSeen == FALSE && isOwner(llDetectedKey(0))) gSeen = TRUE;\n" - " integer busy = anyOf([1]);\n" - " while (!busy) busy = TRUE;\n" - " integer count; count++;\n" - " integer shown = TRUE; llOwnerSay((string)shown);\n" - " integer loose = 5; if (loose == TRUE) llOwnerSay(\"one\");\n" - " integer y; integer z = (y = 5); if (y) llOwnerSay((string)z);\n" - " if (count) llOwnerSay((string)count);\n" - "} }\n"); - ensure("a global toggled: " + r.text, has(r, "local gOn = false") && has(r, "gOn = not gOn") && has(r, "if gOn then")); - ensure("compared with FALSE: " + r.text, has(r, "local gSeen = false") && has(r, "if not gSeen and isOwner(") && has(r, "gSeen = true")); - ensure("a function that answers one: " + r.text, has(r, "return k == ll.GetOwner()")); - ensure("a length, as a truth: " + r.text, has(r, "return #l ~= 0") && has(r, "local busy = anyOf({ 1 })") && has(r, "while not busy do")); - ensure("stepped: " + r.text, has(r, "local count = 0") && has(r, "count += 1")); - ensure("printed: " + r.text, has(r, "local shown = 1")); - ensure("more than TRUE, compared with it: " + r.text, has(r, "local loose = 5") && has(r, "if loose == 1 then")); - ensure("set where the assignment is read: " + r.text, has(r, "local y = 0")); - checksClean(r); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("no comment"); + CHECK_FALSE(has(r, "-- LSL:")); + UNSCOPED_INFO("but noted"); + CHECK(noted(r, "SluaListCompare")); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: numeric for where it counts as LSL's did; while where it might not", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " list l = [1, 2, 3]; integer n = 4; integer total;\n" + " integer a; for (a = 0; a < llGetListLength(l); a++) total += llList2Integer(l, a);\n" + " integer b; for (b = 10; b >= 0; b -= 2) total += b;\n" + " integer c; for (c = 1; c <= n; ++c) total += c;\n" + " integer d; for (d = 0; d < n; ++d) { if (total > 100) d = n; }\n" + " integer e; for (e = 0; e < n; ++e) { n--; }\n" + " integer f; for (f = 0; f < 3; ++f) total += f;\n" + " llOwnerSay((string)(total + f));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("up to a length, walking its items"); + CHECK(has(r, "for _, item in l do\n total += item")); + UNSCOPED_INFO("down by a step"); + CHECK(has(r, "for b = 10, 0, -2 do")); + UNSCOPED_INFO("up to and with"); + CHECK(has(r, "for c = 1, n do")); + UNSCOPED_INFO("a counter set in its body keeps the while"); + CHECK(has(r, "while d < n do")); + UNSCOPED_INFO("a limit set in its body keeps the while"); + CHECK(has(r, "while e < n do")); + { + INFO("a counter read after keeps the while"); + CHECK(has(r, "while f < 3 do")); + CHECK(has(r, "local f = 0")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: integers only used as truths become booleans; number reads keep numbers", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer gOn = FALSE;\n" + "integer gSeen;\n" + "integer isOwner(key k) { return k == llGetOwner(); }\n" + "integer anyOf(list l) { return llGetListLength(l); }\n" + "default { touch_start(integer n) {\n" + " gOn = !gOn;\n" + " if (gOn) llOwnerSay(\"on\"); else llOwnerSay(\"off\");\n" + " if (gSeen == FALSE && isOwner(llDetectedKey(0))) gSeen = TRUE;\n" + " integer busy = anyOf([1]);\n" + " while (!busy) busy = TRUE;\n" + " integer count; count++;\n" + " integer shown = TRUE; llOwnerSay((string)shown);\n" + " integer loose = 5; if (loose == TRUE) llOwnerSay(\"one\");\n" + " integer y; integer z = (y = 5); if (y) llOwnerSay((string)z);\n" + " if (count) llOwnerSay((string)count);\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("a global toggled"); + CHECK(has(r, "local gOn = false")); + CHECK(has(r, "gOn = not gOn")); + CHECK(has(r, "if gOn then")); + } + { + INFO("compared with FALSE"); + CHECK(has(r, "local gSeen = false")); + CHECK(has(r, "if not gSeen and isOwner(")); + CHECK(has(r, "gSeen = true")); + } + UNSCOPED_INFO("a function that answers one"); + CHECK(has(r, "return k == ll.GetOwner()")); + { + INFO("a length, as a truth"); + CHECK(has(r, "return #l ~= 0")); + CHECK(has(r, "local busy = anyOf({ 1 })")); + CHECK(has(r, "while not busy do")); + } + { + INFO("stepped"); + CHECK(has(r, "local count = 0")); + CHECK(has(r, "count += 1")); + } + UNSCOPED_INFO("printed"); + CHECK(has(r, "local shown = 1")); + { + INFO("more than TRUE, compared with it"); + CHECK(has(r, "local loose = 5")); + CHECK(has(r, "if loose == 1 then")); + } + UNSCOPED_INFO("set where the assignment is read"); + CHECK(has(r, "local y = 0")); + require_checks_clean(r); ALLSLToSLua::Options typed; typed.types = true; const ALLSLToSLua::Result t = ALLSLToSLua::convert("integer ready(integer n) { return n > 2; }\n" "default { state_entry() { integer ok = ready(3); if (ok) llOwnerSay(\"yes\"); } }\n", typed); - ensure("typed boolean: " + t.text, has(t, "local function ready(n: number): boolean") && has(t, "local ok: boolean = ready(3)")); - checksClean(t); + CAPTURE(t.text); + { + INFO("typed boolean"); + CHECK(has(t, "local function ready(n: number): boolean")); + CHECK(has(t, "local ok: boolean = ready(3)")); + } + require_checks_clean(t); } - template<> template<> - void allsltoslua_object::test<13>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: the right-first memory hack is an append; a visible order is noted", "[ALLSLToSLua]") { - set_test_name("LSL ran a binary's right side first: (l = []) + l is the append it meant, and where the order could show it is noted"); - const ALLSLToSLua::Result r = convert("list gItems;\n" - "string gLog = \"a\";\n" - "default { touch_start(integer n) {\n" - " gItems = (gItems = []) + gItems + [\"x\"];\n" - " gItems = (gItems = []) + [\"y\"] + gItems;\n" - " gLog = (gLog = \"\") + gLog + \"b\";\n" - " llOwnerSay(gLog + (string)llGetListLength(gItems));\n" - "} }\n"); - ensure("appended: " + r.text, has(r, "table.insert(gItems, \"x\")")); - ensure("prepended: " + r.text, has(r, "table.insert(gItems, 1, \"y\")")); - ensure("a string: " + r.text, has(r, "gLog ..= \"b\"")); - ensure("nothing cleared first: " + r.text, !has(r, "gItems = {} return") && !has(r, "gLog = \"\" return")); - ensure("noted once for each variable: " + r.text, noted(r, "SluaMemoryHack") && count(r, "-- LSL: (gItems = []) + gItems") == 1 && - count(r, "-- LSL: (gLog = \"\") + gLog") == 1); - ensure("not taken as an order that shows", !orderNoted(r)); - checksClean(r); - - const ALLSLToSLua::Result order = convert("integer gCount;\n" - "integer bump() { return ++gCount; }\n" - "default { state_entry() {\n" - " integer i = 1;\n" - " integer j = i + (i = 5);\n" - " integer k = gCount - bump();\n" - " llOwnerSay((string)(j + k));\n" - "} }\n"); - ensure("an assignment the other side reads, named", noted(order, "SluaRightFirstSet", "i")); - ensure("a call that sets what the other side reads, named", noted(order, "SluaRightFirstCall", "bump()")); - ensure("said over its line, with what: " + order.text, has(order, "-- LSL: LSL ran the right side of this before the left, and Luau " - "runs the left first: bump() sets gCount, which the other side " - "reads or sets too.")); - ensure("said over its line: " + order.text, has(order, "-- LSL: LSL ran the right side of this before the left")); - checksClean(order); - - const ALLSLToSLua::Result quiet = convert("integer gCount;\n" - "integer bump() { return ++gCount; }\n" - "default { state_entry() {\n" - " integer i = 1;\n" - " integer j = (i + 1) * bump();\n" - " string s = \"n: \" + (string)bump();\n" - " llOwnerSay(s + (string)(j + (integer)llGetTime()));\n" - "} }\n"); - ensure("nothing either side changes the other reads: " + quiet.text, !orderNoted(quiet)); - checksClean(quiet); - } - - template<> template<> - void allsltoslua_object::test<14>() - { - set_test_name("lists no other holds grown in place: appended, prepended, by a list's name, by a call, several at once, passed to a function that only reads it; a list handed on keeps LSL's copy, and says why"); - const ALLSLToSLua::Result r = convert("list gAll;\n" - "list gSeen;\n" - "list gPassed;\n" - "list gStored;\n" - "list gGrown;\n" - "integer addOne() { gGrown += [1]; return 1; }\n" - "list gKept = [1];\n" - "list parts(string s) { list out = llParseString2List(s, [\",\"], []); out += [\"end\"]; return out; }\n" - "list kept() { return gKept; }\n" - "show(list l) { llOwnerSay(llDumpList2String(l, \",\")); }\n" - "showAll(list l) { show(l); }\n" - "keep(list l) { gStored = l; }\n" - "keepLater(list l) { keep(l); }\n" - "default { touch_start(integer n) {\n" - " gAll = parts(\"a,b\");\n" - " gAll += parts(\"c\");\n" - " gAll = [\"first\"] + gAll;\n" - " list more = [4, 5];\n" - " gAll += more;\n" - " gAll += [llGetTime(), llFrand(1.0)];\n" - " gSeen += [n];\n" - " showAll(gSeen);\n" - " gPassed += [n];\n" - " keepLater(gPassed);\n" - " gKept += [2];\n" - " list a = [1]; list b = a; a += [2];\n" - " show(llListInsertList(gGrown, [addOne()], 0));\n" - " show(kept() + b + a + gAll + gStored);\n" - "} }\n"); - ensure("a local returned, grown in place: " + r.text, has(r, "local out = ll.ParseString2List(s, { \",\" }, {})") && has(r, "table.insert(out, \"end\")")); - ensure("a call's list, put on the end: " + r.text, has(r, "gAll = parts(\"a,b\")") && has(r, "table.extend(gAll, parts(\"c\"))")); - ensure("prepended: " + r.text, has(r, "table.insert(gAll, 1, \"first\")")); - ensure("a list by its name: " + r.text, has(r, "table.extend(gAll, more)")); - ensure("several that do not run apart, each had before any is added: " + r.text, has(r, "table.append(gAll, ll.GetTime(), ll.Frand(1.0))")); - ensure("passed to a function that only reads it: " + r.text, has(r, "table.insert(gSeen, n)")); - ensure("passed to one that keeps it: " + r.text, - has(r, "gPassed = table.extend(table.clone(gPassed), { n })") && - has(r, "-- LSL: LSL's lists were values, and gPassed is passed to a function of the script's that keeps it")); - ensure("returned: " + r.text, has(r, "gKept = table.extend(table.clone(gKept), { 2 })") && has(r, "gKept is returned by a function")); - ensure("given to another: " + r.text, has(r, "a = table.extend(table.clone(a), { 2 })") && has(r, "a is given to another variable")); - ensure("read where a call in the statement grows it: " + r.text, - has(r, "gGrown = table.extend(table.clone(gGrown), { 1 })") && - has(r, "gGrown is read where a call of the script's in the same statement changes it")); - ensure("said once for each: " + r.text, count(r, "and gPassed is") == 1); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<15>() - { - set_test_name("strings built in loops: their pieces in a table, joined once after the loop that builds them; noted where they cannot be"); - const ALLSLToSLua::Result r = convert("string gReport;\n" - "default { touch_start(integer n) {\n" - " list names = [\"a\", \"b\", \"c\"];\n" - " integer outParts = 1;\n" - " string out = \"Names: \";\n" - " integer i;\n" - " for (i = 0; i < llGetListLength(names); ++i) {\n" - " string row = \"\";\n" - " integer j;\n" - " for (j = 0; j < 3; ++j) row += (string)j;\n" - " out = out + llList2String(names, i) + row;\n" - " }\n" - " string seen;\n" - " integer k;\n" - " for (k = 0; k < 3; ++k) { seen += \"x\"; if (llStringLength(seen) > 2) llOwnerSay(seen); }\n" - " for (k = 0; k < 2; ++k) gReport += \"y\";\n" - " llOwnerSay(out + seen + gReport + (string)outParts);\n" - "} }\n"); - ensure("the outer loop's table, a name of its own: " + r.text, has(r, "local outParts2 = {}\n for _, item in names do")); - ensure("the pieces put in: " + r.text, has(r, "table.insert(outParts2, item .. row)")); - ensure("joined after: " + r.text, has(r, " end\n out ..= table.concat(outParts2)")); - ensure("the inner loop's, declared in the outer: " + r.text, - has(r, " local rowParts = {}\n for j = 0, 2 do\n table.insert(rowParts, tostring(j))")); - ensure("declared empty just before it: declared where it is joined: " + r.text, - has(r, " end\n local row = table.concat(rowParts)") && !has(r, "local row = \"\"")); - ensure("declared with words of its own: added to: " + r.text, has(r, "local out = \"Names: \"") && has(r, "out ..= table.concat(outParts2)")); - ensure("read in its loop: noted: " + r.text, has(r, "seen ..= \"x\"") && has(r, "-- LSL: seen is built with .. in a loop")); - ensure("a global: noted: " + r.text, has(r, "gReport ..= \"y\"") && has(r, "-- LSL: gReport is built with .. in a loop")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted("list gItems;\n" + "string gLog = \"a\";\n" + "default { touch_start(integer n) {\n" + " gItems = (gItems = []) + gItems + [\"x\"];\n" + " gItems = (gItems = []) + [\"y\"] + gItems;\n" + " gLog = (gLog = \"\") + gLog + \"b\";\n" + " llOwnerSay(gLog + (string)llGetListLength(gItems));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("appended"); + CHECK(has(r, "table.insert(gItems, \"x\")")); + UNSCOPED_INFO("prepended"); + CHECK(has(r, "table.insert(gItems, 1, \"y\")")); + UNSCOPED_INFO("a string"); + CHECK(has(r, "gLog ..= \"b\"")); + { + INFO("nothing cleared first"); + CHECK_FALSE(has(r, "gItems = {} return")); + CHECK_FALSE(has(r, "gLog = \"\" return")); + } + { + INFO("noted once for each variable"); + CHECK(noted(r, "SluaMemoryHack")); + CHECK(count(r, "-- LSL: (gItems = []) + gItems") == 1); + CHECK(count(r, "-- LSL: (gLog = \"\") + gLog") == 1); + } + UNSCOPED_INFO("not taken as an order that shows"); + CHECK_FALSE(orderNoted(r)); + require_checks_clean(r); + + const ALLSLToSLua::Result order = require_converted("integer gCount;\n" + "integer bump() { return ++gCount; }\n" + "default { state_entry() {\n" + " integer i = 1;\n" + " integer j = i + (i = 5);\n" + " integer k = gCount - bump();\n" + " llOwnerSay((string)(j + k));\n" + "} }\n"); + CAPTURE(order.text); + UNSCOPED_INFO("an assignment the other side reads, named"); + CHECK(noted(order, "SluaRightFirstSet", "i")); + UNSCOPED_INFO("a call that sets what the other side reads, named"); + CHECK(noted(order, "SluaRightFirstCall", "bump()")); + UNSCOPED_INFO("said over its line, with what"); + CHECK(has(order, "-- LSL: LSL ran the right side of this before the left, and Luau " + "runs the left first: bump() sets gCount, which the other side " + "reads or sets too.")); + UNSCOPED_INFO("said over its line"); + CHECK(has(order, "-- LSL: LSL ran the right side of this before the left")); + require_checks_clean(order); + + const ALLSLToSLua::Result quiet = require_converted("integer gCount;\n" + "integer bump() { return ++gCount; }\n" + "default { state_entry() {\n" + " integer i = 1;\n" + " integer j = (i + 1) * bump();\n" + " string s = \"n: \" + (string)bump();\n" + " llOwnerSay(s + (string)(j + (integer)llGetTime()));\n" + "} }\n"); + CAPTURE(quiet.text); + UNSCOPED_INFO("nothing either side changes the other reads"); + CHECK_FALSE(orderNoted(quiet)); + require_checks_clean(quiet); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: unshared lists grown in place; a list handed on copied and noted", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("list gAll;\n" + "list gSeen;\n" + "list gPassed;\n" + "list gStored;\n" + "list gGrown;\n" + "integer addOne() { gGrown += [1]; return 1; }\n" + "list gKept = [1];\n" + "list parts(string s) { list out = llParseString2List(s, [\",\"], []); out += [\"end\"]; return out; }\n" + "list kept() { return gKept; }\n" + "show(list l) { llOwnerSay(llDumpList2String(l, \",\")); }\n" + "showAll(list l) { show(l); }\n" + "keep(list l) { gStored = l; }\n" + "keepLater(list l) { keep(l); }\n" + "default { touch_start(integer n) {\n" + " gAll = parts(\"a,b\");\n" + " gAll += parts(\"c\");\n" + " gAll = [\"first\"] + gAll;\n" + " list more = [4, 5];\n" + " gAll += more;\n" + " gAll += [llGetTime(), llFrand(1.0)];\n" + " gSeen += [n];\n" + " showAll(gSeen);\n" + " gPassed += [n];\n" + " keepLater(gPassed);\n" + " gKept += [2];\n" + " list a = [1]; list b = a; a += [2];\n" + " show(llListInsertList(gGrown, [addOne()], 0));\n" + " show(kept() + b + a + gAll + gStored);\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("a local returned, grown in place"); + CHECK(has(r, "local out = ll.ParseString2List(s, { \",\" }, {})")); + CHECK(has(r, "table.insert(out, \"end\")")); + } + { + INFO("a call's list, put on the end"); + CHECK(has(r, "gAll = parts(\"a,b\")")); + CHECK(has(r, "table.extend(gAll, parts(\"c\"))")); + } + UNSCOPED_INFO("prepended"); + CHECK(has(r, "table.insert(gAll, 1, \"first\")")); + UNSCOPED_INFO("a list by its name"); + CHECK(has(r, "table.extend(gAll, more)")); + UNSCOPED_INFO("several that do not run apart, each had before any is added"); + CHECK(has(r, "table.append(gAll, ll.GetTime(), ll.Frand(1.0))")); + UNSCOPED_INFO("passed to a function that only reads it"); + CHECK(has(r, "table.insert(gSeen, n)")); + { + INFO("passed to one that keeps it"); + CHECK(has(r, "gPassed = table.extend(table.clone(gPassed), { n })")); + CHECK(has(r, "-- LSL: LSL's lists were values, and gPassed is passed to a function of the script's that keeps it")); + } + { + INFO("returned"); + CHECK(has(r, "gKept = table.extend(table.clone(gKept), { 2 })")); + CHECK(has(r, "gKept is returned by a function")); + } + { + INFO("given to another"); + CHECK(has(r, "a = table.extend(table.clone(a), { 2 })")); + CHECK(has(r, "a is given to another variable")); + } + { + INFO("read where a call in the statement grows it"); + CHECK(has(r, "gGrown = table.extend(table.clone(gGrown), { 1 })")); + CHECK(has(r, "gGrown is read where a call of the script's in the same statement changes it")); + } + UNSCOPED_INFO("said once for each"); + CHECK(count(r, "and gPassed is") == 1); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: strings built in loops joined once from a table; noted where not", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("string gReport;\n" + "default { touch_start(integer n) {\n" + " list names = [\"a\", \"b\", \"c\"];\n" + " integer outParts = 1;\n" + " string out = \"Names: \";\n" + " integer i;\n" + " for (i = 0; i < llGetListLength(names); ++i) {\n" + " string row = \"\";\n" + " integer j;\n" + " for (j = 0; j < 3; ++j) row += (string)j;\n" + " out = out + llList2String(names, i) + row;\n" + " }\n" + " string seen;\n" + " integer k;\n" + " for (k = 0; k < 3; ++k) { seen += \"x\"; if (llStringLength(seen) > 2) llOwnerSay(seen); }\n" + " for (k = 0; k < 2; ++k) gReport += \"y\";\n" + " llOwnerSay(out + seen + gReport + (string)outParts);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("the outer loop's table, a name of its own"); + CHECK(has(r, "local outParts2 = {}\n for _, item in names do")); + UNSCOPED_INFO("the pieces put in"); + CHECK(has(r, "table.insert(outParts2, item .. row)")); + UNSCOPED_INFO("joined after"); + CHECK(has(r, " end\n out ..= table.concat(outParts2)")); + UNSCOPED_INFO("the inner loop's, declared in the outer"); + CHECK(has(r, " local rowParts = {}\n for j = 0, 2 do\n table.insert(rowParts, tostring(j))")); + { + INFO("declared empty just before it: declared where it is joined"); + CHECK(has(r, " end\n local row = table.concat(rowParts)")); + CHECK_FALSE(has(r, "local row = \"\"")); + } + { + INFO("declared with words of its own: added to"); + CHECK(has(r, "local out = \"Names: \"")); + CHECK(has(r, "out ..= table.concat(outParts2)")); + } + { + INFO("read in its loop: noted"); + CHECK(has(r, "seen ..= \"x\"")); + CHECK(has(r, "-- LSL: seen is built with .. in a loop")); + } + { + INFO("a global: noted"); + CHECK(has(r, "gReport ..= \"y\"")); + CHECK(has(r, "-- LSL: gReport is built with .. in a loop")); + } + require_checks_clean(r); ALLSLToSLua::Options typed; typed.types = true; const ALLSLToSLua::Result t = ALLSLToSLua::convert("default { state_entry() { string s; integer i; for (i = 0; i < 3; ++i) s += \"z\"; llOwnerSay(s); } }\n", typed); - ensure("typed: " + t.text, has(t, "local sParts: { string } = {}") && has(t, "local s: string = table.concat(sParts)")); - checksClean(t); + CAPTURE(t.text); + CHECK(has(t, "local sParts: { string } = {}")); + CHECK(has(t, "local s: string = table.concat(sParts)")); + require_checks_clean(t); - const ALLSLToSLua::Result read = convert("default { state_entry() { string t = \"\"; llOwnerSay(t); integer i; for (i = 0; i < 3; ++i) t += \"q\"; llOwnerSay(t); } }\n"); - ensure("read between: declared where it was: " + read.text, has(read, "local t = \"\"") && has(read, "t ..= table.concat(tParts)")); - checksClean(read); + const ALLSLToSLua::Result read = require_converted("default { state_entry() { string t = \"\"; llOwnerSay(t); integer i; for (i = 0; i < 3; ++i) t += \"q\"; llOwnerSay(t); } }\n"); + CAPTURE(read.text); + { + INFO("read between: declared where it was"); + CHECK(has(read, "local t = \"\"")); + CHECK(has(read, "t ..= table.concat(tParts)")); + } + require_checks_clean(read); } - template<> template<> - void allsltoslua_object::test<16>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: compound assignments: x op= y; integer /= and %= whole; vector %= cross", "[ALLSLToSLua]") { - set_test_name("x = x op y as Luau's own compound assignments; an integer's /= and %= written whole, as / and % are; a vector's %= its cross product"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " integer a = 7; float f = 3.0; vector v = <1, 0, 0>;\n" - " a /= 2; a %= 3; f /= 2.0; v %= <0, 1, 0>;\n" - " integer b = 1; integer c = 2; string s = \"x\";\n" - " b = b + c; b = b - (c - 1); b = b - c - 1; c = c * 2 + b; s = s + \"y\" + (string)b;\n" - " llOwnerSay((string)a + (string)f + (string)v + s + (string)(b + c));\n" - "} }\n"); - ensure("integer division: " + r.text, has(r, "a = bit32.s32(a / 2)") && !has(r, "//=")); - ensure("remainder: " + r.text, has(r, "a = math.fmod(a, 3)") && !has(r, "%=")); - ensure("a float's: " + r.text, has(r, "f /= 2.0")); - ensure("a vector's cross product: " + r.text, has(r, "v = vector.cross(v, vector(0, 1, 0))")); - ensure("x = x op y: " + r.text, has(r, "b += c") && has(r, "b -= c - 1")); - ensure("not where the left of the operator is more than x: " + r.text, has(r, "b = b - c - 1") && has(r, "c = c * 2 + b")); - ensure("a string's pieces, joined: " + r.text, has(r, "s ..= `y{b}`")); - ensure("a float's and a vector's as LSL writes them, in one interpolated string: " + r.text, - has(r, "print(`{a}{string.format(\"%.6f\", f)}{ll.DumpList2String({ v }, \"\")}{s}{b + c}`)")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<17>() - { - set_test_name("a minus before a negative number is bracketed, not a comment"); - const ALLSLToSLua::Result r = convert("default { state_entry() { integer a = -5; llOwnerSay((string)(-(-2147483648)) + (string)(-a) + (string)(- -a)); } }\n"); - ensure("bracketed: " + r.text, has(r, "-(-2147483648)") && !has(r, "--2147483648")); - ensure("a name as it was: " + r.text, has(r, "}{-a}{")); - ensure("twice: " + r.text, has(r, "-(-a)")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<18>() - { - set_test_name("a script of one state: its state_exit, which never ran and SLua has no event for, left out and said"); - const ALLSLToSLua::Result r = convert("default { state_entry() { llOwnerSay(\"in\"); } state_exit() { llOwnerSay(\"out\"); } touch_start(integer n) { } }\n"); - ensure("not an event: " + r.text, !has(r, "\"state_exit\"") && !has(r, "print(\"out\")")); - ensure("said where it was: " + r.text, noted(r, "SluaStateExit") && has(r, "-- LSL: state_exit runs as a script leaves a state") && - has(r, "-- state_exit, left out")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<19>() - { - set_test_name("a function SLua has nowhere left out and said; one whose ll list has booleans for LSL's 1 and 0 kept on llcompat"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " llPointAt(ZERO_VECTOR);\n" - " list e = llGetExperienceList(NULL_KEY);\n" - " if (llList2Integer(llGetPrimitiveParams([PRIM_FULLBRIGHT, 0]), 0)) llOwnerSay((string)llGetListLength(e));\n" - "} }\n"); - ensure("left out: " + r.text, !has(r, "PointAt(") && has(r, "-- llPointAt, left out") && noted(r, "SluaAbsent", "llPointAt")); - ensure("its empty value where read: " + r.text, has(r, "local e = {}") && noted(r, "SluaAbsent", "llGetExperienceList")); - ensure("llcompat, said: " + r.text, has(r, "llcompat.GetPrimitiveParams({ PRIM_FULLBRIGHT, 0 })") && noted(r, "SluaBoolList", "GetPrimitiveParams")); - ensure("not a truth: " + r.text, !has(r, "if llcompat.GetPrimitiveParams")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " integer a = 7; float f = 3.0; vector v = <1, 0, 0>;\n" + " a /= 2; a %= 3; f /= 2.0; v %= <0, 1, 0>;\n" + " integer b = 1; integer c = 2; string s = \"x\";\n" + " b = b + c; b = b - (c - 1); b = b - c - 1; c = c * 2 + b; s = s + \"y\" + (string)b;\n" + " llOwnerSay((string)a + (string)f + (string)v + s + (string)(b + c));\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("integer division"); + CHECK(has(r, "a = bit32.s32(a / 2)")); + CHECK_FALSE(has(r, "//=")); + } + { + INFO("remainder"); + CHECK(has(r, "a = math.fmod(a, 3)")); + CHECK_FALSE(has(r, "%=")); + } + UNSCOPED_INFO("a float's"); + CHECK(has(r, "f /= 2.0")); + UNSCOPED_INFO("a vector's cross product"); + CHECK(has(r, "v = vector.cross(v, vector(0, 1, 0))")); + { + INFO("x = x op y"); + CHECK(has(r, "b += c")); + CHECK(has(r, "b -= c - 1")); + } + { + INFO("not where the left of the operator is more than x"); + CHECK(has(r, "b = b - c - 1")); + CHECK(has(r, "c = c * 2 + b")); + } + UNSCOPED_INFO("a string's pieces, joined"); + CHECK(has(r, "s ..= `y{b}`")); + UNSCOPED_INFO("a float's and a vector's as LSL writes them, in one interpolated string"); + CHECK(has(r, "print(`{a}{string.format(\"%.6f\", f)}{ll.DumpList2String({ v }, \"\")}{s}{b + c}`)")); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<20>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a minus before a negative number is bracketed and not a comment", "[ALLSLToSLua]") { - set_test_name("NULL_KEY and the constants LSL types a string and SLua a uuid: a key where compared with one or given as one, LSL's string where read as one"); - const ALLSLToSLua::Result r = convert("string gName = NULL_KEY;\n" - "key gKey = NULL_KEY;\n" - "string nothing() { return NULL_KEY; }\n" - "default { touch_start(integer n) {\n" - " key k = llDetectedKey(0);\n" - " if (k != NULL_KEY) llOwnerSay(\"someone\");\n" - " if (NULL_KEY != \"\") llOwnerSay(llGetSubString(NULL_KEY, 0, 7));\n" - " list l = [NULL_KEY, TEXTURE_BLANK];\n" - " if (NULL_KEY) llOwnerSay(gName + nothing() + (string)gKey + (string)llGetListLength(l));\n" - "} }\n"); - ensure("a key against a key: " + r.text, has(r, "if k ~= NULL_KEY then")); - ensure("a string where LSL's string was: " + r.text, has(r, "tostring(NULL_KEY) ~= \"\"") && has(r, "ll.GetSubString(tostring(NULL_KEY), 1, 8)")); - ensure("in a list, as LSL had it: " + r.text, has(r, "{ tostring(NULL_KEY), tostring(TEXTURE_BLANK) }")); - ensure("a string's truth: " + r.text, has(r, "if tostring(NULL_KEY) ~= \"\" then print(")); - ensure("given to a string and a key: " + r.text, has(r, "local gName = tostring(NULL_KEY)") && has(r, "local gKey = NULL_KEY")); - ensure("returned as a string function's: " + r.text, has(r, "return tostring(NULL_KEY)")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted("default { state_entry() { integer a = -5; llOwnerSay((string)(-(-2147483648)) + (string)(-a) + (string)(- -a)); } }\n"); + CAPTURE(r.text); + { + INFO("bracketed"); + CHECK(has(r, "-(-2147483648)")); + CHECK_FALSE(has(r, "--2147483648")); + } + UNSCOPED_INFO("a name as it was"); + CHECK(has(r, "}{-a}{")); + UNSCOPED_INFO("twice"); + CHECK(has(r, "-(-a)")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a one-state script's state_exit never ran: left out and said", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() { llOwnerSay(\"in\"); } state_exit() { llOwnerSay(\"out\"); } touch_start(integer n) { } }\n"); + CAPTURE(r.text); + { + INFO("not an event"); + CHECK_FALSE(has(r, "\"state_exit\"")); + CHECK_FALSE(has(r, "print(\"out\")")); + } + { + INFO("said where it was"); + CHECK(noted(r, "SluaStateExit")); + CHECK(has(r, "-- LSL: state_exit runs as a script leaves a state")); + CHECK(has(r, "-- state_exit, left out")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a function SLua lacks left out and said; boolean ll lists kept on llcompat", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " llPointAt(ZERO_VECTOR);\n" + " list e = llGetExperienceList(NULL_KEY);\n" + " if (llList2Integer(llGetPrimitiveParams([PRIM_FULLBRIGHT, 0]), 0)) llOwnerSay((string)llGetListLength(e));\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("left out"); + CHECK_FALSE(has(r, "PointAt(")); + CHECK(has(r, "-- llPointAt, left out")); + CHECK(noted(r, "SluaAbsent", "llPointAt")); + } + { + INFO("its empty value where read"); + CHECK(has(r, "local e = {}")); + CHECK(noted(r, "SluaAbsent", "llGetExperienceList")); + } + { + INFO("llcompat, said"); + CHECK(has(r, "llcompat.GetPrimitiveParams({ PRIM_FULLBRIGHT, 0 })")); + CHECK(noted(r, "SluaBoolList", "GetPrimitiveParams")); + } + UNSCOPED_INFO("not a truth"); + CHECK_FALSE(has(r, "if llcompat.GetPrimitiveParams")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: NULL_KEY and such constants: a uuid as a key and a string as text", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("string gName = NULL_KEY;\n" + "key gKey = NULL_KEY;\n" + "string nothing() { return NULL_KEY; }\n" + "default { touch_start(integer n) {\n" + " key k = llDetectedKey(0);\n" + " if (k != NULL_KEY) llOwnerSay(\"someone\");\n" + " if (NULL_KEY != \"\") llOwnerSay(llGetSubString(NULL_KEY, 0, 7));\n" + " list l = [NULL_KEY, TEXTURE_BLANK];\n" + " if (NULL_KEY) llOwnerSay(gName + nothing() + (string)gKey + (string)llGetListLength(l));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a key against a key"); + CHECK(has(r, "if k ~= NULL_KEY then")); + { + INFO("a string where LSL's string was"); + CHECK(has(r, "tostring(NULL_KEY) ~= \"\"")); + CHECK(has(r, "ll.GetSubString(tostring(NULL_KEY), 1, 8)")); + } + UNSCOPED_INFO("in a list, as LSL had it"); + CHECK(has(r, "{ tostring(NULL_KEY), tostring(TEXTURE_BLANK) }")); + UNSCOPED_INFO("a string's truth"); + CHECK(has(r, "if tostring(NULL_KEY) ~= \"\" then print(")); + { + INFO("given to a string and a key"); + CHECK(has(r, "local gName = tostring(NULL_KEY)")); + CHECK(has(r, "local gKey = NULL_KEY")); + } + UNSCOPED_INFO("returned as a string function's"); + CHECK(has(r, "return tostring(NULL_KEY)")); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<21>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: Tailslide's test scripts convert and check clean or are refused", "[ALLSLToSLua]") { - set_test_name("Tailslide's own test scripts: every one it takes converts and checks as SLua, and each it refuses is refused"); // Those Tailslide says are wrong LSL, by design. static const std::set REFUSED = { "bad_globals.lsl", "bugs/0002.lsl", "bugs/0003.lsl", "bugs/0006.lsl", "bugs/0007.lsl", "bugs/0010.lsl", "bugs/0015.lsl", "bugs/0017.lsl", "bugs/0019.lsl", "bugs/bad-for-exprs.lsl", @@ -816,7 +1106,8 @@ namespace tut } } std::sort(files.begin(), files.end()); - ensure("the scripts: " + dir, files.size() > 100); + CAPTURE(dir); + REQUIRE(files.size() > 100); std::string wrong; size_t converted = 0; for (const std::string& name : files) @@ -845,217 +1136,282 @@ namespace tut } } } - ensure("every one as it should be:\n" + wrong, wrong.empty()); - ensure_equals("those taken, converted", converted + REFUSED.size(), files.size()); + CAPTURE(wrong); + CHECK(wrong.empty()); + UNSCOPED_INFO("those taken, converted"); + CHECK(converted + REFUSED.size() == files.size()); } - template<> template<> - void allsltoslua_object::test<22>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: deprecated ll: SLua's own way where it means the same or llcompat noted", "[ALLSLToSLua]") { - set_test_name("what SLua deprecates: its own way where it means the same -- math.floor(x + 0.5), utf8.len, an index read as LSL's -- and llcompat, with SLua's word, where not"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " string s = \"a:b\"; list l = [\"x\", 2];\n" - " integer at = llSubStringIndex(s, \":\");\n" - " integer n = llStringLength(s) + llRound(1.5);\n" - " llOwnerSay(llList2String(l, at) + (string)(at + n));\n" - "} }\n"); - ensure("an index read as LSL's: " + r.text, has(r, "local at = (ll.SubStringIndex(s, \":\") or 0) - 1")); - ensure("utf8.len and half up: " + r.text, has(r, "local n = (utf8.len(s) :: number) + math.floor(1.5 + 0.5)")); - ensure("llcompat, with SLua's word: " + r.text, - has(r, "llcompat.List2String(l, at)") && has(r, "-- LSL: SLua deprecates ll.List2String: Use '[]' and 'tostring' instead.")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<23>() - { - set_test_name("keys that hold text: link_message's id as SLua passes it, a string; a key given text that is no UUID kept a string; text where SLua takes it, ll's key too"); - const ALLSLToSLua::Result r = convert("key DOMAIN = \"MY CHANNEL\";\n" - "key gOwner;\n" - "send(key to) { llMessageLinked(LINK_SET, 0, \"\", to); }\n" - "default {\n" - " state_entry() {\n" - " gOwner = llGetOwner();\n" - " llMessageLinked(LINK_SET, 1, \"hi\", DOMAIN);\n" - " send(\"other text\");\n" - " llMessageLinked(LINK_SET, 2, \"x\", gOwner);\n" - " }\n" - " link_message(integer s, integer n, string m, key id) {\n" - " if (id == DOMAIN) llOwnerSay(m);\n" - " if (id == gOwner) llOwnerSay(\"owner\");\n" - " key k = id;\n" - " llOwnerSay((string)llGetOwnerKey(\"abc\") + (string)k);\n" - " }\n" - "}\n"); - ensure("a key given text, a string: " + r.text, has(r, "local DOMAIN = \"MY CHANNEL\"")); - ensure("text where SLua takes it: " + r.text, - has(r, "ll.MessageLinked(LINK_SET, 1, \"hi\", DOMAIN)") && has(r, "ll.MessageLinked(LINK_SET, 2, \"x\", gOwner)") && - has(r, "send(\"other text\")") && has(r, "ll.MessageLinked(LINK_SET, 0, \"\", to)")); - ensure("compared as text: " + r.text, has(r, "if id == DOMAIN then") && has(r, "if id == tostring(gOwner) then")); - ensure("a key given the id, text: " + r.text, has(r, "local k = id") && has(r, "{k}`)")); - ensure("ll's key given text as it is, said: " + r.text, - has(r, "ll.GetOwnerKey(\"abc\" :: any)") && noted(r, "SluaKeyAsText") && !noted(r, "SluaKeyText")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<24>() - { - set_test_name("a note only where it says something: && not over a read, a deprecated call's reason rather than its indexes; bit32's range made LSL's, not for an & with a flag nor a truth"); - const ALLSLToSLua::Result r = convert("integer sayIt() { llOwnerSay(\"x\"); return 1; }\n" - "default { changed(integer what) {\n" - " if (what & CHANGED_LINK && llGetLinkNumber() == 0) llDie();\n" - " integer m = what | 0x80000000;\n" - " list l = [\"a\"];\n" - " llOwnerSay(llList2String(l, what) + (string)m);\n" - " if ((what & 3) && sayIt()) llDie();\n" - "} }\n"); - ensure("bit32's range made LSL's where it shows: " + r.text, has(r, "local m = bit32.s32(bit32.bor(what, ") && count(r, "-- LSL: bit32") == 1); - ensure("not for a flag's truth: " + r.text, has(r, "if bit32.btest(what, CHANGED_LINK)")); - ensure("&& said once, over a call that does something: " + r.text, - count(r, "leaves its right side unrun") == 1 && has(r, "LSL ran both sides, the right one first.\n if bit32.btest(what, 3) and sayIt()")); - ensure("the deprecated call's reason only: " + r.text, has(r, "-- LSL: SLua deprecates ll.List2String") && !has(r, "takes indexes from 0")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<25>() - { - set_test_name("flatter expressions: text joined in a chain without brackets, and an &, | or ^ of the same again one bit32 call"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " string b = \"b\"; integer n = 3; integer f = 5;\n" - " llOwnerSay(\"a\" + b + \"c\" + (string)n);\n" - " llSetTextureAnim(ANIM_ON | ROTATE | LOOP, ALL_SIDES, 0, 0, 0, TWO_PI, 1.0);\n" - " llOwnerSay((string)((f | 2) & n));\n" - "} }\n"); - ensure("a chain, one string: " + r.text, has(r, "print(`a{b}c{n}`)")); - ensure("one call: " + r.text, has(r, "ll.SetTextureAnim(bit32.bor(ANIM_ON, ROTATE, LOOP), ALL_SIDES")); - ensure("not across operators: " + r.text, has(r, "bit32.band(bit32.bor(f, 2), n)")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<26>() - { - set_test_name("steps as statements: x++ after its statement, ++x and x = v before it, where nothing else in it reads x; kept in place where something could"); - const ALLSLToSLua::Result r = convert("integer gLine;\n" - "integer bump() { return ++gLine; }\n" - "default { state_entry() {\n" - " integer n = 0;\n" - " llSay(n++, \"a\");\n" - " integer m = ++n;\n" - " integer k = n++ + n;\n" - " llOwnerSay((string)(gLine++));\n" - " llOwnerSay((string)bump() + (string)(gLine++) + (string)(m + k));\n" - "} }\n"); - ensure("after: " + r.text, has(r, "ll.Say(n, \"a\")\nn += 1\n")); - ensure("before: " + r.text, has(r, "\nn += 1\nlocal m = n\n")); - ensure("read again, kept: " + r.text, has(r, "local k = (function() local was = n; n += 1 return was end)() + n")); - ensure("a global with nothing of the script's called: " + r.text, has(r, "print(tostring(gLine))\ngLine += 1\n")); - ensure("a global beside a call of the script's, kept: " + r.text, has(r, "print(`{bump()}{(function() local was = gLine;")); - ensure("a return's step before it: " + r.text, has(r, " gLine += 1\n return gLine\n")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " string s = \"a:b\"; list l = [\"x\", 2];\n" + " integer at = llSubStringIndex(s, \":\");\n" + " integer n = llStringLength(s) + llRound(1.5);\n" + " llOwnerSay(llList2String(l, at) + (string)(at + n));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("an index read as LSL's"); + CHECK(has(r, "local at = (ll.SubStringIndex(s, \":\") or 0) - 1")); + UNSCOPED_INFO("utf8.len and half up"); + CHECK(has(r, "local n = (utf8.len(s) :: number) + math.floor(1.5 + 0.5)")); + { + INFO("llcompat, with SLua's word"); + CHECK(has(r, "llcompat.List2String(l, at)")); + CHECK(has(r, "-- LSL: SLua deprecates ll.List2String: Use '[]' and 'tostring' instead.")); + } + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<27>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: keys that hold text stay strings: link_message's id and non-UUID text", "[ALLSLToSLua]") { - set_test_name("simpler conditions: !(a == b) as a ~= b, a boolean given TRUE or FALSE by an if as the check itself"); - const ALLSLToSLua::Result r = convert("integer gOn;\n" - "default { touch_start(integer n) {\n" - " if (!(n == 2)) llOwnerSay(\"not two\");\n" - " if (!(n != 3)) llOwnerSay(\"three\");\n" - " if (n > 1) gOn = TRUE; else gOn = FALSE;\n" - " if (n > 5) { gOn = FALSE; } else { gOn = TRUE; }\n" - " if (gOn) llOwnerSay(\"on\");\n" - "} }\n"); - ensure("flipped: " + r.text, has(r, "if n ~= 2 then") && has(r, "if n == 3 then")); - const ALLSLToSLua::Result nots = convert("integer gOn;\n" - "default { touch_start(integer n) {\n" - " if (!n) llOwnerSay(\"none\"); if (!(n & 4)) llOwnerSay(\"no four\"); if (!(n > 1)) llDie();\n" - " gOn = !gOn; if (gOn) llOwnerSay(\"on\");\n" - "} }\n"); - ensure("not a number: the other way round: " + nots.text, - has(nots, "if n == 0 then") && has(nots, "if not bit32.btest(n, 4) then") && has(nots, "if not (n > 1) then") && - has(nots, "gOn = not gOn")); - checksClean(nots); - ensure("the check itself: " + r.text, has(r, " gOn = n > 1\n") && has(r, " gOn = not (n > 5)\n")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<28>() - { - set_test_name("a counter never below nought is an index ll takes moved on by one, and a loop whose counter is only such an index counts from 1"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " integer n = llGetInventoryNumber(INVENTORY_SOUND);\n" - " integer i; for (i = 0; i < n; ++i) llOwnerSay(llGetInventoryName(INVENTORY_SOUND, i));\n" - " integer j; for (j = 0; j < n; ++j) llOwnerSay((string)j + llGetInventoryName(INVENTORY_SOUND, j));\n" - " integer k; for (k = n - 1; k >= 0; --k) llOwnerSay(llGetInventoryName(INVENTORY_SOUND, k));\n" - " string s = \"abcdef\";\n" - " integer c; for (c = 0; c <= 2; ++c) llOwnerSay(llGetSubString(s, c, c + 1));\n" - "} }\n"); - ensure("counting from 1: " + r.text, has(r, "for i = 1, n do\n print(ll.GetInventoryName(INVENTORY_SOUND, i))")); - ensure("read as a number too, moved on by one: " + r.text, - has(r, "for j = 0, n - 1 do\n print(`{j}{ll.GetInventoryName(INVENTORY_SOUND, j + 1)}`)")); - ensure("down to nought: " + r.text, has(r, "for k = n - 1, 0, -1 do\n print(ll.GetInventoryName(INVENTORY_SOUND, k + 1))")); - ensure("to and with, a number added: " + r.text, has(r, "for c = 1, 3 do\n print(ll.GetSubString(s, c, c + 1))")); - ensure("no llcompat: " + r.text, !has(r, "llcompat.")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<29>() - { - set_test_name("a list's items read by index where they are already what is asked for: within its length as they are, else LSL's empty value past the end; tostring where that makes them so; llcompat where nothing does"); - const ALLSLToSLua::Result r = convert("list gNames = [\"a\", \"b\"];\n" - "list parts(string s) { return llParseString2List(s, [\",\"], []); }\n" - "default { state_entry() {\n" - " list p = parts(\"x,y\");\n" - " list mixed = [1, \"two\", 3.0];\n" - " list keys = [llGetOwner(), \"text\"];\n" - " integer n = 1;\n" - " llOwnerSay(llList2String(gNames, 0) + llList2String(p, -1) + llList2String(keys, 1));\n" - " llOwnerSay(llList2String(mixed, 2) + (string)llList2Integer(mixed, 0) + llList2String(gNames, n));\n" - " integer i; for (i = 0; i < llGetListLength(p); ++i) llOwnerSay(llList2String(p, i) + llGetSubString(\"abc\", i, i));\n" - "} }\n"); - ensure("from the start, empty past the end: " + r.text, has(r, "(gNames[1] or \"\")")); - ensure("back from the end, through a function's list: " + r.text, has(r, "(p[#p] or \"\")")); - ensure("keys and text made text: " + r.text, has(r, "tostring(keys[2] or \"\")")); - ensure("a float's text is not tostring's, and an index not known: llcompat: " + r.text, - has(r, "llcompat.List2String(mixed, 2)") && has(r, "llcompat.List2String(gNames, n)")); - ensure("mixed items for a number: llcompat: " + r.text, has(r, "llcompat.List2Integer(mixed, 0)")); - ensure("within its length, as it is, counting from 1: " + r.text, has(r, "for i = 1, #p do\n print(p[i] .. ll.GetSubString(\"abc\", i, i))")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<30>() - { - set_test_name("a list no other holds taken from or put to at its front or back in place: table.remove and table.insert; anywhere else, or one held elsewhere, as LSL made a new one"); - const ALLSLToSLua::Result r = convert("list gQueue;\n" - "list gShared;\n" - "keep(list l) { gShared = l; }\n" - "default { touch_start(integer n) {\n" - " gQueue += [n];\n" - " gQueue = llListInsertList(gQueue, [n + 1], 0);\n" - " gQueue = llDeleteSubList(gQueue, 0, 0);\n" - " gQueue = llDeleteSubList(gQueue, -1, -1);\n" - " gQueue = llDeleteSubList(gQueue, 1, 1);\n" - " keep(gShared);\n" - " gShared = llDeleteSubList(gShared, 0, 0);\n" - "} }\n"); - ensure("in place: " + r.text, has(r, "table.insert(gQueue, 1, n + 1)") && has(r, "table.remove(gQueue, 1)") && has(r, "table.remove(gQueue)\n")); - ensure("elsewhere in the list, as LSL did: " + r.text, has(r, "gQueue = llcompat.DeleteSubList(gQueue, 1, 1)")); - ensure("one held elsewhere, as LSL did: " + r.text, has(r, "gShared = llcompat.DeleteSubList(gShared, 0, 0)")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted("key DOMAIN = \"MY CHANNEL\";\n" + "key gOwner;\n" + "send(key to) { llMessageLinked(LINK_SET, 0, \"\", to); }\n" + "default {\n" + " state_entry() {\n" + " gOwner = llGetOwner();\n" + " llMessageLinked(LINK_SET, 1, \"hi\", DOMAIN);\n" + " send(\"other text\");\n" + " llMessageLinked(LINK_SET, 2, \"x\", gOwner);\n" + " }\n" + " link_message(integer s, integer n, string m, key id) {\n" + " if (id == DOMAIN) llOwnerSay(m);\n" + " if (id == gOwner) llOwnerSay(\"owner\");\n" + " key k = id;\n" + " llOwnerSay((string)llGetOwnerKey(\"abc\") + (string)k);\n" + " }\n" + "}\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a key given text, a string"); + CHECK(has(r, "local DOMAIN = \"MY CHANNEL\"")); + { + INFO("text where SLua takes it"); + CHECK(has(r, "ll.MessageLinked(LINK_SET, 1, \"hi\", DOMAIN)")); + CHECK(has(r, "ll.MessageLinked(LINK_SET, 2, \"x\", gOwner)")); + CHECK(has(r, "send(\"other text\")")); + CHECK(has(r, "ll.MessageLinked(LINK_SET, 0, \"\", to)")); + } + { + INFO("compared as text"); + CHECK(has(r, "if id == DOMAIN then")); + CHECK(has(r, "if id == tostring(gOwner) then")); + } + { + INFO("a key given the id, text"); + CHECK(has(r, "local k = id")); + CHECK(has(r, "{k}`)")); + } + { + INFO("ll's key given text as it is, said"); + CHECK(has(r, "ll.GetOwnerKey(\"abc\" :: any)")); + CHECK(noted(r, "SluaKeyAsText")); + CHECK_FALSE(noted(r, "SluaKeyText")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: notes only where they say something: &&; deprecations; bit32's range", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer sayIt() { llOwnerSay(\"x\"); return 1; }\n" + "default { changed(integer what) {\n" + " if (what & CHANGED_LINK && llGetLinkNumber() == 0) llDie();\n" + " integer m = what | 0x80000000;\n" + " list l = [\"a\"];\n" + " llOwnerSay(llList2String(l, what) + (string)m);\n" + " if ((what & 3) && sayIt()) llDie();\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("bit32's range made LSL's where it shows"); + CHECK(has(r, "local m = bit32.s32(bit32.bor(what, ")); + CHECK(count(r, "-- LSL: bit32") == 1); + } + UNSCOPED_INFO("not for a flag's truth"); + CHECK(has(r, "if bit32.btest(what, CHANGED_LINK)")); + { + INFO("&& said once, over a call that does something"); + CHECK(count(r, "leaves its right side unrun") == 1); + CHECK(has(r, "LSL ran both sides, the right one first.\n if bit32.btest(what, 3) and sayIt()")); + } + { + INFO("the deprecated call's reason only"); + CHECK(has(r, "-- LSL: SLua deprecates ll.List2String")); + CHECK_FALSE(has(r, "takes indexes from 0")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: flatter expressions: joined text unbracketed; one bit32 call per chain", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " string b = \"b\"; integer n = 3; integer f = 5;\n" + " llOwnerSay(\"a\" + b + \"c\" + (string)n);\n" + " llSetTextureAnim(ANIM_ON | ROTATE | LOOP, ALL_SIDES, 0, 0, 0, TWO_PI, 1.0);\n" + " llOwnerSay((string)((f | 2) & n));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a chain, one string"); + CHECK(has(r, "print(`a{b}c{n}`)")); + UNSCOPED_INFO("one call"); + CHECK(has(r, "ll.SetTextureAnim(bit32.bor(ANIM_ON, ROTATE, LOOP), ALL_SIDES")); + UNSCOPED_INFO("not across operators"); + CHECK(has(r, "bit32.band(bit32.bor(f, 2), n)")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: steps as statements before or after theirs unless something reads x", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer gLine;\n" + "integer bump() { return ++gLine; }\n" + "default { state_entry() {\n" + " integer n = 0;\n" + " llSay(n++, \"a\");\n" + " integer m = ++n;\n" + " integer k = n++ + n;\n" + " llOwnerSay((string)(gLine++));\n" + " llOwnerSay((string)bump() + (string)(gLine++) + (string)(m + k));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("after"); + CHECK(has(r, "ll.Say(n, \"a\")\nn += 1\n")); + UNSCOPED_INFO("before"); + CHECK(has(r, "\nn += 1\nlocal m = n\n")); + UNSCOPED_INFO("read again, kept"); + CHECK(has(r, "local k = (function() local was = n; n += 1 return was end)() + n")); + UNSCOPED_INFO("a global with nothing of the script's called"); + CHECK(has(r, "print(tostring(gLine))\ngLine += 1\n")); + UNSCOPED_INFO("a global beside a call of the script's, kept"); + CHECK(has(r, "print(`{bump()}{(function() local was = gLine;")); + UNSCOPED_INFO("a return's step before it"); + CHECK(has(r, " gLine += 1\n return gLine\n")); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<31>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: simpler conditions: negated comparisons flipped; if-set booleans direct", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer gOn;\n" + "default { touch_start(integer n) {\n" + " if (!(n == 2)) llOwnerSay(\"not two\");\n" + " if (!(n != 3)) llOwnerSay(\"three\");\n" + " if (n > 1) gOn = TRUE; else gOn = FALSE;\n" + " if (n > 5) { gOn = FALSE; } else { gOn = TRUE; }\n" + " if (gOn) llOwnerSay(\"on\");\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("flipped"); + CHECK(has(r, "if n ~= 2 then")); + CHECK(has(r, "if n == 3 then")); + } + const ALLSLToSLua::Result nots = require_converted("integer gOn;\n" + "default { touch_start(integer n) {\n" + " if (!n) llOwnerSay(\"none\"); if (!(n & 4)) llOwnerSay(\"no four\"); if (!(n > 1)) llDie();\n" + " gOn = !gOn; if (gOn) llOwnerSay(\"on\");\n" + "} }\n"); + CAPTURE(nots.text); + { + INFO("not a number: the other way round"); + CHECK(has(nots, "if n == 0 then")); + CHECK(has(nots, "if not bit32.btest(n, 4) then")); + CHECK(has(nots, "if not (n > 1) then")); + CHECK(has(nots, "gOn = not gOn")); + } + require_checks_clean(nots); + { + INFO("the check itself"); + CHECK(has(r, " gOn = n > 1\n")); + CHECK(has(r, " gOn = not (n > 5)\n")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a non-negative counter moved on by one as an index; loops count from 1", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " integer n = llGetInventoryNumber(INVENTORY_SOUND);\n" + " integer i; for (i = 0; i < n; ++i) llOwnerSay(llGetInventoryName(INVENTORY_SOUND, i));\n" + " integer j; for (j = 0; j < n; ++j) llOwnerSay((string)j + llGetInventoryName(INVENTORY_SOUND, j));\n" + " integer k; for (k = n - 1; k >= 0; --k) llOwnerSay(llGetInventoryName(INVENTORY_SOUND, k));\n" + " string s = \"abcdef\";\n" + " integer c; for (c = 0; c <= 2; ++c) llOwnerSay(llGetSubString(s, c, c + 1));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("counting from 1"); + CHECK(has(r, "for i = 1, n do\n print(ll.GetInventoryName(INVENTORY_SOUND, i))")); + UNSCOPED_INFO("read as a number too, moved on by one"); + CHECK(has(r, "for j = 0, n - 1 do\n print(`{j}{ll.GetInventoryName(INVENTORY_SOUND, j + 1)}`)")); + UNSCOPED_INFO("down to nought"); + CHECK(has(r, "for k = n - 1, 0, -1 do\n print(ll.GetInventoryName(INVENTORY_SOUND, k + 1))")); + UNSCOPED_INFO("to and with, a number added"); + CHECK(has(r, "for c = 1, 3 do\n print(ll.GetSubString(s, c, c + 1))")); + UNSCOPED_INFO("no llcompat"); + CHECK_FALSE(has(r, "llcompat.")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: list items read by index directly or through tostring; else llcompat", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("list gNames = [\"a\", \"b\"];\n" + "list parts(string s) { return llParseString2List(s, [\",\"], []); }\n" + "default { state_entry() {\n" + " list p = parts(\"x,y\");\n" + " list mixed = [1, \"two\", 3.0];\n" + " list keys = [llGetOwner(), \"text\"];\n" + " integer n = 1;\n" + " llOwnerSay(llList2String(gNames, 0) + llList2String(p, -1) + llList2String(keys, 1));\n" + " llOwnerSay(llList2String(mixed, 2) + (string)llList2Integer(mixed, 0) + llList2String(gNames, n));\n" + " integer i; for (i = 0; i < llGetListLength(p); ++i) llOwnerSay(llList2String(p, i) + llGetSubString(\"abc\", i, i));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("from the start, empty past the end"); + CHECK(has(r, "(gNames[1] or \"\")")); + UNSCOPED_INFO("back from the end, through a function's list"); + CHECK(has(r, "(p[#p] or \"\")")); + UNSCOPED_INFO("keys and text made text"); + CHECK(has(r, "tostring(keys[2] or \"\")")); + { + INFO("a float's text is not tostring's, and an index not known: llcompat"); + CHECK(has(r, "llcompat.List2String(mixed, 2)")); + CHECK(has(r, "llcompat.List2String(gNames, n)")); + } + UNSCOPED_INFO("mixed items for a number: llcompat"); + CHECK(has(r, "llcompat.List2Integer(mixed, 0)")); + UNSCOPED_INFO("within its length, as it is, counting from 1"); + CHECK(has(r, "for i = 1, #p do\n print(p[i] .. ll.GetSubString(\"abc\", i, i))")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: unshared lists changed at either end in place; elsewhere a new list", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("list gQueue;\n" + "list gShared;\n" + "keep(list l) { gShared = l; }\n" + "default { touch_start(integer n) {\n" + " gQueue += [n];\n" + " gQueue = llListInsertList(gQueue, [n + 1], 0);\n" + " gQueue = llDeleteSubList(gQueue, 0, 0);\n" + " gQueue = llDeleteSubList(gQueue, -1, -1);\n" + " gQueue = llDeleteSubList(gQueue, 1, 1);\n" + " keep(gShared);\n" + " gShared = llDeleteSubList(gShared, 0, 0);\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("in place"); + CHECK(has(r, "table.insert(gQueue, 1, n + 1)")); + CHECK(has(r, "table.remove(gQueue, 1)")); + CHECK(has(r, "table.remove(gQueue)\n")); + } + UNSCOPED_INFO("elsewhere in the list, as LSL did"); + CHECK(has(r, "gQueue = llcompat.DeleteSubList(gQueue, 1, 1)")); + UNSCOPED_INFO("one held elsewhere, as LSL did"); + CHECK(has(r, "gShared = llcompat.DeleteSubList(gShared, 0, 0)")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: notes are keyed with their words and lint; the studio's words used", "[ALLSLToSLua]") { - set_test_name("each note is keyed, with the words it was said with, and carries the lint that finds the same in SLua where one " - "does; the studio's words for a note are its comment's, and the header's"); const std::string lsl = "default {\n" " touch_start(integer n) {\n" " string s = \"abc\";\n" @@ -1066,26 +1422,40 @@ namespace tut "}\n"; ALLSLToSLua::Options close = ALLSLToSLua::Options::closeToLSL(); const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, close); - ensure("converted", r.converted); + CHECK(r.converted); std::string listed; for (const ALScriptProblem& p : r.notes) { listed += p.key + "(" + (p.args.empty() ? std::string() : p.args[0]) + ") " + p.code + "\n"; } + CAPTURE(listed); // Once for each function, with the lint where there is one. - ensure("an index: " + listed, noted(r, "SluaIndex", "GetSubString")); + UNSCOPED_INFO("an index"); + CHECK(noted(r, "SluaIndex", "GetSubString")); size_t indexes = 0; for (const ALScriptProblem& p : r.notes) { indexes += p.key == "SluaIndex"; const bool compat = p.key == "SluaIndex" || p.key == "SluaIndexFound" || p.key == "SluaBool"; - ensure("the lint of " + p.key + ": " + listed, compat ? p.code == "SlCompatCall" : p.code.empty()); - ensure_equals("no mark left unfilled", p.message, ALScriptProblem::fill(p.message, p.args)); + CAPTURE(p.key); + REQUIRE((compat ? p.code == "SlCompatCall" : p.code.empty())); + UNSCOPED_INFO("no mark left unfilled"); + REQUIRE(p.message == ALScriptProblem::fill(p.message, p.args)); + } + UNSCOPED_INFO("one index note for one function"); + CHECK(indexes == size_t(1)); + { + INFO("a found index and a boolean"); + CHECK(noted(r, "SluaIndexFound", "SubStringIndex")); + CHECK(noted(r, "SluaBool", "SameGroup")); + } + UNSCOPED_INFO("words filled"); + CHECK(has(r, "-- LSL: llcompat.GetSubString takes indexes from 0, as LSL did; ll.GetSubString takes them from 1.")); + { + INFO("the lint by key"); + CHECK(std::string(ALLSLToSLua::lintOf("SluaIndex")) == "SlCompatCall"); + CHECK_FALSE(ALLSLToSLua::lintOf("SluaListCompare")); } - ensure_equals("one index note for one function", indexes, size_t(1)); - ensure("a found index and a boolean: " + listed, noted(r, "SluaIndexFound", "SubStringIndex") && noted(r, "SluaBool", "SameGroup")); - ensure("words filled: " + listed, has(r, "-- LSL: llcompat.GetSubString takes indexes from 0, as LSL did; ll.GetSubString takes them from 1.")); - ensure("the lint by key", std::string(ALLSLToSLua::lintOf("SluaIndex")) == "SlCompatCall" && !ALLSLToSLua::lintOf("SluaListCompare")); // The studio's words: each comment's, each note's, and the header's, // a line at a time. @@ -1093,52 +1463,81 @@ namespace tut return key == "SluaHeader" ? std::string("first\nsecond") : "<" + key + (args.empty() ? "" : " " + args[0]) + ">"; }; const ALLSLToSLua::Result said = ALLSLToSLua::convert(lsl, close); - ensure("the header's: " + said.text, said.text.rfind("-- first\n-- second\n\n", 0) == 0); - ensure("a comment's: " + said.text, has(said, "-- LSL: \n")); - ensure("a note's", noted(said, "SluaIndex", "GetSubString") && [&] { - for (const ALScriptProblem& p : said.notes) - { - if (p.key == "SluaIndex") + CAPTURE(said.text); + UNSCOPED_INFO("the header's"); + CHECK(said.text.rfind("-- first\n-- second\n\n", 0) == 0); + UNSCOPED_INFO("a comment's"); + CHECK(has(said, "-- LSL: \n")); + { + INFO("a note's"); + CHECK(noted(said, "SluaIndex", "GetSubString")); + CHECK([&] { + for (const ALScriptProblem& p : said.notes) { - return p.message == ""; + if (p.key == "SluaIndex") + { + return p.message == ""; + } } - } - return false; - }()); + return false; + }()); + } } - template<> template<> - void allsltoslua_object::test<32>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: LSL comments read back as notes with Done and the lint's fix", "[ALLSLToSLua]") { - set_test_name("each LSL comment read back as a note: Done takes it out; one a lint finds too offers the lint's fix with it, read in " - "the words it was written in"); - ensure("the definitions", definitions); + CHECK(definitions); const std::string lsl = "default { state_entry() { string s = \"abc\"; llOwnerSay(llGetSubString(s, 0, 2)); } }"; const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, ALLSLToSLua::Options::closeToLSL()); - ensure("converted", r.converted); + CAPTURE(r.text); + CHECK(r.converted); ALScriptProblems notes = ALLSLToSLua::notesIn(r.text, nullptr); - ensure("as many as the converter noted: " + r.text, notes.size() == r.notes.size() && !notes.empty()); + { + INFO("as many as the converter noted"); + REQUIRE(notes.size() == r.notes.size()); + REQUIRE_FALSE(notes.empty()); + } ALScriptProblem* index = nullptr; for (ALScriptProblem& note : notes) { - ensure("a note's", note.key == "SluaNote" && note.severity == ALScriptProblem::Severity::Note && note.fixes.size() == 1 && - note.fixes[0].safe && note.fixes[0].removes && !note.fixes[0].preferred); + REQUIRE(note.key == "SluaNote"); + REQUIRE(note.severity == ALScriptProblem::Severity::Note); + REQUIRE(note.fixes.size() == 1); + REQUIRE(note.fixes[0].safe); + REQUIRE(note.fixes[0].removes); + REQUIRE_FALSE(note.fixes[0].preferred); index = note.message.rfind("llcompat.GetSubString takes indexes", 0) == 0 ? ¬e : index; } - ensure("the index's, the lint's: " + r.text, index && index->code == "SlCompatCall"); + { + INFO("the index's, the lint's"); + REQUIRE(index); + CHECK(index->code == "SlCompatCall"); + } const size_t at = index - notes.data(); ALLSLToSLua::linkNotes(notes, service.check(r.text), r.text); const ALScriptProblem& linked = notes[at]; - ensure("the lint's fix first, then Done", linked.fixes.size() == 2 && linked.fixes[0].preferred && - linked.fixes[0].title == "Write it ll.GetSubString(s, 1, 3), and take the note out" && - linked.fixes[1].title == "Done: take the note out"); + { + INFO("the lint's fix first, then Done"); + REQUIRE(linked.fixes.size() == 2); + CHECK(linked.fixes[0].preferred); + CHECK(linked.fixes[0].title == "Write it ll.GetSubString(s, 1, 3), and take the note out"); + CHECK(linked.fixes[1].title == "Done: take the note out"); + } const std::optional made = ALScriptFixes::apply(r.text, linked.fixes[0]); - ensure("made: ll's, the note gone", made && made->find("ll.GetSubString(s, 1, 3)") != std::string::npos && - made->find("llcompat.GetSubString takes indexes") == std::string::npos); + { + INFO("made: ll's, the note gone"); + REQUIRE(made); + CHECK(made->find("ll.GetSubString(s, 1, 3)") != std::string::npos); + CHECK(made->find("llcompat.GetSubString takes indexes") == std::string::npos); + } const std::optional done = ALScriptFixes::apply(r.text, linked.fixes[1]); - ensure("done: the line gone, the call kept", done && done->find("llcompat.GetSubString takes indexes") == std::string::npos && - done->find("llcompat.GetSubString(s, 0, 2)") != std::string::npos && - std::count(done->begin(), done->end(), '\n') + 1 == std::count(r.text.begin(), r.text.end(), '\n')); + { + INFO("done: the line gone, the call kept"); + REQUIRE(done); + CHECK(done->find("llcompat.GetSubString takes indexes") == std::string::npos); + CHECK(done->find("llcompat.GetSubString(s, 0, 2)") != std::string::npos); + CHECK(std::count(done->begin(), done->end(), '\n') + 1 == std::count(r.text.begin(), r.text.end(), '\n')); + } // Written in the studio's words, read back in them. ALLSLToSLua::Options close = ALLSLToSLua::Options::closeToLSL(); @@ -1146,158 +1545,204 @@ namespace tut return ALScriptProblem::fill(key == "SluaIndex" ? "Index de [1]" : english, args); }; const ALLSLToSLua::Result said = ALLSLToSLua::convert(lsl, close); - ensure("written so", has(said, "-- LSL: Index de GetSubString\n")); + UNSCOPED_INFO("written so"); + CHECK(has(said, "-- LSL: Index de GetSubString\n")); const auto linted = [](const ALScriptProblems& read) { return std::count_if(read.begin(), read.end(), [](const ALScriptProblem& note) { return note.code == "SlCompatCall"; }); }; - ensure("read in the studio's words", linted(ALLSLToSLua::notesIn(said.text, close.words)) == 1); - ensure("not in English's", linted(ALLSLToSLua::notesIn(said.text, nullptr)) == 0); - } - - template<> template<> - void allsltoslua_object::test<33>() - { - set_test_name("a parameter the script only reads as a truth is a boolean, each call giving one; one read as a number, or given more than TRUE where it is asked about TRUE, stays a number; an event's stay the grid's numbers"); - const ALLSLToSLua::Result r = convert("say(integer loud, string text) { if (loud) llShout(0, text); else llSay(0, text); }\n" - "glow(integer on) { if (!on) return; llOwnerSay(\"glow\"); }\n" - "integer next(integer n) { return n + 1; }\n" - "integer exactly(integer f) { return f == TRUE; }\n" - "default {\n" - " touch_start(integer n) {\n" - " say(TRUE, \"hi\");\n" - " say(n > 1, \"many\");\n" - " say(n, \"some\");\n" - " glow(FALSE);\n" - " llOwnerSay((string)next(n));\n" - " exactly(1); exactly(5);\n" - " }\n" - " on_rez(integer p) { if (p) llOwnerSay(\"rezzed\"); }\n" - "}\n"); - ensure("read only as a truth: a boolean: " + r.text, has(r, "local function say(loud, text)") && has(r, "if loud then")); - ensure("each call giving one, a number as its truth: " + r.text, - has(r, "say(true, \"hi\")") && has(r, "say(n > 1, \"many\")") && has(r, "say(n ~= 0, \"some\")")); - ensure("not, as a truth: " + r.text, has(r, "if not on then") && has(r, "glow(false)")); - ensure("read as a number: kept one: " + r.text, has(r, "return n + 1") && has(r, "next_(n)")); - ensure("given 5 where asked about TRUE: kept a number: " + r.text, has(r, "f == 1") && has(r, "exactly(5)")); - ensure("an event's: the grid's number: " + r.text, has(r, "if p ~= 0 then")); - checksClean(r); + UNSCOPED_INFO("read in the studio's words"); + CHECK(linted(ALLSLToSLua::notesIn(said.text, close.words)) == 1); + UNSCOPED_INFO("not in English's"); + CHECK(linted(ALLSLToSLua::notesIn(said.text, nullptr)) == 0); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: parameters only read as truths are booleans; others and events numbers", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("say(integer loud, string text) { if (loud) llShout(0, text); else llSay(0, text); }\n" + "glow(integer on) { if (!on) return; llOwnerSay(\"glow\"); }\n" + "integer next(integer n) { return n + 1; }\n" + "integer exactly(integer f) { return f == TRUE; }\n" + "default {\n" + " touch_start(integer n) {\n" + " say(TRUE, \"hi\");\n" + " say(n > 1, \"many\");\n" + " say(n, \"some\");\n" + " glow(FALSE);\n" + " llOwnerSay((string)next(n));\n" + " exactly(1); exactly(5);\n" + " }\n" + " on_rez(integer p) { if (p) llOwnerSay(\"rezzed\"); }\n" + "}\n"); + CAPTURE(r.text); + { + INFO("read only as a truth: a boolean"); + CHECK(has(r, "local function say(loud, text)")); + CHECK(has(r, "if loud then")); + } + { + INFO("each call giving one, a number as its truth"); + CHECK(has(r, "say(true, \"hi\")")); + CHECK(has(r, "say(n > 1, \"many\")")); + CHECK(has(r, "say(n ~= 0, \"some\")")); + } + { + INFO("not, as a truth"); + CHECK(has(r, "if not on then")); + CHECK(has(r, "glow(false)")); + } + { + INFO("read as a number: kept one"); + CHECK(has(r, "return n + 1")); + CHECK(has(r, "next_(n)")); + } + { + INFO("given 5 where asked about TRUE: kept a number"); + CHECK(has(r, "f == 1")); + CHECK(has(r, "exactly(5)")); + } + UNSCOPED_INFO("an event's: the grid's number"); + CHECK(has(r, "if p ~= 0 then")); + require_checks_clean(r); ALLSLToSLua::Options typed; typed.types = true; const ALLSLToSLua::Result t = ALLSLToSLua::convert("say(integer loud, string text) { if (loud) llSay(0, text); }\n" "default { state_entry() { say(TRUE, \"hi\"); } }\n", typed); - ensure("typed: " + t.text, has(t, "local function say(loud: boolean, text: string)")); - checksClean(t); - } - - template<> template<> - void allsltoslua_object::test<34>() - { - set_test_name("a counting loop over a list's items, its counter read as nothing else, walks the items; from another start, or read otherwise, it counts"); - const ALLSLToSLua::Result r = convert("list gNames = [\"a\", \"b\"];\n" - "default { state_entry() {\n" - " list keys = [llGetOwner()];\n" - " integer i;\n" - " for (i = 0; i < llGetListLength(gNames); ++i) llOwnerSay(llList2String(gNames, i));\n" - " integer j;\n" - " for (j = 0; j < llGetListLength(gNames); ++j) {\n" - " integer k;\n" - " for (k = 0; k < llGetListLength(keys); ++k) llOwnerSay(llList2String(gNames, j) + llList2String(keys, k));\n" - " }\n" - " integer m;\n" - " for (m = 1; m < llGetListLength(gNames); ++m) llOwnerSay(llList2String(gNames, m));\n" - " integer n;\n" - " for (n = 0; n < llGetListLength(gNames); ++n) llOwnerSay(llList2String(gNames, n) + (string)n);\n" - "} }\n"); - ensure("walked: " + r.text, has(r, "for _, item in gNames do\n print(item)\n")); - ensure("one inside another, each its own name, a key's made text: " + r.text, - has(r, "for _, item2 in keys do\n print(item .. tostring(item2))")); - ensure("from the second: counted: " + r.text, has(r, "for m = 2, #gNames do\n print(gNames[m])")); - ensure("its counter read as a number too: counted: " + r.text, has(r, "for n = 0, #gNames - 1 do")); - checksClean(r); - - const ALLSLToSLua::Result named = convert("list gNames = [\"a\"];\n" - "integer _ = 2;\n" - "default { state_entry() {\n" - " integer i;\n" - " for (i = 0; i < llGetListLength(gNames); ++i) llOwnerSay(llList2String(gNames, i) + (string)_);\n" - "} }\n"); - ensure("an index that would hide the script's _ named afresh: " + named.text, has(named, "for _2, item in gNames do")); - checksClean(named); - } - - template<> template<> - void allsltoslua_object::test<35>() - { - set_test_name("strings built in loops: one built by two loops declared once, the second adding to it; one appended to another in the loop it builds read as it stands, not built"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " string s;\n" - " integer i;\n" - " for (i = 0; i < 2; ++i) s += \"a\";\n" - " integer j;\n" - " for (j = 0; j < 2; ++j) s += \"b\";\n" - " llOwnerSay(s);\n" - " string line;\n" - " string all;\n" - " integer k;\n" - " for (k = 0; k < 3; ++k) { line += \"x\"; all += line; }\n" - " llOwnerSay(all);\n" - "} }\n"); - ensure("declared at the first loop's join: " + r.text, has(r, "local s = table.concat(sParts)")); - ensure("the second adds to it: " + r.text, has(r, "s ..= table.concat(sParts2)") && !has(r, "local s = table.concat(sParts2)")); - ensure("one read in its loop kept as it is: " + r.text, has(r, "local line = \"\"") && !has(r, "table.concat(lineParts)")); - ensure("the other built from it: " + r.text, has(r, "table.insert(allParts, line)") && has(r, "local all = table.concat(allParts)")); - checksClean(r); + CAPTURE(t.text); + CHECK(has(t, "local function say(loud: boolean, text: string)")); + require_checks_clean(t); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a counting loop from 0 whose counter only indexes walks the items", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("list gNames = [\"a\", \"b\"];\n" + "default { state_entry() {\n" + " list keys = [llGetOwner()];\n" + " integer i;\n" + " for (i = 0; i < llGetListLength(gNames); ++i) llOwnerSay(llList2String(gNames, i));\n" + " integer j;\n" + " for (j = 0; j < llGetListLength(gNames); ++j) {\n" + " integer k;\n" + " for (k = 0; k < llGetListLength(keys); ++k) llOwnerSay(llList2String(gNames, j) + llList2String(keys, k));\n" + " }\n" + " integer m;\n" + " for (m = 1; m < llGetListLength(gNames); ++m) llOwnerSay(llList2String(gNames, m));\n" + " integer n;\n" + " for (n = 0; n < llGetListLength(gNames); ++n) llOwnerSay(llList2String(gNames, n) + (string)n);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("walked"); + CHECK(has(r, "for _, item in gNames do\n print(item)\n")); + UNSCOPED_INFO("one inside another, each its own name, a key's made text"); + CHECK(has(r, "for _, item2 in keys do\n print(item .. tostring(item2))")); + UNSCOPED_INFO("from the second: counted"); + CHECK(has(r, "for m = 2, #gNames do\n print(gNames[m])")); + UNSCOPED_INFO("its counter read as a number too: counted"); + CHECK(has(r, "for n = 0, #gNames - 1 do")); + require_checks_clean(r); + + const ALLSLToSLua::Result named = require_converted("list gNames = [\"a\"];\n" + "integer _ = 2;\n" + "default { state_entry() {\n" + " integer i;\n" + " for (i = 0; i < llGetListLength(gNames); ++i) llOwnerSay(llList2String(gNames, i) + (string)_);\n" + "} }\n"); + CAPTURE(named.text); + UNSCOPED_INFO("an index that would hide the script's _ named afresh"); + CHECK(has(named, "for _2, item in gNames do")); + require_checks_clean(named); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: strings built in loops: two loops share one; one read in its loop kept", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " string s;\n" + " integer i;\n" + " for (i = 0; i < 2; ++i) s += \"a\";\n" + " integer j;\n" + " for (j = 0; j < 2; ++j) s += \"b\";\n" + " llOwnerSay(s);\n" + " string line;\n" + " string all;\n" + " integer k;\n" + " for (k = 0; k < 3; ++k) { line += \"x\"; all += line; }\n" + " llOwnerSay(all);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("declared at the first loop's join"); + CHECK(has(r, "local s = table.concat(sParts)")); + { + INFO("the second adds to it"); + CHECK(has(r, "s ..= table.concat(sParts2)")); + CHECK_FALSE(has(r, "local s = table.concat(sParts2)")); + } + { + INFO("one read in its loop kept as it is"); + CHECK(has(r, "local line = \"\"")); + CHECK_FALSE(has(r, "table.concat(lineParts)")); + } + { + INFO("the other built from it"); + CHECK(has(r, "table.insert(allParts, line)")); + CHECK(has(r, "local all = table.concat(allParts)")); + } + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<36>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: bit32 answers made signed where they can pass 2147483647 and it shows", "[ALLSLToSLua]") { - set_test_name("bit32's answers made LSL's signed integers where they can pass 2147483647 -- a message number with its top bit set, the mask " - "that finds it, ~, a shift -- and not where bit32 takes them again, only truth is asked, or an & is with a constant not below nought"); // A protocol of link messages whose numbers have their top bit set, // as a set of scripts may share: before, the mask's answer was never // the message's number, and no message was heard. - const ALLSLToSLua::Result r = convert("integer MASK = 0xFFFF0000;\n" - "integer MSG = 0xB3470000;\n" - "integer FN_MASK = 0x0000FF00;\n" - "integer FN_RESET = 0x0500;\n" - "default {\n" - " state_entry() {\n" - " llMessageLinked(LINK_SET, MSG | FN_RESET, \"\", \"\");\n" - " }\n" - " link_message(integer sender, integer num, string text, key id) {\n" - " if ((num & MASK) == MSG) {\n" - " integer fn = num & FN_MASK;\n" - " if (fn == FN_RESET) llOwnerSay(\"reset\");\n" - " }\n" - " integer inverse = ~num;\n" - " integer high = num << 4;\n" - " integer low = num >> 2;\n" - " integer part = FN_MASK >> 8;\n" - " if (~llGetPermissions() & PERMISSION_TRIGGER_ANIMATION) llOwnerSay(\"ask\");\n" - " llOwnerSay((string)(inverse + high + low + part));\n" - " }\n" - "}\n"); - ensure("the mask's answer signed: " + r.text, has(r, "if bit32.s32(bit32.band(num, MASK)) == MSG then")); - ensure("the number sent signed: " + r.text, has(r, "ll.MessageLinked(LINK_SET, bit32.s32(bit32.bor(MSG, FN_RESET)), \"\", \"\")")); - ensure("an & with a constant not below nought as it is: " + r.text, has(r, "local fn = bit32.band(num, FN_MASK)\n")); - ensure("~ signed: " + r.text, has(r, "local inverse = bit32.s32(bit32.bnot(num))")); - ensure("<< signed: " + r.text, has(r, "local high = bit32.s32(bit32.lshift(num, 4))")); - ensure(">> of what may be below nought signed: " + r.text, has(r, "local low = bit32.s32(bit32.arshift(num, 2))")); - ensure(">> of a constant not below nought as it is: " + r.text, has(r, "local part = bit32.arshift(FN_MASK, 8)\n")); - ensure("a truth, and what bit32 takes again, as they are: " + r.text, - has(r, "if bit32.btest(bit32.bnot(ll.GetPermissions()), PERMISSION_TRIGGER_ANIMATION) then")); - ensure("SLua's own bit32.s32, no helper, said once: " + r.text, !has(r, "local function int32") && count(r, "-- LSL: bit32 answers") == 1); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<37>() - { - set_test_name("the script's comments carried over, each over what it stood over: the script's own at the top, a global's, those inside a list on their lines, " - "a trailing one, a brace's after its opening line, between branches, at a block's end; // as --, /* */ as --[[ ]] at a level nothing in it closes"); + const ALLSLToSLua::Result r = require_converted("integer MASK = 0xFFFF0000;\n" + "integer MSG = 0xB3470000;\n" + "integer FN_MASK = 0x0000FF00;\n" + "integer FN_RESET = 0x0500;\n" + "default {\n" + " state_entry() {\n" + " llMessageLinked(LINK_SET, MSG | FN_RESET, \"\", \"\");\n" + " }\n" + " link_message(integer sender, integer num, string text, key id) {\n" + " if ((num & MASK) == MSG) {\n" + " integer fn = num & FN_MASK;\n" + " if (fn == FN_RESET) llOwnerSay(\"reset\");\n" + " }\n" + " integer inverse = ~num;\n" + " integer high = num << 4;\n" + " integer low = num >> 2;\n" + " integer part = FN_MASK >> 8;\n" + " if (~llGetPermissions() & PERMISSION_TRIGGER_ANIMATION) llOwnerSay(\"ask\");\n" + " llOwnerSay((string)(inverse + high + low + part));\n" + " }\n" + "}\n"); + CAPTURE(r.text); + UNSCOPED_INFO("the mask's answer signed"); + CHECK(has(r, "if bit32.s32(bit32.band(num, MASK)) == MSG then")); + UNSCOPED_INFO("the number sent signed"); + CHECK(has(r, "ll.MessageLinked(LINK_SET, bit32.s32(bit32.bor(MSG, FN_RESET)), \"\", \"\")")); + UNSCOPED_INFO("an & with a constant not below nought as it is"); + CHECK(has(r, "local fn = bit32.band(num, FN_MASK)\n")); + UNSCOPED_INFO("~ signed"); + CHECK(has(r, "local inverse = bit32.s32(bit32.bnot(num))")); + UNSCOPED_INFO("<< signed"); + CHECK(has(r, "local high = bit32.s32(bit32.lshift(num, 4))")); + UNSCOPED_INFO(">> of what may be below nought signed"); + CHECK(has(r, "local low = bit32.s32(bit32.arshift(num, 2))")); + UNSCOPED_INFO(">> of a constant not below nought as it is"); + CHECK(has(r, "local part = bit32.arshift(FN_MASK, 8)\n")); + UNSCOPED_INFO("a truth, and what bit32 takes again, as they are"); + CHECK(has(r, "if bit32.btest(bit32.bnot(ll.GetPermissions()), PERMISSION_TRIGGER_ANIMATION) then")); + { + INFO("SLua's own bit32.s32, no helper, said once"); + CHECK_FALSE(has(r, "local function int32")); + CHECK(count(r, "-- LSL: bit32 answers") == 1); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: comments carried over where they stood with blocks at a safe level", "[ALLSLToSLua]") + { const std::string lsl = "// Door script by Someone.\n" "// Do as you like with it.\n" "\n" @@ -1338,37 +1783,62 @@ namespace tut " /* it ends ]] here */\n" " }\n" "}\n"; - const ALLSLToSLua::Result r = convert(lsl); + const ALLSLToSLua::Result r = require_converted(lsl); + CAPTURE(r.text); const auto at = [&r](const std::string& text) { return r.text.find(text); }; - ensure("the script's own at the top: " + r.text, has(r, "-- Door script by Someone.\n-- Do as you like with it.\n\n")); - ensure("over everything, once: " + r.text, at("-- Door script by Someone.") < at("local SWING") && count(r, "Door script") == 1); - ensure("a global's, what trailed it still after it: " + r.text, has(r, "-- How far it swings, in degrees\nlocal SWING = 90.0 -- a right angle\n")); - ensure("those inside a list, on their lines: " + r.text, - has(r, "local NAMES = {\n \"open\", -- when it is open\n \"closed\" -- when it is shut\n}\n")); - ensure("a block comment as Luau's: " + r.text, has(r, "--[[ old code:\ninteger unused() { return 1; }\n]]\n")); - ensure("over the function it stood over: " + r.text, at("--[[ old code:") > at("local NAMES") && at("--[[ old code:") < at("-- Turns the door.")); - ensure("a brace's, after the line that opens it: " + r.text, has(r, "-- Turns the door.\nlocal function swing(by) -- by degrees\n")); - ensure("over a statement: " + r.text, has(r, " -- the rotation it turns by\n local r = ")); - ensure("a trailing one, after its statement: " + r.text, has(r, " ll.SetLinkPrimitiveParamsFast(LINK_THIS, { PRIM_ROT_LOCAL, r * ll.GetLocalRot() }) -- turn\n")); - ensure("between branches, over the elseif: " + r.text, has(r, " -- when it shuts\n elseif by < 0 then\n")); - ensure("an empty branch's, in it: " + r.text, has(r, " if false then\n --[[ left off ]]\n elseif by == 0 then\n")); - ensure("at a block's end: " + r.text, has(r, " -- done\nend\n")); - ensure("a state's, over its handler: " + r.text, has(r, "-- on a touch\nLLEvents:on(\"touch_start\"")); - ensure("not a block of Luau's: " + r.text, has(r, " swing(SWING) -- [[ not a block of Luau's ]]\n")); - ensure("a level nothing in it closes: " + r.text, has(r, " --[=[ it ends ]] here ]=]\nend)")); - checksClean(r); + UNSCOPED_INFO("the script's own at the top"); + CHECK(has(r, "-- Door script by Someone.\n-- Do as you like with it.\n\n")); + { + INFO("over everything, once"); + CHECK(at("-- Door script by Someone.") < at("local SWING")); + CHECK(count(r, "Door script") == 1); + } + UNSCOPED_INFO("a global's, what trailed it still after it"); + CHECK(has(r, "-- How far it swings, in degrees\nlocal SWING = 90.0 -- a right angle\n")); + UNSCOPED_INFO("those inside a list, on their lines"); + CHECK(has(r, "local NAMES = {\n \"open\", -- when it is open\n \"closed\" -- when it is shut\n}\n")); + UNSCOPED_INFO("a block comment as Luau's"); + CHECK(has(r, "--[[ old code:\ninteger unused() { return 1; }\n]]\n")); + { + INFO("over the function it stood over"); + CHECK(at("--[[ old code:") > at("local NAMES")); + CHECK(at("--[[ old code:") < at("-- Turns the door.")); + } + UNSCOPED_INFO("a brace's, after the line that opens it"); + CHECK(has(r, "-- Turns the door.\nlocal function swing(by) -- by degrees\n")); + UNSCOPED_INFO("over a statement"); + CHECK(has(r, " -- the rotation it turns by\n local r = ")); + UNSCOPED_INFO("a trailing one, after its statement"); + CHECK(has(r, " ll.SetLinkPrimitiveParamsFast(LINK_THIS, { PRIM_ROT_LOCAL, r * ll.GetLocalRot() }) -- turn\n")); + UNSCOPED_INFO("between branches, over the elseif"); + CHECK(has(r, " -- when it shuts\n elseif by < 0 then\n")); + UNSCOPED_INFO("an empty branch's, in it"); + CHECK(has(r, " if false then\n --[[ left off ]]\n elseif by == 0 then\n")); + UNSCOPED_INFO("at a block's end"); + CHECK(has(r, " -- done\nend\n")); + UNSCOPED_INFO("a state's, over its handler"); + CHECK(has(r, "-- on a touch\nLLEvents:on(\"touch_start\"")); + UNSCOPED_INFO("not a block of Luau's"); + CHECK(has(r, " swing(SWING) -- [[ not a block of Luau's ]]\n")); + UNSCOPED_INFO("a level nothing in it closes"); + CHECK(has(r, " --[=[ it ends ]] here ]=]\nend)")); + require_checks_clean(r); ALLSLToSLua::Options none; none.keepComments = false; const ALLSLToSLua::Result bare = ALLSLToSLua::convert(lsl, none); - ensure("none where asked: " + bare.text, bare.converted && bare.text.find("Door script") == std::string::npos && - bare.text.find("degrees") == std::string::npos && bare.text.find("--[[") == std::string::npos); + CAPTURE(bare.text); + { + INFO("none where asked"); + CHECK(bare.converted); + CHECK(bare.text.find("Door script") == std::string::npos); + CHECK(bare.text.find("degrees") == std::string::npos); + CHECK(bare.text.find("--[[") == std::string::npos); + } } - template<> template<> - void allsltoslua_object::test<38>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: spans: each LSL line beside the SLua line made of it", "[ALLSLToSLua]") { - set_test_name("spans: each global, function, handler and statement's LSL line beside the line of SLua made of it, under the head, its notes and comments"); const std::string lsl = "integer count = 0;\n" // 0 "add(integer n)\n" // 1 "{\n" // 2 @@ -1383,7 +1853,7 @@ namespace tut " llSay(0, (string)count);\n" // 11 " }\n" // 12 "}\n"; // 13 - const ALLSLToSLua::Result r = convert(lsl); + const ALLSLToSLua::Result r = require_converted(lsl); std::vector lines; for (size_t from = 0; from <= r.text.size();) { @@ -1393,9 +1863,13 @@ namespace tut } for (const ALLSLToSLua::Span& span : r.spans) { - ensure("within both: " + std::to_string(span.lslFirst) + " " + std::to_string(span.sluaFirst), - span.lslFirst >= 0 && span.lslLast < 14 && span.lslFirst <= span.lslLast && span.sluaFirst >= 0 && span.sluaFirst <= span.sluaLast && - span.sluaLast < static_cast(lines.size())); + CAPTURE(span.lslFirst, span.sluaFirst); + REQUIRE(span.lslFirst >= 0); + REQUIRE(span.lslLast < 14); + REQUIRE(span.lslFirst <= span.lslLast); + REQUIRE(span.sluaFirst >= 0); + REQUIRE(span.sluaFirst <= span.sluaLast); + REQUIRE(span.sluaLast < static_cast(lines.size())); } const auto at = [&](S32 lsl_line) -> std::string { for (const ALLSLToSLua::Span& span : r.spans) @@ -1407,111 +1881,166 @@ namespace tut } return "(none)"; }; - ensure("the global: " + at(0) + "\n" + r.text, at(0).find("local count") != std::string::npos); - ensure("the function: " + at(1), at(1).find("function add(") != std::string::npos); - ensure("its statement: " + at(3), at(3).find("count") != std::string::npos && at(3).find("n") != std::string::npos); - ensure("the handler: " + at(7), at(7).find("touch_start") != std::string::npos); - ensure("a call under its comment, not the comment: " + at(10), at(10).find("add(1)") != std::string::npos); - ensure("the next: " + at(11), at(11).find("ll.Say(0") != std::string::npos); - } - - template<> template<> - void allsltoslua_object::test<39>() - { - set_test_name("in fewer calls: an & asked whether it is nought by bit32.btest, values added together by one table.append, a key's default " - "the null key; and hexadecimal as written where SLua reads the same number"); - const ALLSLToSLua::Result r = convert("integer MASK = 0x0000FF00;\n" - "integer HIGH = 0xF0000000;\n" - "integer S_MASK = 0xF0000000;\n" - "integer BIG = 4294967296;\n" - "integer BIG_HEX = 0x100000000;\n" - "integer WRAPPED = 2147483648;\n" - "key gSitter;\n" - "default { touch_start(integer n) {\n" - " integer flags = llGetParcelFlags(llGetPos());\n" - " if (flags & PARCEL_FLAG_ALLOW_CREATE_OBJECTS) llOwnerSay(\"build\");\n" - " if (!(flags & MASK & 0x300)) llOwnerSay(\"none\");\n" - " if ((flags & 4) == 0) llOwnerSay(\"no 4\");\n" - " integer masked = flags & MASK;\n" - " integer top = flags & 0x80000000;\n" - " integer sign = flags & S_MASK; if (flags & ~S_MASK) sign = 0;\n" - " integer low = 0xFFFF0000; sign = sign | low;\n" - " list params = [PRIM_NAME, \"a\"];\n" - " params += [PRIM_LINK_TARGET, LINK_ROOT, PRIM_POSITION, llGetPos()];\n" - " params += n;\n" - " llSetLinkPrimitiveParamsFast(LINK_THIS, params);\n" - " float f = (float)\"1.5\";\n" - " llOwnerSay((string)(masked + HIGH + top + BIG + BIG_HEX + WRAPPED + sign) + (string)f + (string)gSitter);\n" - "} }\n"); - ensure("an & as a condition: " + r.text, has(r, "if bit32.btest(flags, PARCEL_FLAG_ALLOW_CREATE_OBJECTS) then")); - ensure("not of one, an & of &s one call: " + r.text, has(r, "if not bit32.btest(flags, MASK, 0x300) then")); - ensure("against nought: " + r.text, has(r, "if not bit32.btest(flags, 4) then")); - ensure("an & as a number is still bit32.band: " + r.text, has(r, "local masked = bit32.band(flags, MASK)")); - ensure("one bit32 takes, as written, which it reads the same: " + r.text, has(r, "local top = bit32.s32(bit32.band(flags, 0x80000000))")); - ensure("values added together: " + r.text, - has(r, "table.append(params, PRIM_LINK_TARGET, LINK_ROOT, PRIM_POSITION, ll.GetPos())") && has(r, "table.insert(params, n)")); - ensure("the null key: " + r.text, has(r, "local gSitter = NULL_KEY") && !has(r, "uuid(\"\")")); - ensure("a float from a string: " + r.text, has(r, "lslFloat(\"1.5\")") && has(r, "return llcompat.List2Float({ s }, 0)")); - ensure("hexadecimal kept: " + r.text, has(r, "local MASK = 0x0000FF00")); - ensure("past 0x7FFFFFFF, which SLua reads as another number, as LSL had it: " + r.text, has(r, "local HIGH = -268435456")); + UNSCOPED_INFO("the global: " + at(0) + "\n" + r.text); + CHECK(at(0).find("local count") != std::string::npos); + UNSCOPED_INFO("the function: " + at(1)); + CHECK(at(1).find("function add(") != std::string::npos); + { + INFO("its statement: " + at(3)); + CHECK(at(3).find("count") != std::string::npos); + CHECK(at(3).find("n") != std::string::npos); + } + UNSCOPED_INFO("the handler: " + at(7)); + CHECK(at(7).find("touch_start") != std::string::npos); + UNSCOPED_INFO("a call under its comment, not the comment: " + at(10)); + CHECK(at(10).find("add(1)") != std::string::npos); + UNSCOPED_INFO("the next: " + at(11)); + CHECK(at(11).find("ll.Say(0") != std::string::npos); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: fewer calls: bit32.btest and table.append; hexadecimal kept where safe", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer MASK = 0x0000FF00;\n" + "integer HIGH = 0xF0000000;\n" + "integer S_MASK = 0xF0000000;\n" + "integer BIG = 4294967296;\n" + "integer BIG_HEX = 0x100000000;\n" + "integer WRAPPED = 2147483648;\n" + "key gSitter;\n" + "default { touch_start(integer n) {\n" + " integer flags = llGetParcelFlags(llGetPos());\n" + " if (flags & PARCEL_FLAG_ALLOW_CREATE_OBJECTS) llOwnerSay(\"build\");\n" + " if (!(flags & MASK & 0x300)) llOwnerSay(\"none\");\n" + " if ((flags & 4) == 0) llOwnerSay(\"no 4\");\n" + " integer masked = flags & MASK;\n" + " integer top = flags & 0x80000000;\n" + " integer sign = flags & S_MASK; if (flags & ~S_MASK) sign = 0;\n" + " integer low = 0xFFFF0000; sign = sign | low;\n" + " list params = [PRIM_NAME, \"a\"];\n" + " params += [PRIM_LINK_TARGET, LINK_ROOT, PRIM_POSITION, llGetPos()];\n" + " params += n;\n" + " llSetLinkPrimitiveParamsFast(LINK_THIS, params);\n" + " float f = (float)\"1.5\";\n" + " llOwnerSay((string)(masked + HIGH + top + BIG + BIG_HEX + WRAPPED + sign) + (string)f + (string)gSitter);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("an & as a condition"); + CHECK(has(r, "if bit32.btest(flags, PARCEL_FLAG_ALLOW_CREATE_OBJECTS) then")); + UNSCOPED_INFO("not of one, an & of &s one call"); + CHECK(has(r, "if not bit32.btest(flags, MASK, 0x300) then")); + UNSCOPED_INFO("against nought"); + CHECK(has(r, "if not bit32.btest(flags, 4) then")); + UNSCOPED_INFO("an & as a number is still bit32.band"); + CHECK(has(r, "local masked = bit32.band(flags, MASK)")); + UNSCOPED_INFO("one bit32 takes, as written, which it reads the same"); + CHECK(has(r, "local top = bit32.s32(bit32.band(flags, 0x80000000))")); + { + INFO("values added together"); + CHECK(has(r, "table.append(params, PRIM_LINK_TARGET, LINK_ROOT, PRIM_POSITION, ll.GetPos())")); + CHECK(has(r, "table.insert(params, n)")); + } + { + INFO("the null key"); + CHECK(has(r, "local gSitter = NULL_KEY")); + CHECK_FALSE(has(r, "uuid(\"\")")); + } + { + INFO("a float from a string"); + CHECK(has(r, "lslFloat(\"1.5\")")); + CHECK(has(r, "return llcompat.List2Float({ s }, 0)")); + } + UNSCOPED_INFO("hexadecimal kept"); + CHECK(has(r, "local MASK = 0x0000FF00")); + UNSCOPED_INFO("past 0x7FFFFFFF, which SLua reads as another number, as LSL had it"); + CHECK(has(r, "local HIGH = -268435456")); // As the grid's 32-bit hosts read them: strtoul stops at 0xFFFFFFFF. - ensure("past 32 bits, -1: " + r.text, has(r, "local BIG = -1\n") && has(r, "local BIG_HEX = -1\n")); - ensure("and said: " + r.text, noted(r, "SluaIntegerPast32Bits", "4294967296") && noted(r, "SluaIntegerPast32Bits", "0x100000000")); - ensure("a global only bit32 reads keeps its hexadecimal: " + r.text, has(r, "local S_MASK = 0xF0000000\n") && - has(r, "bit32.band(flags, S_MASK)")); - ensure("one read as a number does not: " + r.text, has(r, "local HIGH = -268435456")); - ensure("nor a local, never set, only bit32 reads: " + r.text, has(r, "local low = 0xFFFF0000\n")); + { + INFO("past 32 bits, -1"); + CHECK(has(r, "local BIG = -1\n")); + CHECK(has(r, "local BIG_HEX = -1\n")); + } + { + INFO("and said"); + CHECK(noted(r, "SluaIntegerPast32Bits", "4294967296")); + CHECK(noted(r, "SluaIntegerPast32Bits", "0x100000000")); + } + { + INFO("a global only bit32 reads keeps its hexadecimal"); + CHECK(has(r, "local S_MASK = 0xF0000000\n")); + CHECK(has(r, "bit32.band(flags, S_MASK)")); + } + UNSCOPED_INFO("one read as a number does not"); + CHECK(has(r, "local HIGH = -268435456")); + UNSCOPED_INFO("nor a local, never set, only bit32 reads"); + CHECK(has(r, "local low = 0xFFFF0000\n")); // Each said once, over the first place: what SLua reads such a // number as, and what bit32.s32 is for. const auto times = [&r](const char* key) { return std::count_if(r.notes.begin(), r.notes.end(), [key](const ALScriptProblem& p) { return p.key == key; }); }; - ensure("the hexadecimal's number said once, over the first: " + r.text, - times("SluaHexSign") == 1 && noted(r, "SluaHexSign", "0xF0000000") && - has(r, "-- LSL: SLua reads 0xF0000000 as 4026531840, where LSL wrapped it to -268435456: bit32 takes either the same, so it " + { + INFO("the hexadecimal's number said once, over the first"); + CHECK(times("SluaHexSign") == 1); + CHECK(noted(r, "SluaHexSign", "0xF0000000")); + CHECK(has(r, "-- LSL: SLua reads 0xF0000000 as 4026531840, where LSL wrapped it to -268435456: bit32 takes either the same, so it " "stays as written where only bit32 reads it, and is -268435456 where it is read as a number.\nlocal HIGH = -268435456")); - ensure("bit32.s32 said once: " + r.text, times("SluaBit32Signed") == 1 && count(r, "-- LSL: bit32 answers 0 to 4294967295") == 1); - ensure("within them, wrapped: " + r.text, has(r, "local WRAPPED = -2147483648\n")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<40>() - { - set_test_name("numeric for by a constant's name, and a counter its fors share; each function local but one called before it is written"); - const ALLSLToSLua::Result r = convert("integer ACTIVE_PROPS_STRIDE = 3;\n" - "list activeProps = [\"a\", 1, 2, \"b\", 3, 4];\n" - "setProps(list unattachedProps) {\n" - " integer i;\n" - " for (i = llGetListLength(activeProps) - ACTIVE_PROPS_STRIDE; i >= 0; i -= ACTIVE_PROPS_STRIDE) {\n" - " llOwnerSay((string)i);\n" - " }\n" - " for (i = 0; i < llGetListLength(unattachedProps); i++) {\n" - " llOwnerSay((string)i + llList2String(unattachedProps, i));\n" - " }\n" - " report();\n" - "}\n" - "report() { llOwnerSay(\"done\"); }\n" - "integer twice(integer n) { return n * 2; }\n" - "default { state_entry() { setProps([\"x\"]); llOwnerSay((string)twice(2)); } }\n"); - ensure("down by the constant's name: " + r.text, has(r, "for i = #activeProps - ACTIVE_PROPS_STRIDE, 0, -ACTIVE_PROPS_STRIDE do")); - ensure("the same counter's other for: " + r.text, has(r, "for i = 0, #unattachedProps - 1 do")); - ensure("the counter's declaration gone: " + r.text, !has(r, "local i = 0") && !has(r, "while i")); - ensure("only the one called first declared first: " + r.text, has(r, "local report\n") && has(r, "local function setProps(") && - has(r, "\nfunction report()") && has(r, "local function twice(")); - checksClean(r); - - const ALLSLToSLua::Result set = convert("integer gStep = 2;\n" - "default { state_entry() { integer i; for (i = 0; i < 10; i += gStep) llOwnerSay((string)i); gStep = 3; } }\n"); - ensure("a step by a global something sets keeps the while: " + set.text, has(set, "while i < 10 do")); - checksClean(set); - } - - template<> template<> - void allsltoslua_object::test<41>() - { - set_test_name("the script's comments as it laid them out: one after a statement still after it, rules of stars and slashes as dashes, " - "the //* toggle as ---[[, and an if on one line on one line"); + } + { + INFO("bit32.s32 said once"); + CHECK(times("SluaBit32Signed") == 1); + CHECK(count(r, "-- LSL: bit32 answers 0 to 4294967295") == 1); + } + UNSCOPED_INFO("within them, wrapped"); + CHECK(has(r, "local WRAPPED = -2147483648\n")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: numeric for by a constant and a shared counter; one forward declaration", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer ACTIVE_PROPS_STRIDE = 3;\n" + "list activeProps = [\"a\", 1, 2, \"b\", 3, 4];\n" + "setProps(list unattachedProps) {\n" + " integer i;\n" + " for (i = llGetListLength(activeProps) - ACTIVE_PROPS_STRIDE; i >= 0; i -= ACTIVE_PROPS_STRIDE) {\n" + " llOwnerSay((string)i);\n" + " }\n" + " for (i = 0; i < llGetListLength(unattachedProps); i++) {\n" + " llOwnerSay((string)i + llList2String(unattachedProps, i));\n" + " }\n" + " report();\n" + "}\n" + "report() { llOwnerSay(\"done\"); }\n" + "integer twice(integer n) { return n * 2; }\n" + "default { state_entry() { setProps([\"x\"]); llOwnerSay((string)twice(2)); } }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("down by the constant's name"); + CHECK(has(r, "for i = #activeProps - ACTIVE_PROPS_STRIDE, 0, -ACTIVE_PROPS_STRIDE do")); + UNSCOPED_INFO("the same counter's other for"); + CHECK(has(r, "for i = 0, #unattachedProps - 1 do")); + { + INFO("the counter's declaration gone"); + CHECK_FALSE(has(r, "local i = 0")); + CHECK_FALSE(has(r, "while i")); + } + { + INFO("only the one called first declared first"); + CHECK(has(r, "local report\n")); + CHECK(has(r, "local function setProps(")); + CHECK(has(r, "\nfunction report()")); + CHECK(has(r, "local function twice(")); + } + require_checks_clean(r); + + const ALLSLToSLua::Result set = require_converted("integer gStep = 2;\n" + "default { state_entry() { integer i; for (i = 0; i < 10; i += gStep) llOwnerSay((string)i); gStep = 3; } }\n"); + CAPTURE(set.text); + UNSCOPED_INFO("a step by a global something sets keeps the while"); + CHECK(has(set, "while i < 10 do")); + require_checks_clean(set); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: comments as laid out: trailing; rules as dashes; the toggle; one-line ifs", "[ALLSLToSLua]") + { ALLSLToSLua::Options options; options.types = true; const ALLSLToSLua::Result r = ALLSLToSLua::convert("/***************************************/\n" @@ -1538,18 +2067,31 @@ namespace tut "}\n" "default { state_entry() { llOwnerSay(get(allowed(S_MASK)) + (string)gDebug); } }\n", options); - ensure("converted", r.converted); - ensure("a rule of stars: " + r.text, has(r, "\n-----------------------------------------\n--------------- CONSTANTS ---------------\n")); - ensure("after its statement: " + r.text, has(r, "local S_MASK: number = -268435456 -- -268435456\n")); - ensure("a rule of slashes: " + r.text, has(r, "\n---------------------------------------------------------------\n") || - has(r, "------------------------- DEBUGGING ---------------------------\n")); - ensure("the toggle: " + r.text, has(r, "---[[\nlocal function get(") && has(r, "end\n--]]\n")); - ensure("the toggle the other way: " + r.text, has(r, "--[[\nstring old() { return \"\"; }\n--]]")); - ensure("an if on one line on one: " + r.text, - has(r, " if bit32.btest(parcelFlags, PARCEL_FLAG_ALLOW_CREATE_OBJECTS) then return 1 end -- may build\n")); - ensure("one on two lines on three: " + r.text, has(r, " if bit32.btest(parcelFlags, 4) then\n return 2\n end\n")); - ensure("after a declaration and a block comment after a step: " + r.text, - has(r, "local n: number = 3 -- three\n") && has(r, "n += 1 --[[ and one ]]\n")); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("a rule of stars"); + CHECK(has(r, "\n-----------------------------------------\n--------------- CONSTANTS ---------------\n")); + UNSCOPED_INFO("after its statement"); + CHECK(has(r, "local S_MASK: number = -268435456 -- -268435456\n")); + UNSCOPED_INFO("a rule of slashes"); + CHECK((has(r, "\n---------------------------------------------------------------\n") || + has(r, "------------------------- DEBUGGING ---------------------------\n"))); + { + INFO("the toggle"); + CHECK(has(r, "---[[\nlocal function get(")); + CHECK(has(r, "end\n--]]\n")); + } + UNSCOPED_INFO("the toggle the other way"); + CHECK(has(r, "--[[\nstring old() { return \"\"; }\n--]]")); + UNSCOPED_INFO("an if on one line on one"); + CHECK(has(r, " if bit32.btest(parcelFlags, PARCEL_FLAG_ALLOW_CREATE_OBJECTS) then return 1 end -- may build\n")); + UNSCOPED_INFO("one on two lines on three"); + CHECK(has(r, " if bit32.btest(parcelFlags, 4) then\n return 2\n end\n")); + { + INFO("after a declaration and a block comment after a step"); + CHECK(has(r, "local n: number = 3 -- three\n")); + CHECK(has(r, "n += 1 --[[ and one ]]\n")); + } // The lines beside the LSL's past an if put on one line. std::vector lines; for (size_t from = 0; from <= r.text.size();) @@ -1568,16 +2110,20 @@ namespace tut } return "(none)"; }; - ensure("the if on one line beside its line: " + at(15), at(15).find("then return 1 end") != std::string::npos); - ensure("the next beside its own: " + at(16), at(16).find("if bit32.btest(parcelFlags, 4) then") != std::string::npos); - ensure("and those after: " + at(18) + " / " + at(19), at(18).find("local n: number = 3") != std::string::npos && at(19).find("n += 1") != std::string::npos); - checksClean(r); + UNSCOPED_INFO("the if on one line beside its line: " + at(15)); + CHECK(at(15).find("then return 1 end") != std::string::npos); + UNSCOPED_INFO("the next beside its own: " + at(16)); + CHECK(at(16).find("if bit32.btest(parcelFlags, 4) then") != std::string::npos); + { + INFO("and those after: " + at(18) + " / " + at(19)); + CHECK(at(18).find("local n: number = 3") != std::string::npos); + CHECK(at(19).find("n += 1") != std::string::npos); + } + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<42>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: declarations the LSL lined up lined up again and others as written", "[ALLSLToSLua]") { - set_test_name("declarations the LSL lined up lined up again, their = and the comments after them; those it did not, as written"); ALLSLToSLua::Options options; options.types = true; const ALLSLToSLua::Result r = ALLSLToSLua::convert("integer S_MASK = 0xF0000000; // the sign\n" @@ -1592,19 +2138,21 @@ namespace tut " llOwnerSay(PREFIX + (string)(S_MASK + ACTIVE_STRIDE + a + x + yy) + bee);\n" "} }\n", options); - ensure("converted", r.converted); - ensure("globals lined up: " + r.text, has(r, "local S_MASK: number = -268435456 -- the sign\n" - "local ACTIVE_STRIDE: number = 3 -- per prop\n" - "local PREFIX: string = \"ARS#\" -- linkset data\n")); - ensure("not lined up, not lined up: " + r.text, has(r, "local a: number = 1\nlocal bee: string = \"b\"\n")); - ensure("locals lined up: " + r.text, has(r, "\nlocal x: number = 1\nlocal yy: number = 2.5\n")); - checksClean(r); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("globals lined up"); + CHECK(has(r, "local S_MASK: number = -268435456 -- the sign\n" + "local ACTIVE_STRIDE: number = 3 -- per prop\n" + "local PREFIX: string = \"ARS#\" -- linkset data\n")); + UNSCOPED_INFO("not lined up, not lined up"); + CHECK(has(r, "local a: number = 1\nlocal bee: string = \"b\"\n")); + UNSCOPED_INFO("locals lined up"); + CHECK(has(r, "\nlocal x: number = 1\nlocal yy: number = 2.5\n")); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<43>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a vector or rotation cast from text bracketed only where needed", "[ALLSLToSLua]") { - set_test_name("text made a vector or a rotation bracketed only where an operator around it needs it"); const ALLSLToSLua::Result r = ALLSLToSLua::convert("vector offset;\n" "default { state_entry() {\n" " list command = [\"a\", \"<1,2,3>\"];\n" @@ -1613,78 +2161,91 @@ namespace tut " llOwnerSay((string)((vector)llList2String(command, 1) * 2.0));\n" " llSetPos((vector)llList2String(command, 1));\n" "} }\n"); - ensure("converted", r.converted); - ensure("assigned as it is: " + r.text, has(r, "offset = tovector(command[2] or \"\") or ZERO_VECTOR\n")); - ensure("a local's value too: " + r.text, has(r, "local r = toquaternion(command[2] or \"\") or ZERO_ROTATION\n")); - ensure("an argument too: " + r.text, has(r, "ll.SetPos(tovector(command[2] or \"\") or ZERO_VECTOR)")); - ensure("an operand bracketed: " + r.text, has(r, "(tovector(command[2] or \"\") or ZERO_VECTOR) * 2")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<44>() - { - set_test_name("an integer made text where text is joined, interpolated; alone, or a lone piece of a table's, by tostring"); - const ALLSLToSLua::Result r = convert("set(integer keey, string value) { llLinksetDataWrite(\"ARS#\" + (string)keey, value); }\n" - "default { state_entry() {\n" - " integer n = 4; float f = 1.5; key k = llGetOwner();\n" - " set(n, (string)n);\n" - " llOwnerSay((string)f + \";\" + (string)k + \";\" + (string)(n * 2) + (string)(-n));\n" - "} }\n"); - ensure("converted", r.converted); - ensure("joined, interpolated: " + r.text, has(r, "ll.LinksetDataWrite(`ARS#{keey}`, value)")); - ensure("alone, still text: " + r.text, has(r, "set(n, tostring(n))")); - ensure("a float's six places, a key's text, a product bracketed, a negation bare: " + r.text, - has(r, "print(`{string.format(\"%.6f\", f)};{k};{n * 2}{-n}`)")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<45>() - { - set_test_name("a vector made text as LSL wrote it, five places a part, which tostring would not: noted, once"); - const ALLSLToSLua::Result r = convert("integer SEAT_NUM = 0;\n" - "vector offset = ZERO_VECTOR;\n" - "set(integer which, string value) { llLinksetDataWrite((string)which, value); }\n" - "default { state_entry() {\n" - " set(7, (string)SEAT_NUM + \";\" + (string)offset);\n" - " llOwnerSay((string)ZERO_ROTATION);\n" - "} }\n"); - ensure("converted", r.converted); - ensure("LSL's text kept: " + r.text, has(r, "set(7, `{SEAT_NUM};{ll.DumpList2String({ offset }, \"\")}`)")); - ensure("a rotation's too: " + r.text, has(r, "ll.DumpList2String({ ZERO_ROTATION }, \"\")")); - ensure("said why, once: " + r.text, noted(r, "SluaVectorText") && r.text.find("-- LSL: ll.DumpList2String writes") != std::string::npos && - r.text.find("-- LSL: ll.DumpList2String writes") == r.text.rfind("-- LSL: ll.DumpList2String writes")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<46>() - { - set_test_name("text joined with a piece that is no string as one interpolated string, its own text escaped; strings alone with .."); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " key k = llGetOwner(); integer n = 3; string s = \"x\"; float f = 0.5;\n" - " llOwnerSay(\"100% `{a}` \\\\ \\\"q\\\"\\n\" + (string)k);\n" - " llOwnerSay(\"n=\" + (string)n + \";\" + s);\n" - " llOwnerSay(\"a\" + llToUpper(\"b\" + (string)k) + (string)f);\n" - " s = s + \"/\" + (string)k;\n" - " string all = \"\";\n" - " integer i; for (i = 0; i < n; ++i) all += (string)k + \",\";\n" - " llOwnerSay(all + s);\n" - "} }\n"); - ensure("converted", r.converted); - ensure("its text escaped -- a backtick, a brace, a backslash, a line -- and % as it is: " + r.text, - has(r, "print(`100% \\`\\{a}\\` \\\\ \"q\"\\n{k}`)")); - ensure("a number joined too: " + r.text, has(r, "print(`n={n};{s}`)")); - ensure("strings alone, with ..: " + r.text, has(r, "print(all .. s)")); - ensure("one inside another's braces: " + r.text, has(r, "print(`a{ll.ToUpper(`b{k}`)}{string.format(\"%.6f\", f)}`)")); - ensure("joined onto a string: " + r.text, has(r, "s ..= `/{k}`")); - checksClean(r); - } - template<> template<> - void allsltoslua_object::test<47>() - { - set_test_name("spans end where the SLua made of them does: a block at its end, a statement at its own line, a comment over the next left to the next"); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("assigned as it is"); + CHECK(has(r, "offset = tovector(command[2] or \"\") or ZERO_VECTOR\n")); + UNSCOPED_INFO("a local's value too"); + CHECK(has(r, "local r = toquaternion(command[2] or \"\") or ZERO_ROTATION\n")); + UNSCOPED_INFO("an argument too"); + CHECK(has(r, "ll.SetPos(tovector(command[2] or \"\") or ZERO_VECTOR)")); + UNSCOPED_INFO("an operand bracketed"); + CHECK(has(r, "(tovector(command[2] or \"\") or ZERO_VECTOR) * 2")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: an integer as text interpolated where joined and by tostring alone", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("set(integer keey, string value) { llLinksetDataWrite(\"ARS#\" + (string)keey, value); }\n" + "default { state_entry() {\n" + " integer n = 4; float f = 1.5; key k = llGetOwner();\n" + " set(n, (string)n);\n" + " llOwnerSay((string)f + \";\" + (string)k + \";\" + (string)(n * 2) + (string)(-n));\n" + "} }\n"); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("joined, interpolated"); + CHECK(has(r, "ll.LinksetDataWrite(`ARS#{keey}`, value)")); + UNSCOPED_INFO("alone, still text"); + CHECK(has(r, "set(n, tostring(n))")); + UNSCOPED_INFO("a float's six places, a key's text, a product bracketed, a negation bare"); + CHECK(has(r, "print(`{string.format(\"%.6f\", f)};{k};{n * 2}{-n}`)")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a vector as text with five places as LSL wrote it noted once", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer SEAT_NUM = 0;\n" + "vector offset = ZERO_VECTOR;\n" + "set(integer which, string value) { llLinksetDataWrite((string)which, value); }\n" + "default { state_entry() {\n" + " set(7, (string)SEAT_NUM + \";\" + (string)offset);\n" + " llOwnerSay((string)ZERO_ROTATION);\n" + "} }\n"); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("LSL's text kept"); + CHECK(has(r, "set(7, `{SEAT_NUM};{ll.DumpList2String({ offset }, \"\")}`)")); + UNSCOPED_INFO("a rotation's too"); + CHECK(has(r, "ll.DumpList2String({ ZERO_ROTATION }, \"\")")); + { + INFO("said why, once"); + CHECK(noted(r, "SluaVectorText")); + CHECK(r.text.find("-- LSL: ll.DumpList2String writes") != std::string::npos); + CHECK(r.text.find("-- LSL: ll.DumpList2String writes") == r.text.rfind("-- LSL: ll.DumpList2String writes")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: text joined with non-strings interpolated and escaped; strings with ..", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " key k = llGetOwner(); integer n = 3; string s = \"x\"; float f = 0.5;\n" + " llOwnerSay(\"100% `{a}` \\\\ \\\"q\\\"\\n\" + (string)k);\n" + " llOwnerSay(\"n=\" + (string)n + \";\" + s);\n" + " llOwnerSay(\"a\" + llToUpper(\"b\" + (string)k) + (string)f);\n" + " s = s + \"/\" + (string)k;\n" + " string all = \"\";\n" + " integer i; for (i = 0; i < n; ++i) all += (string)k + \",\";\n" + " llOwnerSay(all + s);\n" + "} }\n"); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("its text escaped -- a backtick, a brace, a backslash, a line -- and % as it is"); + CHECK(has(r, "print(`100% \\`\\{a}\\` \\\\ \"q\"\\n{k}`)")); + UNSCOPED_INFO("a number joined too"); + CHECK(has(r, "print(`n={n};{s}`)")); + UNSCOPED_INFO("strings alone, with .."); + CHECK(has(r, "print(all .. s)")); + UNSCOPED_INFO("one inside another's braces"); + CHECK(has(r, "print(`a{ll.ToUpper(`b{k}`)}{string.format(\"%.6f\", f)}`)")); + UNSCOPED_INFO("joined onto a string"); + CHECK(has(r, "s ..= `/{k}`")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: spans end where their SLua does: blocks at end and statements on a line", "[ALLSLToSLua]") + { const std::string lsl = "default\n" // 0 "{\n" // 1 " touch_start(integer d)\n" // 2 @@ -1697,7 +2258,7 @@ namespace tut " llSay(0, \"one\");\n" // 9 " }\n" // 10 "}\n"; // 11 - const ALLSLToSLua::Result r = convert(lsl); + const ALLSLToSLua::Result r = require_converted(lsl); std::vector lines; for (size_t from = 0; from <= r.text.size();) { @@ -1724,84 +2285,119 @@ namespace tut return first == std::string::npos ? std::string() : text.substr(first); }; const ALLSLToSLua::Span* block = span(4); - ensure("the if", block && block->lslLast == 7); - ensure("from its head: " + trimmed(block->sluaFirst), trimmed(block->sluaFirst).find("if d > 1 then") == 0); - ensure("to its end: " + trimmed(block->sluaLast) + "\n" + r.text, trimmed(block->sluaLast) == "end"); + { + INFO("the if"); + REQUIRE(block); + CHECK(block->lslLast == 7); + } + UNSCOPED_INFO("from its head: " + trimmed(block->sluaFirst)); + CHECK(trimmed(block->sluaFirst).find("if d > 1 then") == 0); + UNSCOPED_INFO("to its end: " + trimmed(block->sluaLast) + "\n" + r.text); + CHECK(trimmed(block->sluaLast) == "end"); const ALLSLToSLua::Span* inside = span(6); std::string all; for (const ALLSLToSLua::Span& each : r.spans) { all += std::to_string(each.lslFirst) + "-" + std::to_string(each.lslLast) + " : " + std::to_string(each.sluaFirst) + "-" + std::to_string(each.sluaLast) + "\n"; } - ensure("the statement in it, a line of its own\n" + all + r.text, inside && inside->lslLast == 6 && inside->sluaFirst == inside->sluaLast && - trimmed(inside->sluaFirst).find("ll.Say(0, \"many\")") == 0); + { + INFO("the statement in it, a line of its own\n" + all + r.text); + REQUIRE(inside); + CHECK(inside->lslLast == 6); + CHECK(inside->sluaFirst == inside->sluaLast); + CHECK(trimmed(inside->sluaFirst).find("ll.Say(0, \"many\")") == 0); + } const ALLSLToSLua::Span* next = span(9); - ensure("the next, under the comment: " + (next ? trimmed(next->sluaFirst) : std::string()), - next && next->sluaFirst == next->sluaLast && trimmed(next->sluaFirst).find("ll.Say(0, \"one\")") == 0 && - trimmed(next->sluaFirst - 1) == "-- after"); + { + INFO("the next, under the comment: " + (next ? trimmed(next->sluaFirst) : std::string())); + REQUIRE(next); + CHECK(next->sluaFirst == next->sluaLast); + REQUIRE(trimmed(next->sluaFirst).find("ll.Say(0, \"one\")") == 0); + CHECK(trimmed(next->sluaFirst - 1) == "-- after"); + } const ALLSLToSLua::Span* handler = span(2); - ensure("the handler, over all of it", handler && handler->lslLast == 10 && handler->sluaFirst < block->sluaFirst && handler->sluaLast >= next->sluaLast); - ensure("to its own end: " + trimmed(handler->sluaLast), trimmed(handler->sluaLast).find("end") == 0); - } - - template<> template<> - void allsltoslua_object::test<48>() - { - set_test_name("what the SLua says otherwise: each call's name as written, SLua's own way's too, a name SLua holds for its own, the syntax always; semicolons let go of"); - const ALLSLToSLua::Result r = convert("integer end = 1;\n" - "default\n" - "{\n" - " state_entry()\n" - " {\n" - " if (end != 2 && TRUE) llSay(0, \"hi\");\n" - " llOwnerSay((string)llAbs(end));\n" - " }\n" - "}\n"); + { + INFO("the handler, over all of it"); + REQUIRE(handler); + CHECK(handler->lslLast == 10); + CHECK(handler->sluaFirst < block->sluaFirst); + CHECK(handler->sluaLast >= next->sluaLast); + } + UNSCOPED_INFO("to its own end: " + trimmed(handler->sluaLast)); + CHECK(trimmed(handler->sluaLast).find("end") == 0); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: what the SLua says otherwise: calls and names and syntax; no semicolons", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer end = 1;\n" + "default\n" + "{\n" + " state_entry()\n" + " {\n" + " if (end != 2 && TRUE) llSay(0, \"hi\");\n" + " llOwnerSay((string)llAbs(end));\n" + " }\n" + "}\n"); + CAPTURE(r.text); const auto has_pair = [&r](const std::string& lsl, const std::string& slua) { return std::find(r.same.begin(), r.same.end(), std::make_pair(lsl, slua)) != r.same.end(); }; - ensure("llSay as ll.Say: " + r.text, has_pair("llSay", "ll.Say")); - ensure("llOwnerSay as SLua's print: " + r.text, has_pair("llOwnerSay", "print")); - ensure("llAbs as SLua's own: " + r.text, has_pair("llAbs", "math.abs")); - ensure("a name Luau holds, marked", has_pair("end", "end_") && has(r, "local end_")); - ensure("the syntax", has_pair("!=", "~=") && has_pair("&&", "and") && has_pair("integer", "local") && has_pair("TRUE", "true") && - has_pair("(string)", "tostring")); - ensure("each once", std::count(r.same.begin(), r.same.end(), std::make_pair(std::string("llSay"), std::string("ll.Say"))) == 1); - ensure("semicolons let go of", r.dropped == std::vector{ ";" }); - } - - template<> template<> - void allsltoslua_object::test<49>() - { - set_test_name("an integer made text and added to a string with += joined on as s = s + it has it, by .. alone"); - const ALLSLToSLua::Result r = convert("string gOut;\n" - "string gTwo;\n" - "default { state_entry() {\n" - " integer n = llGetUnixTime();\n" - " gOut += (string)n;\n" - " gTwo = gTwo + (string)n;\n" - " llOwnerSay(gOut + gTwo);\n" - "} }\n"); - ensure("converted", r.converted); - ensure("+=: " + r.text, has(r, "gOut ..= n\n")); - ensure("s = s +: " + r.text, has(r, "gTwo ..= n\n")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<50>() - { - set_test_name("spans: statements sharing an LSL line side by side, not one inside the other"); - const ALLSLToSLua::Result r = convert("integer x;\n" // 0 - "integer y;\n" // 1 - "default\n" // 2 - "{\n" // 3 - " state_entry()\n" // 4 - " {\n" // 5 - " x = 1; y = 2;\n" // 6 - " llSay(0, (string)(x + y));\n" // 7 - " }\n" // 8 - "}\n"); // 9 + UNSCOPED_INFO("llSay as ll.Say"); + CHECK(has_pair("llSay", "ll.Say")); + UNSCOPED_INFO("llOwnerSay as SLua's print"); + CHECK(has_pair("llOwnerSay", "print")); + UNSCOPED_INFO("llAbs as SLua's own"); + CHECK(has_pair("llAbs", "math.abs")); + { + INFO("a name Luau holds, marked"); + CHECK(has_pair("end", "end_")); + CHECK(has(r, "local end_")); + } + { + INFO("the syntax"); + CHECK(has_pair("!=", "~=")); + CHECK(has_pair("&&", "and")); + CHECK(has_pair("integer", "local")); + CHECK(has_pair("TRUE", "true")); + CHECK(has_pair("(string)", "tostring")); + } + UNSCOPED_INFO("each once"); + CHECK(std::count(r.same.begin(), r.same.end(), std::make_pair(std::string("llSay"), std::string("ll.Say"))) == 1); + UNSCOPED_INFO("semicolons let go of"); + CHECK(r.dropped == std::vector{ ";" }); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: an integer added to a string by += or s = s + joined by ..= alone", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("string gOut;\n" + "string gTwo;\n" + "default { state_entry() {\n" + " integer n = llGetUnixTime();\n" + " gOut += (string)n;\n" + " gTwo = gTwo + (string)n;\n" + " llOwnerSay(gOut + gTwo);\n" + "} }\n"); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("+="); + CHECK(has(r, "gOut ..= n\n")); + UNSCOPED_INFO("s = s +"); + CHECK(has(r, "gTwo ..= n\n")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: spans: statements sharing an LSL line side by side and not nested", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer x;\n" // 0 + "integer y;\n" // 1 + "default\n" // 2 + "{\n" // 3 + " state_entry()\n" // 4 + " {\n" // 5 + " x = 1; y = 2;\n" // 6 + " llSay(0, (string)(x + y));\n" // 7 + " }\n" // 8 + "}\n"); // 9 std::vector shared; std::string all; for (const ALLSLToSLua::Span& each : r.spans) @@ -1813,50 +2409,67 @@ namespace tut shared.push_back(&each); } } - ensure_equals("two statements on the line\n" + all + r.text, shared.size(), size_t(2)); - ensure("their SLua apart\n" + all + r.text, shared[0]->sluaLast < shared[1]->sluaFirst || shared[1]->sluaLast < shared[0]->sluaFirst); + CAPTURE(all, r.text); + UNSCOPED_INFO("two statements on the line"); + REQUIRE(shared.size() == size_t(2)); + UNSCOPED_INFO("their SLua apart"); + CHECK((shared[0]->sluaLast < shared[1]->sluaFirst || shared[1]->sluaLast < shared[0]->sluaFirst)); // The handler, over more than one LSL line, still holds both. for (const ALLSLToSLua::Span& each : r.spans) { if (each.lslFirst == 4) { - ensure("the handler over both\n" + all, each.sluaFirst < shared[0]->sluaFirst && each.sluaLast >= shared[1]->sluaLast); + INFO("the handler over both"); + REQUIRE(each.sluaFirst < shared[0]->sluaFirst); + REQUIRE(each.sluaLast >= shared[1]->sluaLast); } } } - template<> template<> - void allsltoslua_object::test<51>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: what the SLua says otherwise: a call as written; marks only where written", "[ALLSLToSLua]") { - set_test_name("what the SLua says otherwise: a call by the name it was written with, not one inside it; marks only where written so"); const auto has_pair = [](const ALLSLToSLua::Result& r, const std::string& lsl, const std::string& slua) { return std::find(r.same.begin(), r.same.end(), std::make_pair(lsl, slua)) != r.same.end(); }; - const ALLSLToSLua::Result plain = convert("default { state_entry() {\n" - " integer n = 2;\n" - " if (n > 1) llSay(0, (string)llPow(llFrand(1.0), 2.0));\n" - "} }\n"); - ensure("llFrand as ll.Frand: " + plain.text, has_pair(plain, "llFrand", "ll.Frand")); - ensure("llPow, written as ^, not as the call inside it: " + plain.text, - std::none_of(plain.same.begin(), plain.same.end(), [](const auto& p) { return p.first == "llPow"; })); - ensure("a block's braces: " + plain.text, has_pair(plain, "{", "then") && has_pair(plain, "}", "end")); - ensure("no vector, no string joined: no marks for them: " + plain.text, - !has_pair(plain, ">", ")") && !has_pair(plain, "<", "vector(") && !has_pair(plain, "+", "..") && !has_pair(plain, "+=", "..=")); - const ALLSLToSLua::Result marks = convert("string gOut;\n" - "default { state_entry() {\n" - " vector v = <1, 2, llFrand(1.0)>;\n" - " gOut += llGetObjectName();\n" - " llSay(0, gOut + llGetObjectDesc() + (string)v.z);\n" - "} }\n"); - ensure("a vector's brackets: " + marks.text, has_pair(marks, "<", "vector(") && has_pair(marks, ">", ")")); - ensure("a string added to: " + marks.text, has_pair(marks, "+=", "..=")); - } - - template<> template<> - void allsltoslua_object::test<52>() - { - set_test_name("a comment after a block's brace stays after the line that opens the block: then, else, elseif, a loop's do, repeat, a function, a " - "handler; one over several lines too, running on; a one-line if with a block comment after its brace still on one line"); + const ALLSLToSLua::Result plain = require_converted("default { state_entry() {\n" + " integer n = 2;\n" + " if (n > 1) llSay(0, (string)llPow(llFrand(1.0), 2.0));\n" + "} }\n"); + CAPTURE(plain.text); + UNSCOPED_INFO("llFrand as ll.Frand"); + CHECK(has_pair(plain, "llFrand", "ll.Frand")); + UNSCOPED_INFO("llPow, written as ^, not as the call inside it"); + CHECK(std::none_of(plain.same.begin(), plain.same.end(), [](const auto& p) { return p.first == "llPow"; })); + { + INFO("a block's braces"); + CHECK(has_pair(plain, "{", "then")); + CHECK(has_pair(plain, "}", "end")); + } + { + INFO("no vector, no string joined: no marks for them"); + CHECK_FALSE(has_pair(plain, ">", ")")); + CHECK_FALSE(has_pair(plain, "<", "vector(")); + CHECK_FALSE(has_pair(plain, "+", "..")); + CHECK_FALSE(has_pair(plain, "+=", "..=")); + } + const ALLSLToSLua::Result marks = require_converted("string gOut;\n" + "default { state_entry() {\n" + " vector v = <1, 2, llFrand(1.0)>;\n" + " gOut += llGetObjectName();\n" + " llSay(0, gOut + llGetObjectDesc() + (string)v.z);\n" + "} }\n"); + CAPTURE(marks.text); + { + INFO("a vector's brackets"); + CHECK(has_pair(marks, "<", "vector(")); + CHECK(has_pair(marks, ">", ")")); + } + UNSCOPED_INFO("a string added to"); + CHECK(has_pair(marks, "+=", "..=")); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a comment after a block's brace stays after the line that opens it", "[ALLSLToSLua]") + { const std::string lsl = "vector offset;\n" "list adjustments;\n" "integer count() { // how many\n" @@ -1888,279 +2501,587 @@ namespace tut " if (n) { /* one */ llOwnerSay(\"y\"); }\n" " }\n" "}\n"; - const ALLSLToSLua::Result r = convert(lsl); - ensure("converted", r.converted); - ensure("then: " + r.text, has(r, " if i < 0 then -- no saved setting. use 0\n offset = ZERO_VECTOR\n")); - ensure("elseif: " + r.text, has(r, " elseif i > 100 then -- too far\n")); - ensure("else: " + r.text, has(r, " else -- restore saved setting\n")); - ensure("not over the if: " + r.text, !has(r, "-- no saved setting. use 0\n -- restore") && count(r, "restore saved setting") == 1); - ensure("a while's do: " + r.text, has(r, " do -- count down\n")); - ensure("repeat: " + r.text, has(r, "repeat --[[ at least once ]]\n")); - ensure("a for's do: " + r.text, has(r, " do -- each\n")); - ensure("a function: " + r.text, has(r, "local function count() -- how many\n")); - ensure("a handler: " + r.text, has(r, "LLEvents:on(\"touch_start\", function(detected) -- whoever touches\n")); - ensure("one over two lines, running on: " + r.text, has(r, " if i ~= 0 then --[[ over\n two lines ]]\n print(\"x\")\n")); - ensure("a one-line if on one line still: " + r.text, has(r, " if n ~= 0 then --[[ one ]] print(\"y\") end\n")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted(lsl); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("then"); + CHECK(has(r, " if i < 0 then -- no saved setting. use 0\n offset = ZERO_VECTOR\n")); + UNSCOPED_INFO("elseif"); + CHECK(has(r, " elseif i > 100 then -- too far\n")); + UNSCOPED_INFO("else"); + CHECK(has(r, " else -- restore saved setting\n")); + { + INFO("not over the if"); + CHECK_FALSE(has(r, "-- no saved setting. use 0\n -- restore")); + CHECK(count(r, "restore saved setting") == 1); + } + UNSCOPED_INFO("a while's do"); + CHECK(has(r, " do -- count down\n")); + UNSCOPED_INFO("repeat"); + CHECK(has(r, "repeat --[[ at least once ]]\n")); + UNSCOPED_INFO("a for's do"); + CHECK(has(r, " do -- each\n")); + UNSCOPED_INFO("a function"); + CHECK(has(r, "local function count() -- how many\n")); + UNSCOPED_INFO("a handler"); + CHECK(has(r, "LLEvents:on(\"touch_start\", function(detected) -- whoever touches\n")); + UNSCOPED_INFO("one over two lines, running on"); + CHECK(has(r, " if i ~= 0 then --[[ over\n two lines ]]\n print(\"x\")\n")); + UNSCOPED_INFO("a one-line if on one line still"); + CHECK(has(r, " if n ~= 0 then --[[ one ]] print(\"y\") end\n")); + require_checks_clean(r); // The same with the field style, and in a state's table. ALLSLToSLua::Options field; field.handlers = ALLSLToSLua::Options::Handlers::Field; - ensure("a field's handler: ", has(ALLSLToSLua::convert(lsl, field), "function LLEvents.touch_start(detected) -- whoever touches\n")); - const ALLSLToSLua::Result states = convert("default {\n" - " touch_start(integer n) { // first state\n" - " state other;\n" - " }\n" - "}\n" - "state other {\n" - " state_entry() { // entered\n" - " llOwnerSay(\"in\");\n" - " }\n" - "}\n"); - ensure("in a state's table: " + states.text, has(states, "touch_start = function(detected) -- first state\n") && - has(states, "state_entry = function() -- entered\n")); - checksClean(states); + UNSCOPED_INFO("a field's handler"); + CHECK(has(ALLSLToSLua::convert(lsl, field), "function LLEvents.touch_start(detected) -- whoever touches\n")); + const ALLSLToSLua::Result states = require_converted("default {\n" + " touch_start(integer n) { // first state\n" + " state other;\n" + " }\n" + "}\n" + "state other {\n" + " state_entry() { // entered\n" + " llOwnerSay(\"in\");\n" + " }\n" + "}\n"); + CAPTURE(states.text); + { + INFO("in a state's table"); + CHECK(has(states, "touch_start = function(detected) -- first state\n")); + CHECK(has(states, "state_entry = function() -- entered\n")); + } + require_checks_clean(states); // A single state's state_entry runs at the top: its comment at the // top of what it ran. - const ALLSLToSLua::Result entry = convert("default {\n state_entry() { // set up\n llOwnerSay(\"hi\");\n }\n}\n"); - ensure("state_entry's, kept: " + entry.text, has(entry, "-- set up\n") && entry.text.find("-- set up") < entry.text.find("ll.OwnerSay")); - } - - template<> template<> - void allsltoslua_object::test<53>() - { - set_test_name("a comment after the brace of a block not written goes over what stands for it, not at the end of what holds it: a boolean's " - "if and else, a lone state's state_exit; an if with -- in a string in its check still on one line"); - const ALLSLToSLua::Result r = convert("integer gMany;\n" - "default {\n" - " touch_start(integer n) {\n" - " if (n > 1) { // many\n" - " gMany = TRUE;\n" - " } else { // one\n" - " gMany = FALSE;\n" - " }\n" - " if (gMany) llOwnerSay(\"many\");\n" - " }\n" - "}\n"); + const ALLSLToSLua::Result entry = require_converted("default {\n state_entry() { // set up\n llOwnerSay(\"hi\");\n }\n}\n"); + CAPTURE(entry.text); + { + INFO("state_entry's, kept"); + CHECK(has(entry, "-- set up\n")); + CHECK(entry.text.find("-- set up") < entry.text.find("ll.OwnerSay")); + } + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a comment after the brace of an unwritten block goes over its stand-in", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer gMany;\n" + "default {\n" + " touch_start(integer n) {\n" + " if (n > 1) { // many\n" + " gMany = TRUE;\n" + " } else { // one\n" + " gMany = FALSE;\n" + " }\n" + " if (gMany) llOwnerSay(\"many\");\n" + " }\n" + "}\n"); + CAPTURE(r.text); const size_t many = r.text.find(" -- many\n"); const size_t one = r.text.find(" -- one\n"); const size_t set = r.text.find(" gMany = n > 1\n"); - ensure("both over the line that stands for the if: " + r.text, set != std::string::npos && many < one && one < set); - ensure("once each: " + r.text, count(r, "-- many") == 1 && count(r, "-- one") == 1); - checksClean(r); - - const ALLSLToSLua::Result exit = convert("default {\n" - " state_entry() {\n" - " llOwnerSay(\"in\");\n" - " }\n" - " state_exit() { // say goodbye\n" - " llOwnerSay(\"out\");\n" - " }\n" - "}\n"); + { + INFO("both over the line that stands for the if"); + REQUIRE(set != std::string::npos); + CHECK(many < one); + CHECK(one < set); + } + { + INFO("once each"); + CHECK(count(r, "-- many") == 1); + CHECK(count(r, "-- one") == 1); + } + require_checks_clean(r); + + const ALLSLToSLua::Result exit = require_converted("default {\n" + " state_entry() {\n" + " llOwnerSay(\"in\");\n" + " }\n" + " state_exit() { // say goodbye\n" + " llOwnerSay(\"out\");\n" + " }\n" + "}\n"); + CAPTURE(exit.text); const size_t left = exit.text.find("-- state_exit, left out"); - ensure("state_exit's, over the line said in its place: " + exit.text, - left != std::string::npos && exit.text.find("-- say goodbye\n") < left && count(exit, "-- say goodbye") == 1); - - const ALLSLToSLua::Result dashes = convert("default {\n" - " listen(integer c, string name, key id, string s) {\n" - " if (s == \" -- x\") llOwnerSay(\"y\");\n" - " }\n" - "}\n"); - ensure("a string's dashes no comment: " + dashes.text, has(dashes, " == \" -- x\" then print(\"y\") end\n")); - } - - template<> template<> - void allsltoslua_object::test<54>() - { - set_test_name("llVecNorm keeps a zero vector zero, as LSL does, where vector.normalize makes NaN of it: through vecNorm, written " - "once, but for a vector known not to be nought; no vecNorm where nothing normalizes"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " vector d = llDetectedPos(0) - llGetPos();\n" - " vector a = llVecNorm(d);\n" - " vector b = llVecNorm(llGetVel());\n" - " vector c = llVecNorm(<0, 3, 4>);\n" - " vector z = llVecNorm(ZERO_VECTOR);\n" - " llOwnerSay((string)(a + b + c + z));\n" - "} }\n"); - ensure("guarded: " + r.text, has(r, "local a = vecNorm(d)") && has(r, "vecNorm(ll.GetVel())")); - ensure("written once: " + r.text, count(r, "local function vecNorm(v: vector): vector") == 1); - ensure("a vector known: " + r.text, has(r, "vector.normalize(vector(0, 3, 4))")); - ensure("nought guarded: " + r.text, has(r, "vecNorm(ZERO_VECTOR)")); - checksClean(r); - const ALLSLToSLua::Result none = convert("default { state_entry() { llOwnerSay((string)llVecMag(<1, 2, 3>)); } }\n"); - ensure("no helper unasked: " + none.text, !has(none, "vecNorm")); - } - - template<> template<> - void allsltoslua_object::test<65>() - { - set_test_name("a script that resets its time keeps a clock of its own, script-wide as a state change leaves LSL's: ll.GetTime read " - "from timeBase, each reset setting it again; one that only reads keeps ll.GetTime; close to LSL, llcompat's three"); - const ALLSLToSLua::Result r = convert("float timeBase;\n" - "default {\n" - " state_entry() { llResetTime(); state running; }\n" - "}\n" - "state running {\n" - " state_entry() { timeBase = llGetTime(); }\n" - " touch_start(integer n) {\n" - " float lap = llGetAndResetTime();\n" - " llGetAndResetTime();\n" - " llOwnerSay((string)(lap + timeBase) + \" \" + (string)(llGetTime() * 2));\n" - " }\n" - "}\n"); - ensure("the clock, once, over everything: " + r.text, - count(r, "local timeBase = ll.GetTime()") == 1 && r.text.find("local timeBase = ll.GetTime()") < r.text.find("local states")); - ensure("the script's own name made another: " + r.text, has(r, "local timeBase_ = 0") && has(r, "timeBase_ = ll.GetTime() - timeBase")); - ensure("a reset: " + r.text, has(r, "state_entry = function()\n timeBase = ll.GetTime()\n setState(\"running\")")); - ensure("both, as a value, through the helper: " + r.text, - has(r, "local lap = getAndResetTime()") && count(r, "local function getAndResetTime(): number") == 1); - ensure("both where nothing reads the time, the reset alone: " + r.text, - has(r, "local lap = getAndResetTime()\n timeBase = ll.GetTime()\n")); - ensure("a read bracketed: " + r.text, has(r, "(ll.GetTime() - timeBase) * 2")); - ensure("nothing of llcompat's: " + r.text, !has(r, "llcompat.ResetTime") && !has(r, "llcompat.GetAndResetTime") && - !noted(r, "SluaCompatOnly", "ResetTime") && !noted(r, "SluaCompatOnly", "GetAndResetTime")); - checksClean(r); - - const ALLSLToSLua::Result reads = convert("default { touch_start(integer n) { llOwnerSay((string)llGetTime()); } }\n"); - ensure("only read, ll's: " + reads.text, has(reads, "ll.GetTime()") && !has(reads, "timeBase")); + { + INFO("state_exit's, over the line said in its place"); + REQUIRE(left != std::string::npos); + CHECK(exit.text.find("-- say goodbye\n") < left); + CHECK(count(exit, "-- say goodbye") == 1); + } + + const ALLSLToSLua::Result dashes = require_converted("default {\n" + " listen(integer c, string name, key id, string s) {\n" + " if (s == \" -- x\") llOwnerSay(\"y\");\n" + " }\n" + "}\n"); + CAPTURE(dashes.text); + UNSCOPED_INFO("a string's dashes no comment"); + CHECK(has(dashes, " == \" -- x\" then print(\"y\") end\n")); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: llVecNorm through one vecNorm that keeps zero zero unless known nonzero", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " vector d = llDetectedPos(0) - llGetPos();\n" + " vector a = llVecNorm(d);\n" + " vector b = llVecNorm(llGetVel());\n" + " vector c = llVecNorm(<0, 3, 4>);\n" + " vector z = llVecNorm(ZERO_VECTOR);\n" + " llOwnerSay((string)(a + b + c + z));\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("guarded"); + CHECK(has(r, "local a = vecNorm(d)")); + CHECK(has(r, "vecNorm(ll.GetVel())")); + } + UNSCOPED_INFO("written once"); + CHECK(count(r, "local function vecNorm(v: vector): vector") == 1); + UNSCOPED_INFO("a vector known"); + CHECK(has(r, "vector.normalize(vector(0, 3, 4))")); + UNSCOPED_INFO("nought guarded"); + CHECK(has(r, "vecNorm(ZERO_VECTOR)")); + require_checks_clean(r); + const ALLSLToSLua::Result none = require_converted("default { state_entry() { llOwnerSay((string)llVecMag(<1, 2, 3>)); } }\n"); + CAPTURE(none.text); + UNSCOPED_INFO("no helper unasked"); + CHECK_FALSE(has(none, "vecNorm")); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: lists appended with table.extend and + on a copy of the left; no joinLists", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("list gKept = [1];\n" + "list kept() { return gKept; }\n" + "show(list l) { llOwnerSay(llDumpList2String(l, \",\")); }\n" + "default { touch_start(integer n) {\n" + " list all = llParseString2List(\"a,b\", [\",\"], []);\n" + " list more = [4, 5];\n" + " all += more;\n" + " all += all;\n" + " all += llList2List(more, 0, n);\n" + " all += [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, " + "26, 27, 28, 29, 30, 31, 32, \"x\"];\n" + " show(all);\n" + " show(kept() + more + all);\n" + " show(more + n);\n" + " show(n + more);\n" + " show([PRIM_NAME, \"x\"] + more);\n" + " show(llParseString2List(\"c\", [], []) + [1] + [<1, 2, 3>]);\n" + " llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_COLOR, ALL_SIDES, <1, 1, 1>, 1.0] + more);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a list by its name"); + CHECK(has(r, "table.extend(all, more)")); + UNSCOPED_INFO("itself"); + CHECK(has(r, "table.extend(all, all)")); + { + INFO("a call's, run once"); + CHECK(has(r, "table.extend(all, llcompat.List2List(more, 0, n))")); + CHECK_FALSE(has(r, "for _, item in")); + } + { + INFO("many values"); + CHECK(has(r, "table.extend(all, { 0, 1, 2,")); + CHECK(has(r, "32, \"x\" })")); + } + UNSCOPED_INFO("one copy, of the variable a call answers"); + CHECK(has(r, "show(table.extend(table.extend(table.clone(kept()), more), all))")); + UNSCOPED_INFO("a variable copied"); + CHECK(has(r, "show(table.extend(table.clone(more), { n }))")); + UNSCOPED_INFO("a value"); + CHECK(has(r, "show(table.extend({ n } :: { any }, more))")); + { + INFO("written out"); + CHECK(has(r, "show(table.extend({ PRIM_NAME, \"x\" } :: { any }, more))")); + CHECK(has(r, "table.extend({ PRIM_COLOR, ALL_SIDES, vector(1, 1, 1), 1.0 } :: { any }, more)")); + } + UNSCOPED_INFO("a library call's"); + CHECK(has(r, "show(table.extend(table.extend(ll.ParseString2List(\"c\", {}, {}) :: { any }, { 1 }), { vector(1, 2, 3) }))")); + { + INFO("no helper, nor a move"); + CHECK_FALSE(has(r, "joinLists")); + CHECK_FALSE(has(r, "table.move(")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: list functions through SLua's own where they do as LSL's; else llcompat", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("list gNames = [\"a\", \"b\"];\n" + "list gKept;\n" + "list gL = [1, 2, 3];\n" + "integer bump() { gL += [0]; return 1; }\n" + "default { touch_start(integer n) {\n" + " list l = [1, 2, 3];\n" + " integer i;\n" + " for (i = 0; i < llGetListLength(l); ++i)\n" + " l = llListReplaceList(l, [llList2Integer(l, i) * 2], i, i);\n" + " list held = [1, 2];\n" + " gKept = held;\n" + " integer j;\n" + " for (j = 0; j < llGetListLength(held); ++j)\n" + " held = llListReplaceList(held, [j], j, j);\n" + " integer k;\n" + " for (k = 0; k < llGetListLength(gL); ++k)\n" + " gL = llListReplaceList(gL, [bump()], k, k);\n" + " l = llListReplaceList(l, [7], -1, -1);\n" + " l = llListInsertList(l, [9], llGetListLength(l));\n" + " list part = llList2List(l, 1, 2);\n" + " list all = llList2List(l, 0, -1);\n" + " list rest = llList2List(l, 2, -1);\n" + " list wrapped = llList2List(l, 3, 1);\n" + " llOwnerSay(llDumpList2String(l, \",\"));\n" + " llOwnerSay(llDumpList2String(gNames + [n], \", \"));\n" + " llOwnerSay(llDumpList2String(l + [1.5], \",\"));\n" + " llOwnerSay((string)l);\n" + " integer at = llListFindList(gNames, [\"b\"]);\n" + " integer none = llListFindList(gNames, [NULL_KEY]);\n" + " if (llListFindList(gNames, [NULL_KEY]) != -1) llOwnerSay(\"null\");\n" + " integer parsed = llListFindList(llParseString2List(\"a,b\", [\",\"], []), [\"b\"]);\n" + " llOwnerSay((string)(at + none + parsed) + llDumpList2String(part + all + rest + wrapped + held, \"\"));\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("replaced in place, in Luau's numeric for"); + CHECK(has(r, "for i = 0, #l - 1 do\n l[i + 1] = l[i + 1] * 2\n")); + { + INFO("a list held elsewhere, replaced as LSL's"); + CHECK(has(r, "for j = 0, #held - 1 do\n")); + CHECK(has(r, " held = llcompat.ListReplaceList(held, { j }, j, j)\n")); + } + { + INFO("a global a call of the script's could change"); + CHECK(has(r, "while k < #gL do")); + CHECK(has(r, "gL = llcompat.ListReplaceList(gL, { bump() }, k, k)")); + } + UNSCOPED_INFO("the last of a list that could be empty"); + CHECK(has(r, "l = llcompat.ListReplaceList(l, { 7 }, -1, -1)")); + UNSCOPED_INFO("on the end"); + CHECK(has(r, "table.insert(l, 9)")); + { + INFO("a part, and all of it"); + CHECK(has(r, "local part = table.move(l, 2, 3, 1, {})")); + CHECK(has(r, "local all = table.clone(l)")); + } + { + INFO("to the end, and the part left out, as LSL's"); + CHECK(has(r, "local rest = llcompat.List2List(l, 2, -1)")); + CHECK(has(r, "local wrapped = llcompat.List2List(l, 3, 1)")); + } + { + INFO("whole numbers joined"); + CHECK(has(r, "print(table.concat(l, \",\"))")); + CHECK(has(r, "print(table.concat(l))")); + } + UNSCOPED_INFO("and text"); + CHECK(has(r, "print(table.concat(table.extend(table.clone(gNames), { n }), \", \"))")); + UNSCOPED_INFO("a float as LSL writes one"); + CHECK(has(r, "print(ll.DumpList2String(table.extend(table.clone(l), { 1.5 }), \",\"))")); + UNSCOPED_INFO("found, its place"); + CHECK(has(r, "local at = (table.find(gNames, \"b\") or 0) - 1")); + { + INFO("NULL_KEY as LSL's string, as its place and found"); + CHECK(has(r, "local none = (table.find(gNames, tostring(NULL_KEY)) or 0) - 1")); + CHECK(has(r, "if table.find(gNames, tostring(NULL_KEY)) ~= nil then")); + } + UNSCOPED_INFO("in a library call's list"); + CHECK(has(r, "local parsed = (table.find(ll.ParseString2List(\"a,b\", { \",\" }, {}) :: { any }, \"b\") or 0) - 1")); + UNSCOPED_INFO("no llcompat but those"); + CHECK(count(r, "llcompat.") == 5); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: llSubStringIndex asked only whether it found is a plain string.find", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " string s = llGetObjectName(); key k = llGetOwner();\n" + " if (llSubStringIndex(s, \"a\") != -1) llOwnerSay(\"a\");\n" + " if (llSubStringIndex(s, \"b\") >= 0) llOwnerSay(\"b\");\n" + " if (llSubStringIndex(s, \"c\") > -1) llOwnerSay(\"c\");\n" + " if (llSubStringIndex(s, \"d\") == -1) llOwnerSay(\"no d\");\n" + " if (!~llSubStringIndex(s, \"e\")) llOwnerSay(\"no e\");\n" + " if (~llSubStringIndex(s, \".\")) llOwnerSay(\"dot\");\n" + " if (~llSubStringIndex(s, k)) llOwnerSay(\"owner\");\n" + " if (~llSubStringIndex(s, \"\")) llOwnerSay(\"empty\");\n" + " integer at = llSubStringIndex(s, \"x\");\n" + " llOwnerSay((string)at);\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("found"); + CHECK(has(r, "if string.find(s, \"a\", 1, true) ~= nil then")); + CHECK(has(r, "if string.find(s, \"b\", 1, true) ~= nil then")); + CHECK(has(r, "if string.find(s, \"c\", 1, true) ~= nil then")); + } + { + INFO("not"); + CHECK(has(r, "if string.find(s, \"d\", 1, true) == nil then")); + CHECK(has(r, "if not (string.find(s, \"e\", 1, true) ~= nil) then")); + } + UNSCOPED_INFO("plain, not a pattern"); + CHECK(has(r, "if string.find(s, \".\", 1, true) ~= nil then")); + UNSCOPED_INFO("a key as its text"); + CHECK(has(r, "if string.find(s, tostring(k), 1, true) ~= nil then")); + UNSCOPED_INFO("the empty text"); + CHECK(has(r, "if string.find(s, \"\", 1, true) ~= nil then")); + UNSCOPED_INFO("its index in characters, ll's"); + CHECK(has(r, "local at = (ll.SubStringIndex(s, \"x\") or 0) - 1")); + UNSCOPED_INFO("found by string.find alone"); + CHECK(count(r, "SubStringIndex") == 1); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a script that resets its time keeps its own timeBase clock; else ll's", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("float timeBase;\n" + "default {\n" + " state_entry() { llResetTime(); state running; }\n" + "}\n" + "state running {\n" + " state_entry() { timeBase = llGetTime(); }\n" + " touch_start(integer n) {\n" + " float lap = llGetAndResetTime();\n" + " llGetAndResetTime();\n" + " llOwnerSay((string)(lap + timeBase) + \" \" + (string)(llGetTime() * 2));\n" + " }\n" + "}\n"); + CAPTURE(r.text); + { + INFO("the clock, once, over everything"); + CHECK(count(r, "local timeBase = ll.GetTime()") == 1); + CHECK(r.text.find("local timeBase = ll.GetTime()") < r.text.find("local states")); + } + { + INFO("the script's own name made another"); + CHECK(has(r, "local timeBase_ = 0")); + CHECK(has(r, "timeBase_ = ll.GetTime() - timeBase")); + } + UNSCOPED_INFO("a reset"); + CHECK(has(r, "state_entry = function()\n timeBase = ll.GetTime()\n setState(\"running\")")); + { + INFO("both, as a value, through the helper"); + CHECK(has(r, "local lap = getAndResetTime()")); + CHECK(count(r, "local function getAndResetTime(): number") == 1); + } + UNSCOPED_INFO("both where nothing reads the time, the reset alone"); + CHECK(has(r, "local lap = getAndResetTime()\n timeBase = ll.GetTime()\n")); + UNSCOPED_INFO("a read bracketed"); + CHECK(has(r, "(ll.GetTime() - timeBase) * 2")); + { + INFO("nothing of llcompat's"); + CHECK_FALSE(has(r, "llcompat.ResetTime")); + CHECK_FALSE(has(r, "llcompat.GetAndResetTime")); + CHECK_FALSE(noted(r, "SluaCompatOnly", "ResetTime")); + CHECK_FALSE(noted(r, "SluaCompatOnly", "GetAndResetTime")); + } + require_checks_clean(r); + + const ALLSLToSLua::Result reads = require_converted("default { touch_start(integer n) { llOwnerSay((string)llGetTime()); } }\n"); + CAPTURE(reads.text); + { + INFO("only read, ll's"); + CHECK(has(reads, "ll.GetTime()")); + CHECK_FALSE(has(reads, "timeBase")); + } const ALLSLToSLua::Result close = ALLSLToSLua::convert("default { touch_start(integer n) {\n" " llOwnerSay((string)llGetTime());\n" " llResetTime();\n" "} }\n", ALLSLToSLua::Options::closeToLSL()); - ensure("close to LSL, llcompat's together: " + close.text, - has(close, "llcompat.GetTime()") && has(close, "llcompat.ResetTime()") && !has(close, "ll.GetTime") && !has(close, "timeBase")); - checksClean(close); - } - - template<> template<> - void allsltoslua_object::test<66>() - { - set_test_name("Base64 through llbase64: encode as it is, the same bytes as LSL's; decode noted once, as it keeps a NUL and what is " - "not UTF-8; close to LSL, ll's, which only SLua's deprecating sets apart from llcompat's"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " key k = llDetectedKey(0);\n" - " string s = llStringToBase64(\"hello\");\n" - " string t = llStringToBase64(k);\n" - " string d = llBase64ToString(s);\n" - " string e = llBase64ToString(t + \"=\");\n" - " llOwnerSay(s + t + d + e);\n" - "} }\n"); - ensure("encode: " + r.text, has(r, "local s = llbase64.encode(\"hello\")") && has(r, "local t = llbase64.encode(tostring(k))")); - ensure("decode: " + r.text, has(r, "local d = llbase64.decode(s)") && has(r, "local e = llbase64.decode(t .. \"=\")")); - ensure("decode noted once: " + r.text, noted(r, "SluaBase64Decode") && count(r, "-- LSL: llbase64.decode keeps every byte") == 1); - ensure("nothing of llcompat's or ll's: " + r.text, !has(r, "llcompat.") && !has(r, "ll.StringToBase64") && !has(r, "ll.Base64ToString") && - !noted(r, "SluaDeprecatedFor", "StringToBase64") && - !noted(r, "SluaDeprecatedFor", "Base64ToString")); - checksClean(r); + CAPTURE(close.text); + { + INFO("close to LSL, llcompat's together"); + CHECK(has(close, "llcompat.GetTime()")); + CHECK(has(close, "llcompat.ResetTime()")); + CHECK_FALSE(has(close, "ll.GetTime")); + CHECK_FALSE(has(close, "timeBase")); + } + require_checks_clean(close); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: Base64 through llbase64 with decode noted once; ll's close to LSL", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " key k = llDetectedKey(0);\n" + " string s = llStringToBase64(\"hello\");\n" + " string t = llStringToBase64(k);\n" + " string d = llBase64ToString(s);\n" + " string e = llBase64ToString(t + \"=\");\n" + " llOwnerSay(s + t + d + e);\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("encode"); + CHECK(has(r, "local s = llbase64.encode(\"hello\")")); + CHECK(has(r, "local t = llbase64.encode(tostring(k))")); + } + { + INFO("decode"); + CHECK(has(r, "local d = llbase64.decode(s)")); + CHECK(has(r, "local e = llbase64.decode(t .. \"=\")")); + } + { + INFO("decode noted once"); + CHECK(noted(r, "SluaBase64Decode")); + CHECK(count(r, "-- LSL: llbase64.decode keeps every byte") == 1); + } + { + INFO("nothing of llcompat's or ll's"); + CHECK_FALSE(has(r, "llcompat.")); + CHECK_FALSE(has(r, "ll.StringToBase64")); + CHECK_FALSE(has(r, "ll.Base64ToString")); + CHECK_FALSE(noted(r, "SluaDeprecatedFor", "StringToBase64")); + CHECK_FALSE(noted(r, "SluaDeprecatedFor", "Base64ToString")); + } + require_checks_clean(r); const ALLSLToSLua::Result close = ALLSLToSLua::convert("default { state_entry() { llOwnerSay(llBase64ToString(llStringToBase64(\"x\"))); } }\n", ALLSLToSLua::Options::closeToLSL()); - ensure("close to LSL: " + close.text, - has(close, "ll.Base64ToString(ll.StringToBase64(\"x\"))") && !has(close, "llcompat.") && !has(close, "llbase64.decode(") && - !has(close, "llbase64.encode(") && !noted(close, "SluaBase64Decode")); - } - - template<> template<> - void allsltoslua_object::test<67>() - { - set_test_name("llAdjustDamage in on_damage as the detected table's adjustDamage, as the llDetected* are; llcompat's elsewhere, where " - "LSL's call only said an error and adjustDamage would stop the handler"); - const ALLSLToSLua::Result r = convert("halve() { llAdjustDamage(0, 2.0); }\n" - "default {\n" - " on_damage(integer n) {\n" - " llAdjustDamage(0, 0.5);\n" - " integer i;\n" - " for (i = 1; i < n; ++i) llAdjustDamage(i, 5);\n" - " if (n > 3) halve();\n" - " }\n" - " touch_start(integer n) { llAdjustDamage(0, 1.0); }\n" - "}\n"); - ensure("on_damage's own: " + r.text, has(r, "detected[1]:adjustDamage(0.5)") && has(r, "detected[i + 1]:adjustDamage(5)") && - noted(r, "SluaDetectedTable")); - ensure("a function's, llcompat's: " + r.text, has(r, "llcompat.AdjustDamage(0, 2.0)")); - ensure("another event's, llcompat's: " + r.text, has(r, "llcompat.AdjustDamage(0, 1.0)")); - checksClean(r); + CAPTURE(close.text); + { + INFO("close to LSL"); + CHECK(has(close, "ll.Base64ToString(ll.StringToBase64(\"x\"))")); + CHECK_FALSE(has(close, "llcompat.")); + CHECK_FALSE(has(close, "llbase64.decode(")); + CHECK_FALSE(has(close, "llbase64.encode(")); + CHECK_FALSE(noted(close, "SluaBase64Decode")); + } + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: llAdjustDamage in on_damage as detected adjustDamage; llcompat elsewhere", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("halve() { llAdjustDamage(0, 2.0); }\n" + "default {\n" + " on_damage(integer n) {\n" + " llAdjustDamage(0, 0.5);\n" + " integer i;\n" + " for (i = 1; i < n; ++i) llAdjustDamage(i, 5);\n" + " if (n > 3) halve();\n" + " }\n" + " touch_start(integer n) { llAdjustDamage(0, 1.0); }\n" + "}\n"); + CAPTURE(r.text); + { + INFO("on_damage's own"); + CHECK(has(r, "detected[1]:adjustDamage(0.5)")); + CHECK(has(r, "detected[i + 1]:adjustDamage(5)")); + CHECK(noted(r, "SluaDetectedTable")); + } + UNSCOPED_INFO("a function's, llcompat's"); + CHECK(has(r, "llcompat.AdjustDamage(0, 2.0)")); + UNSCOPED_INFO("another event's, llcompat's"); + CHECK(has(r, "llcompat.AdjustDamage(0, 1.0)")); + require_checks_clean(r); const ALLSLToSLua::Result close = ALLSLToSLua::convert("default { on_damage(integer n) { llAdjustDamage(0, 0.5); } }\n", ALLSLToSLua::Options::closeToLSL()); - ensure("close to LSL, llcompat's: " + close.text, has(close, "llcompat.AdjustDamage(0, 0.5)") && !has(close, "adjustDamage(")); - } - - template<> template<> - void allsltoslua_object::test<68>() - { - set_test_name("a timer every state's handler of which turns it off first is a one-shot, on LLTimers:once, set again as often as " - "the script likes; on LLTimers:every where a state has no timer handler, or one that does anything first"); - const ALLSLToSLua::Result r = convert("default {\n" - " state_entry() { llSetTimerEvent(5.0); }\n" - " timer() {\n" - " llSetTimerEvent(0.0);\n" - " llOwnerSay(\"once\");\n" - " if (llFrand(1.0) < 0.5) llSetTimerEvent(2);\n" - " }\n" - " touch_start(integer n) { llSetTimerEvent(1); }\n" - "}\n"); - ensure("once: " + r.text, has(r, "timerHandle = LLTimers:once(seconds, function()") && !has(r, "LLTimers:every(seconds, function")); - ensure("said so: " + r.text, has(r, "-- time it is set, as its handler turns it off before anything else.")); - ensure("the handler's own off kept, and set again: " + r.text, - has(r, "function timerHandler()\n setTimer(0.0)\n") && has(r, "setTimer(2)") && has(r, "setTimer(1)")); - checksClean(r); + CAPTURE(close.text); + { + INFO("close to LSL, llcompat's"); + CHECK(has(close, "llcompat.AdjustDamage(0, 0.5)")); + CHECK_FALSE(has(close, "adjustDamage(")); + } + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a timer every handler turns off first is LLTimers:once; else :every", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default {\n" + " state_entry() { llSetTimerEvent(5.0); }\n" + " timer() {\n" + " llSetTimerEvent(0.0);\n" + " llOwnerSay(\"once\");\n" + " if (llFrand(1.0) < 0.5) llSetTimerEvent(2);\n" + " }\n" + " touch_start(integer n) { llSetTimerEvent(1); }\n" + "}\n"); + CAPTURE(r.text); + { + INFO("once"); + CHECK(has(r, "timerHandle = LLTimers:once(seconds, function()")); + CHECK_FALSE(has(r, "LLTimers:every(seconds, function")); + } + UNSCOPED_INFO("said so"); + CHECK(has(r, "-- time it is set, as its handler turns it off before anything else.")); + { + INFO("the handler's own off kept, and set again"); + CHECK(has(r, "function timerHandler()\n setTimer(0.0)\n")); + CHECK(has(r, "setTimer(2)")); + CHECK(has(r, "setTimer(1)")); + } + require_checks_clean(r); // Tapple's: the local the preamble declared, assigned by a function // statement of its name, which types as it is declared. ALLSLToSLua::Options typed; typed.types = true; const ALLSLToSLua::Result types = ALLSLToSLua::convert("default { state_entry() { llSetTimerEvent(5); } timer() { llOwnerSay(\"t\"); } }\n", typed); - ensure("a function statement, typed: " + types.text, - has(types, "local timerHandler: (() -> ())? = nil\n") && has(types, "\nfunction timerHandler()\n print(\"t\")\nend\n") && - !has(types, "timerHandler = function")); - checksClean(types); - - const ALLSLToSLua::Result states = convert("default {\n" - " state_entry() { llSetTimerEvent(1); }\n" - " timer() { llSetTimerEvent(0); state two; }\n" - "}\n" - "state two {\n" - " timer() { llSetTimerEvent(FALSE); llOwnerSay(\"two\"); }\n" - " touch_start(integer n) { llSetTimerEvent(3); }\n" - "}\n"); - ensure("every state's, once: " + states.text, - has(states, "timerHandle = LLTimers:once(seconds, function") && has(states, "states[currentState].timer")); - checksClean(states); - - const ALLSLToSLua::Result without = convert("default {\n" - " state_entry() { llSetTimerEvent(1); state two; }\n" - " timer() { llSetTimerEvent(0); }\n" - "}\n" - "state two {\n" - " touch_start(integer n) { state default; }\n" - "}\n"); - ensure("a state with none, every: " + without.text, - has(without, "timerHandle = LLTimers:every(seconds, function") && !has(without, "LLTimers:once(seconds, function")); - - const ALLSLToSLua::Result later = convert("default {\n" - " state_entry() { llSetTimerEvent(1); }\n" - " timer() { llOwnerSay(\"tick\"); llSetTimerEvent(0); }\n" - "}\n"); - ensure("off after something else, every: " + later.text, - has(later, "timerHandle = LLTimers:every(seconds, function") && !has(later, "LLTimers:once(seconds, function")); - - const ALLSLToSLua::Result again = convert("float gNext = 0.0;\n" - "default {\n" - " state_entry() { llSetTimerEvent(1); }\n" - " timer() { llSetTimerEvent(gNext); }\n" - " touch_start(integer n) { gNext = 2.0; }\n" - "}\n"); - ensure("set to what is not written nought, every: " + again.text, - has(again, "timerHandle = LLTimers:every(seconds, function") && !has(again, "LLTimers:once(seconds, function")); - } - - template<> template<> - void allsltoslua_object::test<75>() - { - set_test_name("an integer's / and % as LSL's, toward nought and the remainder of the dividend's sign: bit32.s32 of the quotient and " - "math.fmod, unnoted; // and % where neither side is below nought; compound ones written whole; a float's / as it was"); + CAPTURE(types.text); + { + INFO("a function statement, typed"); + CHECK(has(types, "local timerHandler: (() -> ())? = nil\n")); + CHECK(has(types, "\nfunction timerHandler()\n print(\"t\")\nend\n")); + CHECK_FALSE(has(types, "timerHandler = function")); + } + require_checks_clean(types); + + const ALLSLToSLua::Result states = require_converted("default {\n" + " state_entry() { llSetTimerEvent(1); }\n" + " timer() { llSetTimerEvent(0); state two; }\n" + "}\n" + "state two {\n" + " timer() { llSetTimerEvent(FALSE); llOwnerSay(\"two\"); }\n" + " touch_start(integer n) { llSetTimerEvent(3); }\n" + "}\n"); + CAPTURE(states.text); + { + INFO("every state's, once"); + CHECK(has(states, "timerHandle = LLTimers:once(seconds, function")); + CHECK(has(states, "states[currentState].timer")); + } + require_checks_clean(states); + + const ALLSLToSLua::Result without = require_converted("default {\n" + " state_entry() { llSetTimerEvent(1); state two; }\n" + " timer() { llSetTimerEvent(0); }\n" + "}\n" + "state two {\n" + " touch_start(integer n) { state default; }\n" + "}\n"); + CAPTURE(without.text); + { + INFO("a state with none, every"); + CHECK(has(without, "timerHandle = LLTimers:every(seconds, function")); + CHECK_FALSE(has(without, "LLTimers:once(seconds, function")); + } + + const ALLSLToSLua::Result later = require_converted("default {\n" + " state_entry() { llSetTimerEvent(1); }\n" + " timer() { llOwnerSay(\"tick\"); llSetTimerEvent(0); }\n" + "}\n"); + CAPTURE(later.text); + { + INFO("off after something else, every"); + CHECK(has(later, "timerHandle = LLTimers:every(seconds, function")); + CHECK_FALSE(has(later, "LLTimers:once(seconds, function")); + } + + const ALLSLToSLua::Result again = require_converted("float gNext = 0.0;\n" + "default {\n" + " state_entry() { llSetTimerEvent(1); }\n" + " timer() { llSetTimerEvent(gNext); }\n" + " touch_start(integer n) { gNext = 2.0; }\n" + "}\n"); + CAPTURE(again.text); + { + INFO("set to what is not written nought, every"); + CHECK(has(again, "timerHandle = LLTimers:every(seconds, function")); + CHECK_FALSE(has(again, "LLTimers:once(seconds, function")); + } + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: integer / and % as LSL's via bit32.s32 and math.fmod; // and % if safe", "[ALLSLToSLua]") + { const char* const script = "integer gSize = 16;\n" "default { touch_start(integer n) {\n" " integer a = n - 3; integer b = 1 - n; float f = 2.5;\n" @@ -2173,24 +3094,47 @@ namespace tut " f = f / 2;\n" " llOwnerSay((string)(q + m + sum + back + cut + half + a + b) + (string)f);\n" "} }\n"; - const ALLSLToSLua::Result r = convert(script); - ensure("toward nought: " + r.text, has(r, "local q = bit32.s32(a / b)") && has(r, "local m = math.fmod(a, b)")); - ensure("bracketed as / binds: " + r.text, has(r, "bit32.s32((a + 1) / (b - 2)) + math.fmod(a + 1, b - 2)")); - ensure("a side below nought: " + r.text, has(r, "bit32.s32(-7 / 2) + math.fmod(7, -2)")); - ensure("neither below nought: " + r.text, has(r, "gSize // 4 + gSize % 3 + bit32.band(n, 255) // 16 + bit32.band(n, 255) % gSize")); - ensure("one known, not the other: " + r.text, has(r, "local half = bit32.s32(a / gSize)")); - ensure("compound ones whole: " + r.text, has(r, "a = bit32.s32(a / b)") && has(r, "a = math.fmod(a, b)") && has(r, "b = bit32.s32(b / 2)") && - has(r, "b = math.fmod(b, 3)") && !has(r, "//=") && !has(r, "%=")); - ensure("a float's: " + r.text, has(r, "f /= 2")); - ensure("unnoted: " + r.text, !noted(r, "SluaIntegerDivision") && !noted(r, "SluaModulo")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted(script); + CAPTURE(r.text); + { + INFO("toward nought"); + CHECK(has(r, "local q = bit32.s32(a / b)")); + CHECK(has(r, "local m = math.fmod(a, b)")); + } + UNSCOPED_INFO("bracketed as / binds"); + CHECK(has(r, "bit32.s32((a + 1) / (b - 2)) + math.fmod(a + 1, b - 2)")); + UNSCOPED_INFO("a side below nought"); + CHECK(has(r, "bit32.s32(-7 / 2) + math.fmod(7, -2)")); + UNSCOPED_INFO("neither below nought"); + CHECK(has(r, "gSize // 4 + gSize % 3 + bit32.band(n, 255) // 16 + bit32.band(n, 255) % gSize")); + UNSCOPED_INFO("one known, not the other"); + CHECK(has(r, "local half = bit32.s32(a / gSize)")); + { + INFO("compound ones whole"); + CHECK(has(r, "a = bit32.s32(a / b)")); + CHECK(has(r, "a = math.fmod(a, b)")); + CHECK(has(r, "b = bit32.s32(b / 2)")); + CHECK(has(r, "b = math.fmod(b, 3)")); + CHECK_FALSE(has(r, "//=")); + CHECK_FALSE(has(r, "%=")); + } + UNSCOPED_INFO("a float's"); + CHECK(has(r, "f /= 2")); + { + INFO("unnoted"); + CHECK_FALSE(noted(r, "SluaIntegerDivision")); + CHECK_FALSE(noted(r, "SluaModulo")); + } + require_checks_clean(r); // Typed, each one a number to the checker, strict under the grid's // solver: math.fmod's one answer and bit32.s32's. ALLSLToSLua::Options options; options.types = true; const ALLSLToSLua::Result typed = ALLSLToSLua::convert(script, options); - ensure("typed: " + typed.text, has(typed, "local q: number = bit32.s32(a / b)") && has(typed, "local m: number = math.fmod(a, b)")); - checksClean(typed); + CAPTURE(typed.text); + CHECK(has(typed, "local q: number = bit32.s32(a / b)")); + CHECK(has(typed, "local m: number = math.fmod(a, b)")); + require_checks_clean(typed); if (!newSolver) { std::string said; @@ -2198,491 +3142,231 @@ namespace tut { said += p.severity == ALScriptProblem::Severity::Error ? p.message + "\n" : std::string(); } - ensure("strict:\n" + said + "---\n" + typed.text, said.empty()); + CAPTURE(said); + CHECK(said.empty()); } } - template<> template<> - void allsltoslua_object::test<76>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: keys from empty or capital text noted; the XorBase64Strings pair noted", "[ALLSLToSLua]") { - set_test_name("a key cast from no text said to be NULL_KEY, and one with capitals said to be written small, cast or given; a key " - "given no text kept text; other text as it was said; the two XorBase64Strings said to be what ll.XorBase64 is not, " - "LSL's kept"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " key a = (key)\"\"; key c = (key)\"A2E76FCD-9360-4F6D-A924-938F923DF11D\";\n" - " if ((key)\"\" == NULL_KEY) llOwnerSay(\"same\");\n" - " string s = llGetObjectDesc(); key d = (key)s;\n" - " llOwnerSay((string)a + (string)c + (string)d + llXorBase64(\"a\", \"b\") + llXorBase64Strings(\"a\", \"b\") +\n" - " llXorBase64StringsCorrect(\"a\", \"b\"));\n" - "} }\n"); - ensure("no text, NULL_KEY: " + r.text, has(r, "local a = uuid(\"\")") && noted(r, "SluaKeyEmpty") && - has(r, "-- LSL: SLua's uuid of no text is NULL_KEY")); - ensure("capitals, small: " + r.text, has(r, "uuid(\"A2E76FCD-9360-4F6D-A924-938F923DF11D\")") && noted(r, "SluaKeyCase")); - ensure("other text as it was: " + r.text, has(r, "local d = uuid(s)") && noted(r, "SluaUuid")); - ensure("ll.XorBase64 as it is: " + r.text, has(r, "ll.XorBase64(\"a\", \"b\")")); - ensure("the two, LSL's: " + r.text, - has(r, "ll.XorBase64Strings(\"a\", \"b\")") && has(r, "ll.XorBase64StringsCorrect(\"a\", \"b\")") && !has(r, "llcompat.")); - ensure("said what ll.XorBase64 is not: " + r.text, noted(r, "SluaXorBase64Wrong") && noted(r, "SluaXorBase64Nul") && - !noted(r, "SluaDeprecated", "XorBase64Strings") && - !noted(r, "SluaDeprecated", "XorBase64StringsCorrect")); + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " key a = (key)\"\"; key c = (key)\"A2E76FCD-9360-4F6D-A924-938F923DF11D\";\n" + " if ((key)\"\" == NULL_KEY) llOwnerSay(\"same\");\n" + " string s = llGetObjectDesc(); key d = (key)s;\n" + " llOwnerSay((string)a + (string)c + (string)d + llXorBase64(\"a\", \"b\") + llXorBase64Strings(\"a\", \"b\") +\n" + " llXorBase64StringsCorrect(\"a\", \"b\"));\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("no text, NULL_KEY"); + CHECK(has(r, "local a = uuid(\"\")")); + CHECK(noted(r, "SluaKeyEmpty")); + CHECK(has(r, "-- LSL: SLua's uuid of no text is NULL_KEY")); + } + { + INFO("capitals, small"); + CHECK(has(r, "uuid(\"A2E76FCD-9360-4F6D-A924-938F923DF11D\")")); + CHECK(noted(r, "SluaKeyCase")); + } + { + INFO("other text as it was"); + CHECK(has(r, "local d = uuid(s)")); + CHECK(noted(r, "SluaUuid")); + } + UNSCOPED_INFO("ll.XorBase64 as it is"); + CHECK(has(r, "ll.XorBase64(\"a\", \"b\")")); + { + INFO("the two, LSL's"); + CHECK(has(r, "ll.XorBase64Strings(\"a\", \"b\")")); + CHECK(has(r, "ll.XorBase64StringsCorrect(\"a\", \"b\")")); + CHECK_FALSE(has(r, "llcompat.")); + } + { + INFO("said what ll.XorBase64 is not"); + CHECK(noted(r, "SluaXorBase64Wrong")); + CHECK(noted(r, "SluaXorBase64Nul")); + CHECK_FALSE(noted(r, "SluaDeprecated", "XorBase64Strings")); + CHECK_FALSE(noted(r, "SluaDeprecated", "XorBase64StringsCorrect")); + } for (const ALScriptProblem& p : r.notes) { if (p.key == "SluaXorBase64Wrong" || p.key == "SluaXorBase64Nul") { - ensure("named: " + p.message, p.message.find("for ll.XorBase64,") != std::string::npos); + CAPTURE(p.message); + REQUIRE(p.message.find("for ll.XorBase64,") != std::string::npos); } } - checksClean(r); + require_checks_clean(r); // Given rather than cast: no text kept as text, as other text that is // no UUID is; capitals the same said, and a UUID written small // nothing at all. - const ALLSLToSLua::Result given = convert("default { touch_start(integer n) {\n" - " key b = \"\"; key c = \"A2E76FCD-9360-4F6D-A924-938F923DF11D\";\n" - " key l = \"a2e76fcd-9360-4f6d-a924-938f923df11d\";\n" - " llOwnerSay((string)llGetOwnerKey(b) + (string)llGetOwnerKey(c) + (string)llGetOwnerKey(l));\n" - "} }\n"); - ensure("given no text, text: " + given.text, has(given, "local b = \"\"") && has(given, "ll.GetOwnerKey(b :: any)") && - !noted(given, "SluaKeyEmpty") && noted(given, "SluaKeyCase")); - ensure("nothing else said of them: " + given.text, !noted(given, "SluaKeyText") && !noted(given, "SluaUuidText")); - checksClean(given); - } - - template<> template<> - void allsltoslua_object::test<90>() - { - set_test_name("the //* toggle with a word after its star: ---[[ and the word over the code, --]] under it, which SLua reads as " - "two line comments, and a dash taken out of the opener as the block comment the closer closes"); - ALLSLToSLua::Options options; - options.types = true; - const ALLSLToSLua::Result r = ALLSLToSLua::convert("//* OVERRIDE\n" - "integer allowAttachedAO;\n" - "//*/\n" - "default { state_entry() { if (allowAttachedAO) llOwnerSay(\"on\"); } }\n", - options); - ensure("converted", r.converted); - ensure("the toggle: " + r.text, has(r, "\n---[[ OVERRIDE\nlocal allowAttachedAO: boolean = false\n--]]\n")); - ensure_equals("the opener a line comment", commentAt(r.text, "---[["), std::string("---[[ OVERRIDE")); - ensure_equals("the closer one", commentAt(r.text, "--]]"), std::string("--]]")); - ensure_equals("turned off", toggled(r.text, "---[["), std::string("--[[ OVERRIDE\nlocal allowAttachedAO: boolean = false\n--]]")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<91>() - { - set_test_name("what is half of a toggle: an opener only where a closer follows it, and no rule of stars nor a comment closed on " - "its line once a / is out; openers since the last closer share the next; a closer's own comment after it SLua's"); - const ALLSLToSLua::Result lone = convert("integer a;\n" - "//* nothing closes this\n" - "integer b;\n" - "default { state_entry() { llOwnerSay((string)(a + b)); } }\n"); - ensure("a lone opener a comment with a star: " + lone.text, has(lone, "\n--* nothing closes this\nlocal b = 0\n") && !has(lone, "---[")); - checksClean(lone); - - const ALLSLToSLua::Result r = convert("//* not one */\n" - "//**************\n" - "integer c = 1;\n" - "//* one\n" - "integer d = 2;\n" - "//* two\n" - "integer e = 3;\n" - "//*/ // both\n" - "//*/\n" - "default { state_entry() { llOwnerSay((string)(c + d + e)); } }\n"); - ensure("closed on its line: " + r.text, has(r, "\n--* not one */\n")); - ensure("a rule of stars: " + r.text, has(r, "\n--**************\n")); - ensure("two openers and a closer, then one alone: " + r.text, - has(r, "\n---[[ one\nlocal d = 2\n---[[ two\nlocal e = 3\n--]] -- both\n--]]\n")); - ensure_equals("the first off", toggled(r.text, "---[[ one"), std::string("--[[ one\nlocal d = 2\n---[[ two\nlocal e = 3\n--]]")); - ensure_equals("the second off", toggled(r.text, "---[[ two"), std::string("--[[ two\nlocal e = 3\n--]]")); - std::string off = r.text; - off.erase(off.find("---[[ one"), 1); - ensure_equals("the closer's own comment after it, off", commentAt(off, "-- both"), std::string("-- both")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<92>() - { - set_test_name("the toggle the other way, /* over the code and //*/ under it: --[[ and --]], which a dash put in the opener makes two " - "line comments again; and an opener over code before one, which shares its closer"); - const ALLSLToSLua::Result r = convert("/* OVERRIDE\n" - "integer allowAttachedAO;\n" - "//*/\n" - "//* A\n" - "integer x = 1;\n" - "/* B\n" - "integer y = 2;\n" - "//*/\n" - "default { state_entry() { llOwnerSay((string)x); } }\n"); - ensure_equals("one block comment", commentAt(r.text, "--[[ OVERRIDE"), std::string("--[[ OVERRIDE\ninteger allowAttachedAO;\n--]]")); - // What it holds stays LSL: nothing says it is code. - std::string on = r.text; - on.insert(on.find("--[[ OVERRIDE"), "-"); - ensure_equals("turned on, the opener a line comment", commentAt(on, "---[[ OVERRIDE"), std::string("---[[ OVERRIDE")); - ensure_equals("and the closer", commentAt(on, "--]]"), std::string("--]]")); - ensure_equals("the opener over code off, to the closer of the one after it", toggled(r.text, "---[[ A"), - std::string("--[[ A\nlocal x = 1\n\n--[[ B\ninteger y = 2;\n--]]")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<93>() - { - set_test_name("a toggle's level, the fewest =s nothing between its first opener and its closer closes once it is off: past a ]] " - "in a string or a comment in it, the opener and closer of one pair at one; nought where the script leaves no byte " - "to mark it with"); - const ALLSLToSLua::Result r = convert("//* OVERRIDE\n" - "string s = \"]]\";\n" - "//*/\n" - "default { state_entry() { llOwnerSay(s); } }\n"); - ensure("past a string's ]]: " + r.text, has(r, "\n---[=[ OVERRIDE\nlocal s = \"]]\"\n--]=]\n")); - ensure_equals("off", toggled(r.text, "---[=["), std::string("--[=[ OVERRIDE\nlocal s = \"]]\"\n--]=]")); - checksClean(r); - - const ALLSLToSLua::Result shared = convert("//* A\n" - "integer x = 1; /* one */\n" - "/* B\n" - "string s = \"]]\";\n" - "//*/\n" - "default { state_entry() { llOwnerSay((string)x); } }\n"); - ensure("one level for the pair: " + shared.text, has(shared, "\n---[=[ A\nlocal x = 1 --[[ one ]]\n") && - has(shared, "\n--[=[ B\nstring s = \"]]\";\n--]=]\n")); - ensure_equals("the opener off", toggled(shared.text, "---[=[ A"), - std::string("--[=[ A\nlocal x = 1 --[[ one ]]\n\n--[=[ B\nstring s = \"]]\";\n--]=]")); - checksClean(shared); - - std::string every = "// "; - for (char c = 1; c < ' '; ++c) + const ALLSLToSLua::Result given = require_converted("default { touch_start(integer n) {\n" + " key b = \"\"; key c = \"A2E76FCD-9360-4F6D-A924-938F923DF11D\";\n" + " key l = \"a2e76fcd-9360-4f6d-a924-938f923df11d\";\n" + " llOwnerSay((string)llGetOwnerKey(b) + (string)llGetOwnerKey(c) + (string)llGetOwnerKey(l));\n" + "} }\n"); + CAPTURE(given.text); { - if (c != '\t' && c != '\n' && c != '\r') - { - every += c; - } + INFO("given no text, text"); + CHECK(has(given, "local b = \"\"")); + CHECK(has(given, "ll.GetOwnerKey(b :: any)")); + CHECK_FALSE(noted(given, "SluaKeyEmpty")); + CHECK(noted(given, "SluaKeyCase")); + } + { + INFO("nothing else said of them"); + CHECK_FALSE(noted(given, "SluaKeyText")); + CHECK_FALSE(noted(given, "SluaUuidText")); } - const ALLSLToSLua::Result unmarked = convert(every + "\ninteger y;\n//* A\ninteger x = 1;\n//*/\n" - "default { state_entry() { llOwnerSay((string)(x + y)); } }\n"); - ensure("paired still: " + unmarked.text, has(unmarked, "\n---[[ A\nlocal x = 1\n--]]\n")); - ensure_equals("no byte of its own left in", count(unmarked, std::string(1, '\x01')), size_t(1)); - checksClean(unmarked); + require_checks_clean(given); } - template<> template<> - void allsltoslua_object::test<94>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a particle rule list written out as a ParticleParams table", "[ALLSLToSLua]") { - set_test_name("where a toggle sits: after code on its line, which stays when it is off, the closer after code it turns off; in a " - "handler; and from the script's first comments, kept off the helpers written over the code"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " integer a = 1; //* A\n" - " llOwnerSay((string)a); //*/\n" - " //* DEBUG\n" - " llOwnerSay(\"debug\");\n" - " //*/\n" - " llOwnerSay(\"done\");\n" - "} }\n"); - ensure("after code: " + r.text, has(r, "\nlocal a = 1 ---[[ A\nprint(tostring(a)) --]]\n")); - ensure_equals("off from it", toggled(r.text, "---[[ A"), std::string("--[[ A\nprint(tostring(a)) --]]")); - ensure("in a handler: " + r.text, has(r, "\n---[[ DEBUG\nprint(\"debug\")\n--]]\nprint(\"done\")\n")); - ensure_equals("off", toggled(r.text, "---[[ DEBUG"), std::string("--[[ DEBUG\nprint(\"debug\")\n--]]")); - checksClean(r); - - const ALLSLToSLua::Result top = convert("//* DEBUG\n" - "\n" - "vector v = <1, 2, 3>;\n" - "//*/\n" - "default { state_entry() { llOwnerSay((string)llVecNorm(v)); } }\n"); - ensure("the helper over it: " + top.text, has(top, "local function vecNorm(") && - top.text.find("local function vecNorm(") < top.text.find("---[[ DEBUG")); - ensure_equals("off", toggled(top.text, "---[[ DEBUG"), std::string("--[[ DEBUG\nlocal v = vector(1, 2, 3)\n--]]")); - checksClean(top); - } - - template<> template<> - void allsltoslua_object::test<85>() - { - set_test_name("a call the definitions deprecate, naming what to use where SLua's own word names nothing, is noted with it: " - "llMakeFire's ll.ParticleSystem, llSoundPreload's ll.PreloadSound; llXorBase64Strings' ll.XorBase64 in a note of " - "its own, which says how the two differ"); - const ALLSLToSLua::Result r = convert("default { state_entry() {\n" - " llMakeFire(10, 1.0, 1.0, 1.0, PI, \"\", ZERO_VECTOR);\n" - " llSoundPreload(\"boom\");\n" - " llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 1.0]);\n" - " llOwnerSay(llXorBase64Strings(\"YQ==\", \"Yg==\"));\n" - " llTakeCamera(llGetOwner());\n" - "} }\n"); - const auto named = [&r](const std::string& bare, const std::string& use) { - return std::any_of(r.notes.begin(), r.notes.end(), [&](const ALScriptProblem& p) { - return p.key == "SluaDeprecatedFor" && p.args.size() == 2 && p.args[0] == bare && p.args[1] == use; - }); - }; - ensure("each, for what it names: " + r.text, - named("MakeFire", "ll.ParticleSystem") && named("SoundPreload", "ll.PreloadSound") && - named("SetPrimitiveParams", "ll.SetLinkPrimitiveParamsFast") && named("TakeCamera", "ll.SetCameraParams")); - ensure("XorBase64Strings' own: " + r.text, noted(r, "SluaXorBase64Wrong") && !named("XorBase64Strings", "ll.XorBase64")); - ensure("said so: " + r.text, has(r, "-- LSL: SLua deprecates ll.MakeFire, for ll.ParticleSystem.\n")); - ensure("none said bare: " + r.text, !noted(r, "SluaDeprecated")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<55>() - { - set_test_name("lists appended with table.extend, which runs what it is given once and answers the list: a list by its name, " - "itself, a call's, many values; + in an expression on a copy of the left, but for a new one, said to hold any " - "value, and another +'s answer; no joinLists"); - const ALLSLToSLua::Result r = convert("list gKept = [1];\n" - "list kept() { return gKept; }\n" - "show(list l) { llOwnerSay(llDumpList2String(l, \",\")); }\n" - "default { touch_start(integer n) {\n" - " list all = llParseString2List(\"a,b\", [\",\"], []);\n" - " list more = [4, 5];\n" - " all += more;\n" - " all += all;\n" - " all += llList2List(more, 0, n);\n" - " all += [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, " - "26, 27, 28, 29, 30, 31, 32, \"x\"];\n" - " show(all);\n" - " show(kept() + more + all);\n" - " show(more + n);\n" - " show(n + more);\n" - " show([PRIM_NAME, \"x\"] + more);\n" - " show(llParseString2List(\"c\", [], []) + [1] + [<1, 2, 3>]);\n" - " llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_COLOR, ALL_SIDES, <1, 1, 1>, 1.0] + more);\n" - "} }\n"); - ensure("a list by its name: " + r.text, has(r, "table.extend(all, more)")); - ensure("itself: " + r.text, has(r, "table.extend(all, all)")); - ensure("a call's, run once: " + r.text, has(r, "table.extend(all, llcompat.List2List(more, 0, n))") && !has(r, "for _, item in")); - ensure("many values: " + r.text, has(r, "table.extend(all, { 0, 1, 2,") && has(r, "32, \"x\" })")); - ensure("one copy, of the variable a call answers: " + r.text, - has(r, "show(table.extend(table.extend(table.clone(kept()), more), all))")); - ensure("a variable copied: " + r.text, has(r, "show(table.extend(table.clone(more), { n }))")); - ensure("a value: " + r.text, has(r, "show(table.extend({ n } :: { any }, more))")); - ensure("written out: " + r.text, has(r, "show(table.extend({ PRIM_NAME, \"x\" } :: { any }, more))") && - has(r, "table.extend({ PRIM_COLOR, ALL_SIDES, vector(1, 1, 1), 1.0 } :: { any }, more)")); - ensure("a library call's: " + r.text, - has(r, "show(table.extend(table.extend(ll.ParseString2List(\"c\", {}, {}) :: { any }, { 1 }), { vector(1, 2, 3) }))")); - ensure("no helper, nor a move: " + r.text, !has(r, "joinLists") && !has(r, "table.move(")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<56>() - { - set_test_name("list forms through SLua's own where they do what LSL's did: an item replaced at a counter within the list, in " - "place, its loop Luau's numeric for, the length kept; one put on the end; a part between two places written out; " - "text and whole numbers joined; one thing's place found; llcompat's where not"); - const ALLSLToSLua::Result r = convert("list gNames = [\"a\", \"b\"];\n" - "list gKept;\n" - "list gL = [1, 2, 3];\n" - "integer bump() { gL += [0]; return 1; }\n" - "default { touch_start(integer n) {\n" - " list l = [1, 2, 3];\n" - " integer i;\n" - " for (i = 0; i < llGetListLength(l); ++i)\n" - " l = llListReplaceList(l, [llList2Integer(l, i) * 2], i, i);\n" - " list held = [1, 2];\n" - " gKept = held;\n" - " integer j;\n" - " for (j = 0; j < llGetListLength(held); ++j)\n" - " held = llListReplaceList(held, [j], j, j);\n" - " integer k;\n" - " for (k = 0; k < llGetListLength(gL); ++k)\n" - " gL = llListReplaceList(gL, [bump()], k, k);\n" - " l = llListReplaceList(l, [7], -1, -1);\n" - " l = llListInsertList(l, [9], llGetListLength(l));\n" - " list part = llList2List(l, 1, 2);\n" - " list all = llList2List(l, 0, -1);\n" - " list rest = llList2List(l, 2, -1);\n" - " list wrapped = llList2List(l, 3, 1);\n" - " llOwnerSay(llDumpList2String(l, \",\"));\n" - " llOwnerSay(llDumpList2String(gNames + [n], \", \"));\n" - " llOwnerSay(llDumpList2String(l + [1.5], \",\"));\n" - " llOwnerSay((string)l);\n" - " integer at = llListFindList(gNames, [\"b\"]);\n" - " integer none = llListFindList(gNames, [NULL_KEY]);\n" - " if (llListFindList(gNames, [NULL_KEY]) != -1) llOwnerSay(\"null\");\n" - " integer parsed = llListFindList(llParseString2List(\"a,b\", [\",\"], []), [\"b\"]);\n" - " llOwnerSay((string)(at + none + parsed) + llDumpList2String(part + all + rest + wrapped + held, \"\"));\n" - "} }\n"); - ensure("replaced in place, in Luau's numeric for: " + r.text, has(r, "for i = 0, #l - 1 do\n l[i + 1] = l[i + 1] * 2\n")); - ensure("a list held elsewhere, replaced as LSL's: " + r.text, - has(r, "for j = 0, #held - 1 do\n") && has(r, " held = llcompat.ListReplaceList(held, { j }, j, j)\n")); - ensure("a global a call of the script's could change: " + r.text, has(r, "while k < #gL do") && - has(r, "gL = llcompat.ListReplaceList(gL, { bump() }, k, k)")); - ensure("the last of a list that could be empty: " + r.text, has(r, "l = llcompat.ListReplaceList(l, { 7 }, -1, -1)")); - ensure("on the end: " + r.text, has(r, "table.insert(l, 9)")); - ensure("a part, and all of it: " + r.text, has(r, "local part = table.move(l, 2, 3, 1, {})") && has(r, "local all = table.clone(l)")); - ensure("to the end, and the part left out, as LSL's: " + r.text, - has(r, "local rest = llcompat.List2List(l, 2, -1)") && has(r, "local wrapped = llcompat.List2List(l, 3, 1)")); - ensure("whole numbers joined: " + r.text, has(r, "print(table.concat(l, \",\"))") && has(r, "print(table.concat(l))")); - ensure("and text: " + r.text, has(r, "print(table.concat(table.extend(table.clone(gNames), { n }), \", \"))")); - ensure("a float as LSL writes one: " + r.text, has(r, "print(ll.DumpList2String(table.extend(table.clone(l), { 1.5 }), \",\"))")); - ensure("found, its place: " + r.text, has(r, "local at = (table.find(gNames, \"b\") or 0) - 1")); - ensure("NULL_KEY as LSL's string, as its place and found: " + r.text, - has(r, "local none = (table.find(gNames, tostring(NULL_KEY)) or 0) - 1") && - has(r, "if table.find(gNames, tostring(NULL_KEY)) ~= nil then")); - ensure("in a library call's list: " + r.text, has(r, "local parsed = (table.find(ll.ParseString2List(\"a,b\", { \",\" }, {}) :: { any }, \"b\") or 0) - 1")); - ensure("no llcompat but those: " + r.text, count(r, "llcompat.") == 5); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<57>() - { - set_test_name("llSubStringIndex asked only whether it found: string.find's plain search, each way LSL asks it, a key as its " - "text, the empty text found; its index still ll's"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " string s = llGetObjectName(); key k = llGetOwner();\n" - " if (llSubStringIndex(s, \"a\") != -1) llOwnerSay(\"a\");\n" - " if (llSubStringIndex(s, \"b\") >= 0) llOwnerSay(\"b\");\n" - " if (llSubStringIndex(s, \"c\") > -1) llOwnerSay(\"c\");\n" - " if (llSubStringIndex(s, \"d\") == -1) llOwnerSay(\"no d\");\n" - " if (!~llSubStringIndex(s, \"e\")) llOwnerSay(\"no e\");\n" - " if (~llSubStringIndex(s, \".\")) llOwnerSay(\"dot\");\n" - " if (~llSubStringIndex(s, k)) llOwnerSay(\"owner\");\n" - " if (~llSubStringIndex(s, \"\")) llOwnerSay(\"empty\");\n" - " integer at = llSubStringIndex(s, \"x\");\n" - " llOwnerSay((string)at);\n" - "} }\n"); - ensure("found: " + r.text, has(r, "if string.find(s, \"a\", 1, true) ~= nil then") && has(r, "if string.find(s, \"b\", 1, true) ~= nil then") && - has(r, "if string.find(s, \"c\", 1, true) ~= nil then")); - ensure("not: " + r.text, has(r, "if string.find(s, \"d\", 1, true) == nil then") && has(r, "if not (string.find(s, \"e\", 1, true) ~= nil) then")); - ensure("plain, not a pattern: " + r.text, has(r, "if string.find(s, \".\", 1, true) ~= nil then")); - ensure("a key as its text: " + r.text, has(r, "if string.find(s, tostring(k), 1, true) ~= nil then")); - ensure("the empty text: " + r.text, has(r, "if string.find(s, \"\", 1, true) ~= nil then")); - ensure("its index in characters, ll's: " + r.text, has(r, "local at = (ll.SubStringIndex(s, \"x\") or 0) - 1")); - ensure("found by string.find alone: " + r.text, count(r, "SubStringIndex") == 1); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<77>() - { - set_test_name("a particle rule list written out as the ParticleParams table ll.ParticleSystem takes: each rule by the key the " - "definitions give it, its value as the list had it, the flags' masks each true, a key to a line where the list had " - "more than one"); - const ALLSLToSLua::Result r = convert("integer gFlags = 3;\n" - "default { touch_start(integer n) {\n" - " llParticleSystem([\n" - " PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_INTERP_COLOR_MASK,\n" - " PSYS_PART_START_COLOR, <1, 0, 0>, PSYS_PART_START_ALPHA, 1,\n" - " PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE,\n" - " PSYS_SRC_TEXTURE, NULL_KEY, PSYS_SRC_TARGET_KEY, llGetOwner()\n" - " ]);\n" - " llLinkParticleSystem(LINK_SET, [PSYS_PART_FLAGS, gFlags, PSYS_SRC_BURST_RATE, 0.1]);\n" - " llLinkParticleSystem(2, [PSYS_PART_FLAGS, PSYS_PART_BOUNCE_MASK]);\n" - " llParticleSystem([]);\n" - "} }\n"); - ensure("a key to a line: " + r.text, has(r, " ll.ParticleSystem({\n" - " emissive = true,\n" - " color_interp = true,\n" - " color_begin = vector(1, 0, 0),\n" - " alpha_begin = 1,\n" - " pattern = PSYS_SRC_PATTERN_EXPLODE,\n" - " texture = tostring(NULL_KEY),\n" - " target_key = ll.GetOwner(),\n" - " })\n")); - ensure("on one line as written; flags not of masks as they are: " + r.text, - has(r, "ll.LinkParticleSystem(LINK_SET, { flags = gFlags, burst_rate = 0.1 })") && has(r, "ll.LinkParticleSystem(2, { bounce = true })")); - ensure("no rules, as they were: " + r.text, has(r, "ll.ParticleSystem({})")); + const ALLSLToSLua::Result r = require_converted("integer gFlags = 3;\n" + "default { touch_start(integer n) {\n" + " llParticleSystem([\n" + " PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_INTERP_COLOR_MASK,\n" + " PSYS_PART_START_COLOR, <1, 0, 0>, PSYS_PART_START_ALPHA, 1,\n" + " PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE,\n" + " PSYS_SRC_TEXTURE, NULL_KEY, PSYS_SRC_TARGET_KEY, llGetOwner()\n" + " ]);\n" + " llLinkParticleSystem(LINK_SET, [PSYS_PART_FLAGS, gFlags, PSYS_SRC_BURST_RATE, 0.1]);\n" + " llLinkParticleSystem(2, [PSYS_PART_FLAGS, PSYS_PART_BOUNCE_MASK]);\n" + " llParticleSystem([]);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a key to a line"); + CHECK(has(r, " ll.ParticleSystem({\n" + " emissive = true,\n" + " color_interp = true,\n" + " color_begin = vector(1, 0, 0),\n" + " alpha_begin = 1,\n" + " pattern = PSYS_SRC_PATTERN_EXPLODE,\n" + " texture = tostring(NULL_KEY),\n" + " target_key = ll.GetOwner(),\n" + " })\n")); + { + INFO("on one line as written; flags not of masks as they are"); + CHECK(has(r, "ll.LinkParticleSystem(LINK_SET, { flags = gFlags, burst_rate = 0.1 })")); + CHECK(has(r, "ll.LinkParticleSystem(2, { bounce = true })")); + } + UNSCOPED_INFO("no rules, as they were"); + CHECK(has(r, "ll.ParticleSystem({})")); const auto same = [&r](const std::string& lsl, const std::string& slua) { return std::find(r.same.begin(), r.same.end(), std::make_pair(lsl, slua)) != r.same.end(); }; - ensure("each constant the same word as its key", same("PSYS_PART_START_COLOR", "color_begin") && same("PSYS_PART_EMISSIVE_MASK", "emissive") && - same("PSYS_SRC_BURST_RATE", "burst_rate")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<78>() - { - set_test_name("a media rule list as MediaParams: TRUE and FALSE, 1 and 0, as booleans, a whitelist's text as its pieces where " - "the serializer joins them back the same; a list where it would not, or a truth is no 1 or 0 written out"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " integer on = llGetLinkNumber();\n" - " llSetPrimMediaParams(0, [PRIM_MEDIA_AUTO_PLAY, TRUE, PRIM_MEDIA_AUTO_LOOP, 0, PRIM_MEDIA_CURRENT_URL, \"http://a.com\",\n" - " PRIM_MEDIA_WIDTH_PIXELS, 512, PRIM_MEDIA_WHITELIST_ENABLE, 1, PRIM_MEDIA_WHITELIST, \"a.com,*.b.com,\"]);\n" - " if (llSetLinkMedia(LINK_THIS, 1, [PRIM_MEDIA_HOME_URL, \"http://h\"]) == STATUS_OK) llOwnerSay(\"ok\");\n" - " llSetPrimMediaParams(1, [PRIM_MEDIA_WHITELIST, \",a.com\"]);\n" - " llSetPrimMediaParams(2, [PRIM_MEDIA_WHITELIST, \"a\\\\,b\"]);\n" - " llSetPrimMediaParams(3, [PRIM_MEDIA_WHITELIST, \"\"]);\n" - " llSetPrimMediaParams(4, [PRIM_MEDIA_AUTO_PLAY, on]);\n" - " llSetPrimMediaParams(5, [PRIM_MEDIA_AUTO_ZOOM, 2]);\n" - "} }\n"); - ensure("a table: " + r.text, has(r, " ll.SetPrimMediaParams(0, {\n" - " auto_play = true,\n" - " auto_loop = false,\n" - " current_url = \"http://a.com\",\n" - " width = 512,\n" - " whitelist_enable = true,\n" - " whitelist = { \"a.com\", \"*.b.com\", \"\" },\n" - " })\n")); - ensure("what it answers read: " + r.text, has(r, "if ll.SetLinkMedia(LINK_THIS, 1, { home_url = \"http://h\" }) == STATUS_OK then")); + { + INFO("each constant the same word as its key"); + CHECK(same("PSYS_PART_START_COLOR", "color_begin")); + CHECK(same("PSYS_PART_EMISSIVE_MASK", "emissive")); + CHECK(same("PSYS_SRC_BURST_RATE", "burst_rate")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a media rule list as MediaParams with booleans and whitelist pieces", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " integer on = llGetLinkNumber();\n" + " llSetPrimMediaParams(0, [PRIM_MEDIA_AUTO_PLAY, TRUE, PRIM_MEDIA_AUTO_LOOP, 0, PRIM_MEDIA_CURRENT_URL, \"http://a.com\",\n" + " PRIM_MEDIA_WIDTH_PIXELS, 512, PRIM_MEDIA_WHITELIST_ENABLE, 1, PRIM_MEDIA_WHITELIST, \"a.com,*.b.com,\"]);\n" + " if (llSetLinkMedia(LINK_THIS, 1, [PRIM_MEDIA_HOME_URL, \"http://h\"]) == STATUS_OK) llOwnerSay(\"ok\");\n" + " llSetPrimMediaParams(1, [PRIM_MEDIA_WHITELIST, \",a.com\"]);\n" + " llSetPrimMediaParams(2, [PRIM_MEDIA_WHITELIST, \"a\\\\,b\"]);\n" + " llSetPrimMediaParams(3, [PRIM_MEDIA_WHITELIST, \"\"]);\n" + " llSetPrimMediaParams(4, [PRIM_MEDIA_AUTO_PLAY, on]);\n" + " llSetPrimMediaParams(5, [PRIM_MEDIA_AUTO_ZOOM, 2]);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a table"); + CHECK(has(r, " ll.SetPrimMediaParams(0, {\n" + " auto_play = true,\n" + " auto_loop = false,\n" + " current_url = \"http://a.com\",\n" + " width = 512,\n" + " whitelist_enable = true,\n" + " whitelist = { \"a.com\", \"*.b.com\", \"\" },\n" + " })\n")); + UNSCOPED_INFO("what it answers read"); + CHECK(has(r, "if ll.SetLinkMedia(LINK_THIS, 1, { home_url = \"http://h\" }) == STATUS_OK then")); // The join leaves a first piece of none out, escapes a \, and sends // no rule at all for no text. - ensure("a whitelist the join would change, a list: " + r.text, - has(r, "ll.SetPrimMediaParams(1, { PRIM_MEDIA_WHITELIST, \",a.com\" })") && has(r, "ll.SetPrimMediaParams(2, { PRIM_MEDIA_WHITELIST, \"a") && - has(r, "ll.SetPrimMediaParams(3, { PRIM_MEDIA_WHITELIST, \"\" })")); - ensure("a truth not written out as one, a list: " + r.text, - has(r, "ll.SetPrimMediaParams(4, { PRIM_MEDIA_AUTO_PLAY, on })") && has(r, "ll.SetPrimMediaParams(5, { PRIM_MEDIA_AUTO_ZOOM, 2 })")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<79>() - { - set_test_name("an HTTP rule list as HttpRequestParams: headers of names each once by name, said where the serializer's order " - "by name is not the order written; the accepts in order; a list for a name twice, a name not written out, or a " - "value moved past one that changes something"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " string token = llGetObjectDesc();\n" - " llHTTPRequest(\"https://x\", [HTTP_METHOD, \"POST\", HTTP_MIMETYPE, \"application/json\", HTTP_VERIFY_CERT, FALSE,\n" - " HTTP_CUSTOM_HEADER, \"X-B\", token, HTTP_BODY_MAXLENGTH, 16384, HTTP_CUSTOM_HEADER, \"X-A\", \"two\",\n" - " HTTP_ACCEPT, \"application/json\", HTTP_ACCEPT, \"text/plain\"], \"{}\");\n" - "} }\n"); - ensure("a table: " + r.text, has(r, " ll.HTTPRequest(\"https://x\", {\n" - " method = \"POST\",\n" - " mimetype = \"application/json\",\n" - " verify_cert = false,\n" - " custom_header = { [\"X-B\"] = token, [\"X-A\"] = \"two\" },\n" - " max_body_length = 16384,\n" - " accept = { \"application/json\", \"text/plain\" },\n" - " }, \"{}\")\n")); - ensure("the headers' order said: " + r.text, noted(r, "SluaHeaderOrder") && has(r, "-- LSL: SLua passes ll.HTTPRequest a custom_header table's headers sorted by name")); - checksClean(r); - - const ALLSLToSLua::Result sorted = convert("default { touch_start(integer n) {\n" - " key id = llHTTPRequest(\"https://y\", [HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"B\", \"2\"], \"\");\n" - " llOwnerSay((string)id);\n" - "} }\n"); - ensure("in order by name, nothing said: " + sorted.text, - has(sorted, "local id = ll.HTTPRequest(\"https://y\", { custom_header = { [\"A\"] = \"1\", [\"B\"] = \"2\" } }, \"\")") && - !noted(sorted, "SluaHeaderOrder")); - checksClean(sorted); - - const ALLSLToSLua::Result lists = convert("integer gCount;\n" - "string counted() { ++gCount; return (string)gCount; }\n" - "default { touch_start(integer n) {\n" - " string token = llGetObjectDesc();\n" - " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\"], \"\");\n" - " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, token, \"1\"], \"\");\n" - " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" - " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" - "} }\n"); - ensure("a name twice, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\" }, \"\")")); - ensure("a name not written out, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, token, \"1\" }, \"\")")); - ensure("moved past another, a list: " + lists.text, has(lists, "{ HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted() }")); - ensure("one after another, run in their order: " + lists.text, has(lists, "{ custom_header = { [\"A\"] = counted(), [\"B\"] = counted() } }")); - checksClean(lists); - } - - template<> template<> - void allsltoslua_object::test<80>() - { - set_test_name("a rule list the table could not say just so stays a list: not written out, a key twice, a rule of another table " - "or a number, a value of another shape, a rule with no value; flags that are not an | of masks each once, as they " - "are; none at all without SLua's idioms"); + { + INFO("a whitelist the join would change, a list"); + CHECK(has(r, "ll.SetPrimMediaParams(1, { PRIM_MEDIA_WHITELIST, \",a.com\" })")); + CHECK(has(r, "ll.SetPrimMediaParams(2, { PRIM_MEDIA_WHITELIST, \"a")); + CHECK(has(r, "ll.SetPrimMediaParams(3, { PRIM_MEDIA_WHITELIST, \"\" })")); + } + { + INFO("a truth not written out as one, a list"); + CHECK(has(r, "ll.SetPrimMediaParams(4, { PRIM_MEDIA_AUTO_PLAY, on })")); + CHECK(has(r, "ll.SetPrimMediaParams(5, { PRIM_MEDIA_AUTO_ZOOM, 2 })")); + } + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: an HTTP rule list as HttpRequestParams with headers by name", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " string token = llGetObjectDesc();\n" + " llHTTPRequest(\"https://x\", [HTTP_METHOD, \"POST\", HTTP_MIMETYPE, \"application/json\", HTTP_VERIFY_CERT, FALSE,\n" + " HTTP_CUSTOM_HEADER, \"X-B\", token, HTTP_BODY_MAXLENGTH, 16384, HTTP_CUSTOM_HEADER, \"X-A\", \"two\",\n" + " HTTP_ACCEPT, \"application/json\", HTTP_ACCEPT, \"text/plain\"], \"{}\");\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("a table"); + CHECK(has(r, " ll.HTTPRequest(\"https://x\", {\n" + " method = \"POST\",\n" + " mimetype = \"application/json\",\n" + " verify_cert = false,\n" + " custom_header = { [\"X-B\"] = token, [\"X-A\"] = \"two\" },\n" + " max_body_length = 16384,\n" + " accept = { \"application/json\", \"text/plain\" },\n" + " }, \"{}\")\n")); + { + INFO("the headers' order said"); + CHECK(noted(r, "SluaHeaderOrder")); + CHECK(has(r, "-- LSL: SLua passes ll.HTTPRequest a custom_header table's headers sorted by name")); + } + require_checks_clean(r); + + const ALLSLToSLua::Result sorted = require_converted("default { touch_start(integer n) {\n" + " key id = llHTTPRequest(\"https://y\", [HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"B\", \"2\"], \"\");\n" + " llOwnerSay((string)id);\n" + "} }\n"); + CAPTURE(sorted.text); + { + INFO("in order by name, nothing said"); + CHECK(has(sorted, "local id = ll.HTTPRequest(\"https://y\", { custom_header = { [\"A\"] = \"1\", [\"B\"] = \"2\" } }, \"\")")); + CHECK_FALSE(noted(sorted, "SluaHeaderOrder")); + } + require_checks_clean(sorted); + + const ALLSLToSLua::Result lists = require_converted("integer gCount;\n" + "string counted() { ++gCount; return (string)gCount; }\n" + "default { touch_start(integer n) {\n" + " string token = llGetObjectDesc();\n" + " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\"], \"\");\n" + " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, token, \"1\"], \"\");\n" + " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" + " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" + "} }\n"); + CAPTURE(lists.text); + UNSCOPED_INFO("a name twice, a list"); + CHECK(has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\" }, \"\")")); + UNSCOPED_INFO("a name not written out, a list"); + CHECK(has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, token, \"1\" }, \"\")")); + UNSCOPED_INFO("moved past another, a list"); + CHECK(has(lists, "{ HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted() }")); + UNSCOPED_INFO("one after another, run in their order"); + CHECK(has(lists, "{ custom_header = { [\"A\"] = counted(), [\"B\"] = counted() } }")); + require_checks_clean(lists); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a rule list the table cannot say just so stays a list", "[ALLSLToSLua]") + { const char* const script = "list gRules = [PSYS_PART_MAX_AGE, 2.0];\n" "default { touch_start(integer n) {\n" " llParticleSystem(gRules);\n" @@ -2695,28 +3379,41 @@ namespace tut " llLinkParticleSystem(2, [PSYS_PART_FLAGS, PSYS_PART_WIND_MASK | PSYS_PART_WIND_MASK]);\n" " llLinkParticleSystem(3, [PSYS_PART_FLAGS, PSYS_PART_WIND_MASK | (PSYS_PART_BOUNCE_MASK | PSYS_PART_RIBBON_MASK)]);\n" "} }\n"; - const ALLSLToSLua::Result r = convert(script); - ensure("not written out: " + r.text, has(r, "ll.ParticleSystem(gRules)")); - ensure("a key twice: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE, 2.0, PSYS_PART_MAX_AGE, 3.0 })")); - ensure("another table's rule: " + r.text, has(r, "ll.ParticleSystem({ PRIM_MEDIA_AUTO_PLAY, 1 })")); - ensure("a number for a rule: " + r.text, has(r, "ll.ParticleSystem({ 7, 2.0 })")); - ensure("a value of another shape: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_START_ALPHA, vector(1, 0, 0) })")); - ensure("no value: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE })")); - ensure("flags as they are: " + r.text, has(r, "ll.LinkParticleSystem(1, { flags = PSYS_PART_WIND_MASK + PSYS_PART_BOUNCE_MASK })") && - has(r, "ll.LinkParticleSystem(2, { flags = bit32.bor(PSYS_PART_WIND_MASK, PSYS_PART_WIND_MASK) })")); - ensure("masks bracketed, each true: " + r.text, has(r, "ll.LinkParticleSystem(3, { wind = true, bounce = true, ribbon = true })")); + const ALLSLToSLua::Result r = require_converted(script); + CAPTURE(r.text); + UNSCOPED_INFO("not written out"); + CHECK(has(r, "ll.ParticleSystem(gRules)")); + UNSCOPED_INFO("a key twice"); + CHECK(has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE, 2.0, PSYS_PART_MAX_AGE, 3.0 })")); + UNSCOPED_INFO("another table's rule"); + CHECK(has(r, "ll.ParticleSystem({ PRIM_MEDIA_AUTO_PLAY, 1 })")); + UNSCOPED_INFO("a number for a rule"); + CHECK(has(r, "ll.ParticleSystem({ 7, 2.0 })")); + UNSCOPED_INFO("a value of another shape"); + CHECK(has(r, "ll.ParticleSystem({ PSYS_PART_START_ALPHA, vector(1, 0, 0) })")); + UNSCOPED_INFO("no value"); + CHECK(has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE })")); + { + INFO("flags as they are"); + CHECK(has(r, "ll.LinkParticleSystem(1, { flags = PSYS_PART_WIND_MASK + PSYS_PART_BOUNCE_MASK })")); + CHECK(has(r, "ll.LinkParticleSystem(2, { flags = bit32.bor(PSYS_PART_WIND_MASK, PSYS_PART_WIND_MASK) })")); + } + UNSCOPED_INFO("masks bracketed, each true"); + CHECK(has(r, "ll.LinkParticleSystem(3, { wind = true, bounce = true, ribbon = true })")); ALLSLToSLua::Options plain; plain.idioms = false; const ALLSLToSLua::Result lists = ALLSLToSLua::convert(script, plain); - ensure("no idioms, no tables: " + lists.text, has(lists, "ll.LinkParticleSystem(3, { PSYS_PART_FLAGS, ") && !has(lists, "wind = true")); + CAPTURE(lists.text); + { + INFO("no idioms, no tables"); + CHECK(has(lists, "ll.LinkParticleSystem(3, { PSYS_PART_FLAGS, ")); + CHECK_FALSE(has(lists, "wind = true")); + } } - template<> template<> - void allsltoslua_object::test<81>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: every rule of the three tables matches the definitions' types", "[ALLSLToSLua]") { - set_test_name("every rule of the three tables at once: each key one the definitions' type has, every key the type has written, " - "and strict under the grid's solver"); - const ALLSLToSLua::Result r = convert( + const ALLSLToSLua::Result r = require_converted( "default { touch_start(integer n) {\n" " key k = llGetOwner();\n" " llParticleSystem([PSYS_PART_FLAGS, PSYS_PART_INTERP_COLOR_MASK | PSYS_PART_INTERP_SCALE_MASK | PSYS_PART_BOUNCE_MASK |\n" @@ -2740,6 +3437,7 @@ namespace tut " HTTP_VERBOSE_THROTTLE, FALSE, HTTP_CUSTOM_HEADER, \"X-A\", \"1\", HTTP_PRAGMA_NO_CACHE, TRUE, HTTP_USER_AGENT, \"me\",\n" " HTTP_ACCEPT, \"text/plain\", HTTP_EXTENDED_ERROR, TRUE], \"\");\n" "} }\n"); + CAPTURE(r.text); // The keys each table was written with, a line each. std::map> written; { @@ -2767,7 +3465,8 @@ namespace tut { const std::string text = defs.str(); const size_t begin = text.find(std::string("export type ") + type + " = {"); - ensure(std::string("the definitions have ") + type, begin != std::string::npos); + UNSCOPED_INFO("the definitions have " << type); + REQUIRE(begin != std::string::npos); const std::string block = text.substr(begin, text.find("\n}", begin) - begin); std::set fields; std::istringstream lines(block); @@ -2791,10 +3490,12 @@ namespace tut { unknown += fields.contains(key) ? "" : " " + key; } - ensure(std::string(type) + " has every key written:" + unknown + "\n" + r.text, unknown.empty()); - ensure(std::string(type) + " written with every key it has:" + missing + "\n" + r.text, missing.empty()); + UNSCOPED_INFO(type << " has every key written:" << unknown); + REQUIRE(unknown.empty()); + UNSCOPED_INFO(type << " written with every key it has:" << missing); + REQUIRE(missing.empty()); } - checksClean(r); + require_checks_clean(r); if (!newSolver) { std::string said; @@ -2802,26 +3503,23 @@ namespace tut { said += p.severity == ALScriptProblem::Severity::Error ? p.message + "\n" : std::string(); } - ensure("strict:\n" + said + "---\n" + r.text, said.empty()); + CAPTURE(said); + CHECK(said.empty()); } } } -namespace tut +namespace { - template<> template<> - void allsltoslua_object::test<82>() - { - set_test_name("a list the LSL wrote over several lines is written over the same lines, nested ones a level further in, each " - "comment in it where it was; a comment after it on the line it ends trails that line; a list on one line, or in " - "a line made inside an expression, stays on one"); - const ALLSLToSLua::Result r = convert("list gColours = [\n" - " // warm\n" - " \"red\", \"orange\",\n" - " /* cool */ \"blue\" // last\n" - "];\n" - "list gTwo = [1, 2,\n" - " 3]; // three\n" + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a list written over several lines keeps its lines and comments", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("list gColours = [\n" + " // warm\n" + " \"red\", \"orange\",\n" + " /* cool */ \"blue\" // last\n" + "];\n" + "list gTwo = [1, 2,\n" + " 3]; // three\n" "default { touch_start(integer n) {\n" " string activeRequest = llList2Json(JSON_OBJECT, [\n" " \"fn\", \"SELECT\",\n" @@ -2849,12 +3547,13 @@ namespace tut " PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_DROP\n" " ]);\n" "} }\n"); - ensure("a global's, its comments where they were: " + r.text, - has(r, "local gColours = {\n -- warm\n \"red\", \"orange\",\n --[[ cool ]] \"blue\" -- last\n}\n")); - ensure("closed on the line it closed on, a comment after it trailing that: " + r.text, - has(r, "local gTwo = { 1, 2,\n 3 } -- three\n")); - ensure("Tapple's, as the LSL had it: " + r.text, - has(r, " local activeRequest = ll.List2Json(JSON_OBJECT, {\n" + CAPTURE(r.text); + UNSCOPED_INFO("a global's, its comments where they were"); + CHECK(has(r, "local gColours = {\n -- warm\n \"red\", \"orange\",\n --[[ cool ]] \"blue\" -- last\n}\n")); + UNSCOPED_INFO("closed on the line it closed on, a comment after it trailing that"); + CHECK(has(r, "local gTwo = { 1, 2,\n 3 } -- three\n")); + UNSCOPED_INFO("Tapple's, as the LSL had it"); + CHECK(has(r, " local activeRequest = ll.List2Json(JSON_OBJECT, {\n" " \"fn\", \"SELECT\",\n" " \"radius\", 10,\n" " \"ima\", ll.List2Json(JSON_ARRAY, { \"Ungulate\" }),\n" @@ -2867,47 +3566,55 @@ namespace tut " }),\n" " \"responders\", \"[]\"\n" " }) -- sent\n")); - ensure("each comment once: " + r.text, count(r, "only the owner") == 1 && count(r, "-- sent") == 1 && count(r, "-- first") == 1); - ensure("in a line made inside an expression, one line: " + r.text, has(r, "(function() m = { 3, 4 } return m end)()")); - ensure("in a condition: " + r.text, has(r, " if table.find({ \"a\", -- first\n \"b\" }")); - ensure("one written another way, its comment over it: " + r.text, has(r, " -- none\n ll.ParticleSystem({\n")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<83>() - { - set_test_name("a detected event's count made of the detected table only where the body names it: none that nothing reads, which " - "a lint would ask to take out"); - const ALLSLToSLua::Result r = convert("selectHorse() {}\n" - "default {\n" - " touch_start (integer num) {\n" - " if (llDetectedKey(0) == llGetOwner()) {\n" - " selectHorse();\n" - " }\n" - " }\n" - " collision_start(integer n) { llOwnerSay((string)n); }\n" - " sensor(integer found) { found = 0; }\n" - "}\n"); - ensure("none that nothing reads: " + r.text, - has(r, "LLEvents:on(\"touch_start\", function(detected)\n -- LSL: detected[n]") && !has(r, "local num")); - ensure("one read: " + r.text, has(r, "function(detected)\n local n = #detected\n print(tostring(n))\n")); - ensure("one set, which needs it there: " + r.text, has(r, " local found = #detected\n found = 0\n")); + { + INFO("each comment once"); + CHECK(count(r, "only the owner") == 1); + CHECK(count(r, "-- sent") == 1); + CHECK(count(r, "-- first") == 1); + } + UNSCOPED_INFO("in a line made inside an expression, one line"); + CHECK(has(r, "(function() m = { 3, 4 } return m end)()")); + UNSCOPED_INFO("in a condition"); + CHECK(has(r, " if table.find({ \"a\", -- first\n \"b\" }")); + UNSCOPED_INFO("one written another way, its comment over it"); + CHECK(has(r, " -- none\n ll.ParticleSystem({\n")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a detected event's count only declared where the body names it", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("selectHorse() {}\n" + "default {\n" + " touch_start (integer num) {\n" + " if (llDetectedKey(0) == llGetOwner()) {\n" + " selectHorse();\n" + " }\n" + " }\n" + " collision_start(integer n) { llOwnerSay((string)n); }\n" + " sensor(integer found) { found = 0; }\n" + "}\n"); + CAPTURE(r.text); + { + INFO("none that nothing reads"); + CHECK(has(r, "LLEvents:on(\"touch_start\", function(detected)\n -- LSL: detected[n]")); + CHECK_FALSE(has(r, "local num")); + } + UNSCOPED_INFO("one read"); + CHECK(has(r, "function(detected)\n local n = #detected\n print(tostring(n))\n")); + UNSCOPED_INFO("one set, which needs it there"); + CHECK(has(r, " local found = #detected\n found = 0\n")); // Unused, of what Luau says: only what the LSL itself never read. std::string unused; for (const ALScriptProblem& p : service.check(r.text)) { unused += p.code == "LocalUnused" ? p.message + "\n" : std::string(); } - ensure_equals("unused", unused, std::string("Variable 'found' is never used; prefix with '_' to silence\n")); - checksClean(r); + CHECK(unused == std::string("Variable 'found' is never used; prefix with '_' to silence\n")); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<84>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: text where ll takes a key passed as it is with :: any and noted once", "[ALLSLToSLua]") { - set_test_name("text given where ll or llcompat takes a key is given as it is, as LSL's function took it, `:: any` for the type " - "checker, said once; a UUID written out still a uuid; the script's own function's key still uuid()"); const std::string lsl = "string gJson;\n" "integer mine(key who) { return who == llGetOwner(); }\n" "default { touch_start(integer n) {\n" @@ -2916,39 +3623,85 @@ namespace tut " llOwnerSay(llKey2Name(\"a2e76fcd-9360-4f6d-a924-938f923df11d\"));\n" " if (mine(llJsonGetValue(gJson, [\"owner\"]))) llOwnerSay(\"mine\");\n" "} }\n"; - const ALLSLToSLua::Result r = convert(lsl); - ensure("Tapple's, as it is: " + r.text, has(r, "ll.Key2Name(ll.JsonGetValue(gJson, { \"selected\" }) :: any)")); - ensure("bracketed where it must be: " + r.text, has(r, "ll.Key2Name((\"a2e76fcd-9360-4f6d-a924-\" .. \"938f923df11d\") :: any)")); - ensure("a UUID written out, a uuid: " + r.text, has(r, "ll.Key2Name(uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\"))")); - ensure("the script's own function's key, uuid(): " + r.text, has(r, "mine(uuid(ll.JsonGetValue(gJson, { \"owner\" })))") && - noted(r, "SluaUuidText")); - ensure("said once: " + r.text, count(r, "-- LSL: SLua's ll takes a key as text") == 1); - checksClean(r); + const ALLSLToSLua::Result r = require_converted(lsl); + CAPTURE(r.text); + UNSCOPED_INFO("Tapple's, as it is"); + CHECK(has(r, "ll.Key2Name(ll.JsonGetValue(gJson, { \"selected\" }) :: any)")); + UNSCOPED_INFO("bracketed where it must be"); + CHECK(has(r, "ll.Key2Name((\"a2e76fcd-9360-4f6d-a924-\" .. \"938f923df11d\") :: any)")); + UNSCOPED_INFO("a UUID written out, a uuid"); + CHECK(has(r, "ll.Key2Name(uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\"))")); + { + INFO("the script's own function's key, uuid()"); + CHECK(has(r, "mine(uuid(ll.JsonGetValue(gJson, { \"owner\" })))")); + CHECK(noted(r, "SluaUuidText")); + } + UNSCOPED_INFO("said once"); + CHECK(count(r, "-- LSL: SLua's ll takes a key as text") == 1); + require_checks_clean(r); if (!newSolver) { // ll's calls alone, strict. - const ALLSLToSLua::Result calls = convert("default { touch_start(integer n) {\n" - " llOwnerSay(llKey2Name(llJsonGetValue(\"{}\", [\"selected\"])));\n" - " llOwnerSay((string)llGetOwnerKey(\"a\" + (string)n));\n" - "} }\n"); + const ALLSLToSLua::Result calls = require_converted("default { touch_start(integer n) {\n" + " llOwnerSay(llKey2Name(llJsonGetValue(\"{}\", [\"selected\"])));\n" + " llOwnerSay((string)llGetOwnerKey(\"a\" + (string)n));\n" + "} }\n"); std::string said; for (const ALScriptProblem& p : service.check("--!strict\n" + calls.text)) { said += p.severity == ALScriptProblem::Severity::Error ? p.message + "\n" : std::string(); } - ensure("strict:\n" + said + "---\n" + calls.text, said.empty() && calls.text.find(":: any") != std::string::npos); + CAPTURE(said, calls.text); + CHECK(said.empty()); + CHECK(calls.text.find(":: any") != std::string::npos); } const ALLSLToSLua::Result close = ALLSLToSLua::convert(lsl, ALLSLToSLua::Options::closeToLSL()); - ensure("llcompat's the same: " + close.text, close.converted && close.text.find(":: any)") != std::string::npos); - checksClean(close); + CAPTURE(close.text); + { + INFO("llcompat's the same"); + CHECK(close.converted); + CHECK(close.text.find(":: any)") != std::string::npos); + } + require_checks_clean(close); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: deprecated calls noted with what to use; XorBase64Strings its own note", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " llMakeFire(10, 1.0, 1.0, 1.0, PI, \"\", ZERO_VECTOR);\n" + " llSoundPreload(\"boom\");\n" + " llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 1.0]);\n" + " llOwnerSay(llXorBase64Strings(\"YQ==\", \"Yg==\"));\n" + " llTakeCamera(llGetOwner());\n" + "} }\n"); + CAPTURE(r.text); + const auto named = [&r](const std::string& bare, const std::string& use) { + return std::any_of(r.notes.begin(), r.notes.end(), [&](const ALScriptProblem& p) { + return p.key == "SluaDeprecatedFor" && p.args.size() == 2 && p.args[0] == bare && p.args[1] == use; + }); + }; + { + INFO("each, for what it names"); + CHECK(named("MakeFire", "ll.ParticleSystem")); + CHECK(named("SoundPreload", "ll.PreloadSound")); + CHECK(named("SetPrimitiveParams", "ll.SetLinkPrimitiveParamsFast")); + CHECK(named("TakeCamera", "ll.SetCameraParams")); + } + { + INFO("XorBase64Strings' own"); + CHECK(noted(r, "SluaXorBase64Wrong")); + CHECK_FALSE(named("XorBase64Strings", "ll.XorBase64")); + } + UNSCOPED_INFO("said so"); + CHECK(has(r, "-- LSL: SLua deprecates ll.MakeFire, for ll.ParticleSystem.\n")); + UNSCOPED_INFO("none said bare"); + CHECK_FALSE(noted(r, "SluaDeprecated")); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<86>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a find's place against a literal compared in SLua's count from 1", "[ALLSLToSLua]") { - set_test_name("a find's place compared with a whole number written out is compared in SLua's count, from 1 or nil, on either side: " - "x == 1 where LSL said == 0; a place below -1 left as written, and one read as a number kept LSL's"); const std::string lsl = "string P = \"cfg\";\n" "list L;\n" "check(string item) {\n" @@ -2964,17 +3717,27 @@ namespace tut " llOwnerSay((string)at);\n" "}\n" "default { touch_start(integer n) { check(\"x\"); } }\n"; - const ALLSLToSLua::Result r = convert(lsl); - ensure("Tapple's: " + r.text, has(r, "if P ~= \"\" and ll.SubStringIndex(item, P) == 1 then")); - ensure("the number on the left: " + r.text, has(r, "if ll.SubStringIndex(item, P) == 1 then print(\"b\")")); - ensure("~=: " + r.text, has(r, "if ll.SubStringIndex(item, P) ~= 3 then")); - ensure("an order, nil read as LSL's -1: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) > 1 then")); - ensure("an order the other way round: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) > 4 then")); - ensure("below -1, as written: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) - 1 == -2 then")); - ensure("under a not: " + r.text, has(r, "if ll.SubStringIndex(item, P) ~= 1 then print(\"g\")")); - ensure("table.find's: " + r.text, has(r, "if table.find(L, item) == 1 then")); - ensure("a number, LSL's: " + r.text, has(r, "local at = (ll.SubStringIndex(item, P) or 0) - 1")); - checksClean(r); + const ALLSLToSLua::Result r = require_converted(lsl); + CAPTURE(r.text); + UNSCOPED_INFO("Tapple's"); + CHECK(has(r, "if P ~= \"\" and ll.SubStringIndex(item, P) == 1 then")); + UNSCOPED_INFO("the number on the left"); + CHECK(has(r, "if ll.SubStringIndex(item, P) == 1 then print(\"b\")")); + UNSCOPED_INFO("~="); + CHECK(has(r, "if ll.SubStringIndex(item, P) ~= 3 then")); + UNSCOPED_INFO("an order, nil read as LSL's -1"); + CHECK(has(r, "if (ll.SubStringIndex(item, P) or 0) > 1 then")); + UNSCOPED_INFO("an order the other way round"); + CHECK(has(r, "if (ll.SubStringIndex(item, P) or 0) > 4 then")); + UNSCOPED_INFO("below -1, as written"); + CHECK(has(r, "if (ll.SubStringIndex(item, P) or 0) - 1 == -2 then")); + UNSCOPED_INFO("under a not"); + CHECK(has(r, "if ll.SubStringIndex(item, P) ~= 1 then print(\"g\")")); + UNSCOPED_INFO("table.find's"); + CHECK(has(r, "if table.find(L, item) == 1 then")); + UNSCOPED_INFO("a number, LSL's"); + CHECK(has(r, "local at = (ll.SubStringIndex(item, P) or 0) - 1")); + require_checks_clean(r); // table.find's alone, without SLua's ll: whether it found, against nil. ALLSLToSLua::Options idioms = ALLSLToSLua::Options::closeToLSL(); @@ -2986,59 +3749,71 @@ namespace tut " if (llListFindList(L, [n]) == 2) llOwnerSay(\"third\");\n" "} }\n", idioms); - ensure("found, against nil: " + found.text, found.converted && has(found, "if table.find(L, n) == nil then") && - has(found, "if table.find(L, n) ~= nil then") && has(found, "if table.find(L, n) == 3 then")); - checksClean(found); + CAPTURE(found.text); + { + INFO("found, against nil"); + CHECK(found.converted); + CHECK(has(found, "if table.find(L, n) == nil then")); + CHECK(has(found, "if table.find(L, n) ~= nil then")); + CHECK(has(found, "if table.find(L, n) == 3 then")); + } + require_checks_clean(found); const ALLSLToSLua::Result close = ALLSLToSLua::convert(lsl, ALLSLToSLua::Options::closeToLSL()); - ensure("llcompat's, LSL's count: " + close.text, close.converted && has(close, "llcompat.SubStringIndex(item, P) == 0")); - checksClean(close); - } - - template<> template<> - void allsltoslua_object::test<87>() - { - set_test_name("a blank line between two statements or globals is kept, one for a run of them; none at a block's start or end, " - "nor a second where a comment over the statement keeps its own"); - const ALLSLToSLua::Result r = convert("integer a = 1;\n" - "integer b = 2;\n" - "\n" - "\n" - "integer c = 3;\n" - "integer preCheck(key avatar) {\n" - "\n" - " // only allow my owner to request contents\n" - " if (avatar != llGetOwner()) return FALSE;\n" - "\n" - " return TRUE;\n" - "}\n" - "default {\n" - " touch_start(integer n) {\n" - " if (preCheck(llDetectedKey(0))) {\n" - " llOwnerSay((string)a);\n" - "\n" - " llOwnerSay((string)b);\n" - "\n" - " }\n" - "\n" - " // said apart\n" - " llOwnerSay((string)c);\n" - " }\n" - "}\n"); - ensure("the globals: " + r.text, has(r, "local a = 1\nlocal b = 2\n\nlocal c = 3\n")); - ensure("Tapple's: " + r.text, has(r, " -- only allow my owner to request contents\n if avatar ~= ll.GetOwner() then return false end\n\n return true\nend")); - ensure("none at a block's start: " + r.text, has(r, "(avatar)\n -- only allow")); - ensure("in a block, none at its end: " + r.text, has(r, " print(tostring(a))\n\n print(tostring(b))\n end\n")); - ensure("one over a comment kept apart: " + r.text, has(r, " end\n\n -- said apart\n print(tostring(c))\n")); - ensure("never two: " + r.text, !has(r, "\n\n\n")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<88>() - { - set_test_name("tables and table types spaced inside their braces as asked, as the formatter spaces them: StyLua's by default, " - "Tapple's line; none where asked; an interpolated string's braces and {} never"); + CAPTURE(close.text); + { + INFO("llcompat's, LSL's count"); + CHECK(close.converted); + CHECK(has(close, "llcompat.SubStringIndex(item, P) == 0")); + } + require_checks_clean(close); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: blank lines between statements kept singly and not at a block's ends", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer a = 1;\n" + "integer b = 2;\n" + "\n" + "\n" + "integer c = 3;\n" + "integer preCheck(key avatar) {\n" + "\n" + " // only allow my owner to request contents\n" + " if (avatar != llGetOwner()) return FALSE;\n" + "\n" + " return TRUE;\n" + "}\n" + "default {\n" + " touch_start(integer n) {\n" + " if (preCheck(llDetectedKey(0))) {\n" + " llOwnerSay((string)a);\n" + "\n" + " llOwnerSay((string)b);\n" + "\n" + " }\n" + "\n" + " // said apart\n" + " llOwnerSay((string)c);\n" + " }\n" + "}\n"); + CAPTURE(r.text); + UNSCOPED_INFO("the globals"); + CHECK(has(r, "local a = 1\nlocal b = 2\n\nlocal c = 3\n")); + UNSCOPED_INFO("Tapple's"); + CHECK(has(r, " -- only allow my owner to request contents\n if avatar ~= ll.GetOwner() then return false end\n\n return true\nend")); + UNSCOPED_INFO("none at a block's start"); + CHECK(has(r, "(avatar)\n -- only allow")); + UNSCOPED_INFO("in a block, none at its end"); + CHECK(has(r, " print(tostring(a))\n\n print(tostring(b))\n end\n")); + UNSCOPED_INFO("one over a comment kept apart"); + CHECK(has(r, " end\n\n -- said apart\n print(tostring(c))\n")); + UNSCOPED_INFO("never two"); + CHECK_FALSE(has(r, "\n\n\n")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: table braces spaced as asked as the formatter spaces them", "[ALLSLToSLua]") + { const std::string lsl = "list gTwo = [1, 2,\n 3];\n" "default {\n" " state_entry() { state two; }\n" @@ -3053,20 +3828,35 @@ namespace tut ALLSLToSLua::Options typed; typed.types = true; const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, typed); - ensure("Tapple's: " + r.text, r.converted && has(r, "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})")); - ensure("a rule table: " + r.text, has(r, "ll.SetPrimMediaParams(0, { auto_play = true, whitelist = { \"a.com\", \"b.com\" } })")); - ensure("the states' type: " + r.text, has(r, "local states: { [string]: { [string]: (...any) -> () } } = {}")); - ensure("over lines: " + r.text, has(r, "local gTwo: { any } = { 1, 2,\n 3 }\n")); - ensure("an interpolated string's: " + r.text, has(r, "`n={#params} {ll.DumpList2String({ vector(1, 2, 3) }, \"\")}`")); - checksClean(r); + CAPTURE(r.text); + { + INFO("Tapple's"); + CHECK(r.converted); + CHECK(has(r, "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})")); + } + UNSCOPED_INFO("a rule table"); + CHECK(has(r, "ll.SetPrimMediaParams(0, { auto_play = true, whitelist = { \"a.com\", \"b.com\" } })")); + UNSCOPED_INFO("the states' type"); + CHECK(has(r, "local states: { [string]: { [string]: (...any) -> () } } = {}")); + UNSCOPED_INFO("over lines"); + CHECK(has(r, "local gTwo: { any } = { 1, 2,\n 3 }\n")); + UNSCOPED_INFO("an interpolated string's"); + CHECK(has(r, "`n={#params} {ll.DumpList2String({ vector(1, 2, 3) }, \"\")}`")); + require_checks_clean(r); typed.braceSpaces = false; const ALLSLToSLua::Result none = ALLSLToSLua::convert(lsl, typed); - ensure("none: " + none.text, none.converted && has(none, "local params: {any} = ll.ParseStringKeepNulls(msg, {\"\\n\"}, {})") && - has(none, "ll.SetPrimMediaParams(0, {auto_play = true, whitelist = {\"a.com\", \"b.com\"}})") && - has(none, "local states: {[string]: {[string]: (...any) -> ()}} = {}") && - has(none, "local gTwo: {any} = {1, 2,\n 3}\n") && !has(none, "{ ")); - checksClean(none); + CAPTURE(none.text); + { + INFO("none"); + CHECK(none.converted); + CHECK(has(none, "local params: {any} = ll.ParseStringKeepNulls(msg, {\"\\n\"}, {})")); + CHECK(has(none, "ll.SetPrimMediaParams(0, {auto_play = true, whitelist = {\"a.com\", \"b.com\"}})")); + CHECK(has(none, "local states: {[string]: {[string]: (...any) -> ()}} = {}")); + CHECK(has(none, "local gTwo: {any} = {1, 2,\n 3}\n")); + CHECK_FALSE(has(none, "{ ")); + } + require_checks_clean(none); // As the formatter would leave them, either way. for (bool spaces : { true, false }) @@ -3075,15 +3865,13 @@ namespace tut formatter.lua = true; formatter.braceSpaces = spaces; const ALLSLToSLua::Result same = spaces ? r : none; - ensure_equals(spaces ? "formatted, spaced" : "formatted, none", ALScriptFormatter::format(same.text, formatter), same.text); + CAPTURE(spaces); + REQUIRE(ALScriptFormatter::format(same.text, formatter) == same.text); } } - template<> template<> - void allsltoslua_object::test<89>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: converted SLua is as format on save leaves it with either brace spacing", "[ALLSLToSLua]") { - set_test_name("converted SLua is as format on save leaves it: a comment after an unbraced body trails it, after the line that " - "opens it where the body is empty; what the LSL lined up, lined up; one line break at the end; either brace spacing"); const std::string lsl = "integer CHANNEL = 1; // channel\n" "integer OWNER_ONLY = 0; // owner\n" "float TIMEOUT = 2.5; // seconds\n" @@ -3114,34 +3902,218 @@ namespace tut "}\n"; for (bool spaces : { true, false }) { + CAPTURE(spaces); ALLSLToSLua::Options options; options.braceSpaces = spaces; const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, options); - ensure("converted", r.converted); - ensure("each after its own: " + r.text, has(r, " print(\"a\") -- after a\n elseif") && has(r, " print(\"b\") -- after b\n else") && - has(r, " print(\"c\") -- after c\n end")); - ensure("an empty body's, after the line that opens it: " + r.text, has(r, " == 2 then -- an empty body\n else\n print(\"f\") -- after f\n")); - ensure("a loop's body's: " + r.text, has(r, " n -= 1 -- after the step\n end")); - ensure("between a block and its else, over what the else runs: " + r.text, has(r, " else\n -- over the else\n print(\"e\")\n")); - ensure("lined up: " + r.text, has(r, "local CHANNEL = 1 -- channel\nlocal OWNER_ONLY = 0 -- owner\nlocal TIMEOUT = 2.5 -- seconds\n")); - ensure("one line break at the end: " + r.text, r.text.size() > 2 && r.text.compare(r.text.size() - 2, 2, "\n\n") != 0 && r.text.back() == '\n'); + CAPTURE(r.text); + REQUIRE(r.converted); + { + INFO("each after its own"); + REQUIRE(has(r, " print(\"a\") -- after a\n elseif")); + REQUIRE(has(r, " print(\"b\") -- after b\n else")); + REQUIRE(has(r, " print(\"c\") -- after c\n end")); + } + UNSCOPED_INFO("an empty body's, after the line that opens it"); + REQUIRE(has(r, " == 2 then -- an empty body\n else\n print(\"f\") -- after f\n")); + UNSCOPED_INFO("a loop's body's"); + REQUIRE(has(r, " n -= 1 -- after the step\n end")); + UNSCOPED_INFO("between a block and its else, over what the else runs"); + REQUIRE(has(r, " else\n -- over the else\n print(\"e\")\n")); + UNSCOPED_INFO("lined up"); + REQUIRE(has(r, "local CHANNEL = 1 -- channel\nlocal OWNER_ONLY = 0 -- owner\nlocal TIMEOUT = 2.5 -- seconds\n")); + { + INFO("one line break at the end"); + REQUIRE(r.text.size() > 2); + REQUIRE(r.text.compare(r.text.size() - 2, 2, "\n\n") != 0); + REQUIRE(r.text.back() == '\n'); + } ALScriptFormatter::Options formatter; formatter.lua = true; formatter.braceSpaces = spaces; - ensure_equals(spaces ? "formatted, spaced" : "formatted, none", ALScriptFormatter::format(r.text, formatter), r.text); - checksClean(r); + REQUIRE(ALScriptFormatter::format(r.text, formatter) == r.text); + require_checks_clean(r); + } + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: the //* toggle with a word after its star as two SLua line comments", "[ALLSLToSLua]") + { + ALLSLToSLua::Options options; + options.types = true; + const ALLSLToSLua::Result r = ALLSLToSLua::convert("//* OVERRIDE\n" + "integer allowAttachedAO;\n" + "//*/\n" + "default { state_entry() { if (allowAttachedAO) llOwnerSay(\"on\"); } }\n", + options); + CAPTURE(r.text); + CHECK(r.converted); + UNSCOPED_INFO("the toggle"); + CHECK(has(r, "\n---[[ OVERRIDE\nlocal allowAttachedAO: boolean = false\n--]]\n")); + UNSCOPED_INFO("the opener a line comment"); + CHECK(require_comment_at(r.text, "---[[") == std::string("---[[ OVERRIDE")); + UNSCOPED_INFO("the closer one"); + CHECK(require_comment_at(r.text, "--]]") == std::string("--]]")); + UNSCOPED_INFO("turned off"); + CHECK(require_toggled(r.text, "---[[") == std::string("--[[ OVERRIDE\nlocal allowAttachedAO: boolean = false\n--]]")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: half a toggle: an opener only before a closer and openers share it", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result lone = require_converted("integer a;\n" + "//* nothing closes this\n" + "integer b;\n" + "default { state_entry() { llOwnerSay((string)(a + b)); } }\n"); + CAPTURE(lone.text); + { + INFO("a lone opener a comment with a star"); + CHECK(has(lone, "\n--* nothing closes this\nlocal b = 0\n")); + CHECK_FALSE(has(lone, "---[")); + } + require_checks_clean(lone); + + const ALLSLToSLua::Result r = require_converted("//* not one */\n" + "//**************\n" + "integer c = 1;\n" + "//* one\n" + "integer d = 2;\n" + "//* two\n" + "integer e = 3;\n" + "//*/ // both\n" + "//*/\n" + "default { state_entry() { llOwnerSay((string)(c + d + e)); } }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("closed on its line"); + CHECK(has(r, "\n--* not one */\n")); + UNSCOPED_INFO("a rule of stars"); + CHECK(has(r, "\n--**************\n")); + UNSCOPED_INFO("two openers and a closer, then one alone"); + CHECK(has(r, "\n---[[ one\nlocal d = 2\n---[[ two\nlocal e = 3\n--]] -- both\n--]]\n")); + UNSCOPED_INFO("the first off"); + CHECK(require_toggled(r.text, "---[[ one") == std::string("--[[ one\nlocal d = 2\n---[[ two\nlocal e = 3\n--]]")); + UNSCOPED_INFO("the second off"); + CHECK(require_toggled(r.text, "---[[ two") == std::string("--[[ two\nlocal e = 3\n--]]")); + std::string off = r.text; + off.erase(off.find("---[[ one"), 1); + UNSCOPED_INFO("the closer's own comment after it, off"); + CHECK(require_comment_at(off, "-- both") == std::string("-- both")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: the toggle the other way round as a block comment SLua can turn on", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("/* OVERRIDE\n" + "integer allowAttachedAO;\n" + "//*/\n" + "//* A\n" + "integer x = 1;\n" + "/* B\n" + "integer y = 2;\n" + "//*/\n" + "default { state_entry() { llOwnerSay((string)x); } }\n"); + UNSCOPED_INFO("one block comment"); + CHECK(require_comment_at(r.text, "--[[ OVERRIDE") == std::string("--[[ OVERRIDE\ninteger allowAttachedAO;\n--]]")); + // What it holds stays LSL: nothing says it is code. + std::string on = r.text; + on.insert(on.find("--[[ OVERRIDE"), "-"); + UNSCOPED_INFO("turned on, the opener a line comment"); + CHECK(require_comment_at(on, "---[[ OVERRIDE") == std::string("---[[ OVERRIDE")); + UNSCOPED_INFO("and the closer"); + CHECK(require_comment_at(on, "--]]") == std::string("--]]")); + UNSCOPED_INFO("the opener over code off, to the closer of the one after it"); + CHECK(require_toggled(r.text, "---[[ A") == + std::string("--[[ A\nlocal x = 1\n\n--[[ B\ninteger y = 2;\n--]]")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a toggle's level is the fewest =s that nothing inside closes", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("//* OVERRIDE\n" + "string s = \"]]\";\n" + "//*/\n" + "default { state_entry() { llOwnerSay(s); } }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("past a string's ]]"); + CHECK(has(r, "\n---[=[ OVERRIDE\nlocal s = \"]]\"\n--]=]\n")); + CHECK(require_toggled(r.text, "---[=[") == std::string("--[=[ OVERRIDE\nlocal s = \"]]\"\n--]=]")); + require_checks_clean(r); + + const ALLSLToSLua::Result shared = require_converted("//* A\n" + "integer x = 1; /* one */\n" + "/* B\n" + "string s = \"]]\";\n" + "//*/\n" + "default { state_entry() { llOwnerSay((string)x); } }\n"); + CAPTURE(shared.text); + { + INFO("one level for the pair"); + CHECK(has(shared, "\n---[=[ A\nlocal x = 1 --[[ one ]]\n")); + CHECK(has(shared, "\n--[=[ B\nstring s = \"]]\";\n--]=]\n")); + } + UNSCOPED_INFO("the opener off"); + CHECK(require_toggled(shared.text, "---[=[ A") == + std::string("--[=[ A\nlocal x = 1 --[[ one ]]\n\n--[=[ B\nstring s = \"]]\";\n--]=]")); + require_checks_clean(shared); + + std::string every = "// "; + for (char c = 1; c < ' '; ++c) + { + if (c != '\t' && c != '\n' && c != '\r') + { + every += c; + } + } + const ALLSLToSLua::Result unmarked = require_converted(every + "\ninteger y;\n//* A\ninteger x = 1;\n//*/\n" + "default { state_entry() { llOwnerSay((string)(x + y)); } }\n"); + CAPTURE(unmarked.text); + UNSCOPED_INFO("paired still"); + CHECK(has(unmarked, "\n---[[ A\nlocal x = 1\n--]]\n")); + UNSCOPED_INFO("no byte of its own left in"); + CHECK(count(unmarked, std::string(1, '\x01')) == size_t(1)); + require_checks_clean(unmarked); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a toggle after code or in a handler or at the top above the helpers", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() {\n" + " integer a = 1; //* A\n" + " llOwnerSay((string)a); //*/\n" + " //* DEBUG\n" + " llOwnerSay(\"debug\");\n" + " //*/\n" + " llOwnerSay(\"done\");\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("after code"); + CHECK(has(r, "\nlocal a = 1 ---[[ A\nprint(tostring(a)) --]]\n")); + UNSCOPED_INFO("off from it"); + CHECK(require_toggled(r.text, "---[[ A") == std::string("--[[ A\nprint(tostring(a)) --]]")); + UNSCOPED_INFO("in a handler"); + CHECK(has(r, "\n---[[ DEBUG\nprint(\"debug\")\n--]]\nprint(\"done\")\n")); + CHECK(require_toggled(r.text, "---[[ DEBUG") == std::string("--[[ DEBUG\nprint(\"debug\")\n--]]")); + require_checks_clean(r); + + const ALLSLToSLua::Result top = require_converted("//* DEBUG\n" + "\n" + "vector v = <1, 2, 3>;\n" + "//*/\n" + "default { state_entry() { llOwnerSay((string)llVecNorm(v)); } }\n"); + CAPTURE(top.text); + { + INFO("the helper over it"); + CHECK(has(top, "local function vecNorm(")); + CHECK(top.text.find("local function vecNorm(") < top.text.find("---[[ DEBUG")); } + CHECK(require_toggled(top.text, "---[[ DEBUG") == std::string("--[[ DEBUG\nlocal v = vector(1, 2, 3)\n--]]")); + require_checks_clean(top); } - template<> template<> - void allsltoslua_object::test<95>() + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a comment after an unbraced body trails the statement ending its line", "[ALLSLToSLua]") { - set_test_name("a comment after an unbraced body, on the line it ends, trails the statement that ends that line: a then's, an " - "elseif's, an else's, a loop's; an empty body's the line that opens it; a one-line if's, the whole if"); - const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" - " integer a; integer b;\n" - " if (n == 1)\n" - " llOwnerSay(\"a\"); // after a\n" + const ALLSLToSLua::Result r = require_converted("default { touch_start(integer n) {\n" + " integer a; integer b;\n" + " if (n == 1)\n" + " llOwnerSay(\"a\"); // after a\n" " else if (n == 2)\n" " llOwnerSay(\"b\"); // after b\n" " else\n" @@ -3156,176 +4128,221 @@ namespace tut " while (a) ; // an empty loop\n" " if (n == 5) llOwnerSay(\"h\"); // after h\n" "} }\n"); - ensure("a then's, an elseif's, an else's: " + r.text, - has(r, " if n == 1 then\n print(\"a\") -- after a\n elseif n == 2 then\n print(\"b\") -- after b\n" + CAPTURE(r.text); + UNSCOPED_INFO("a then's, an elseif's, an else's"); + CHECK(has(r, " if n == 1 then\n print(\"a\") -- after a\n elseif n == 2 then\n print(\"b\") -- after b\n" " else\n print(\"c\") -- after c\n end\n")); - ensure("an empty body's, the line that opens it: " + r.text, - has(r, " == 2 then -- empty\n elseif b == 2 then -- empty too\n else\n a = true -- after a = 2\n end\n")); - ensure("an if on one line inside another: " + r.text, has(r, " if b ~= 0 then print(\"x\") end -- inner, one line\n")); - ensure("a loop's body's: " + r.text, has(r, " n -= 1 -- after the step\n end\n") && has(r, " while a do -- an empty loop\n end\n")); - ensure("a one-line if, as before: " + r.text, has(r, " if n == 5 then print(\"h\") end -- after h\n")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<96>() - { - set_test_name("a float cut to an integer is math.modf's first value: bare where only its first is read anyway -- an operand, all " - "of a value assigned -- and kept to it by brackets where its values would spread"); - const ALLSLToSLua::Result r = convert("integer menuChannel;\n" - "vector gV;\n" - "integer cut(float f) { return (integer)f; }\n" - "default { touch_start(integer n) {\n" - " float f = llFrand(10);\n" - " menuChannel = (integer)llFrand(1000) - 10000;\n" - " integer x = (integer)f;\n" - " x = (integer)f;\n" - " x += (integer)f;\n" - " x = -(integer)f;\n" - " if ((integer)f == 3) llOwnerSay((string)((integer)f));\n" - " llOwnerSay(llList2CSV([(integer)f]));\n" - " gV.z = (integer)f;\n" - " llOwnerSay((string)(cut(f) + x + menuChannel) + (string)gV);\n" - "} }\n"); - ensure("Tapple's: " + r.text, has(r, "menuChannel = math.modf(ll.Frand(1000)) - 10000\n")); - ensure("a local: " + r.text, has(r, "local x = math.modf(f)\n")); - ensure("assigned: " + r.text, has(r, " x = math.modf(f)\n")); - ensure("added: " + r.text, has(r, " x += math.modf(f)\n")); - ensure("its negative: " + r.text, has(r, " x = -math.modf(f)\n")); - ensure("compared: " + r.text, has(r, "if math.modf(f) == 3 then")); - ensure("an argument: " + r.text, has(r, "print(tostring((math.modf(f))))")); - ensure("a list's item: " + r.text, has(r, "{ (math.modf(f)) }")); - ensure("returned: " + r.text, has(r, "return (math.modf(f))")); - ensure("a vector's part, an argument: " + r.text, has(r, "gV = vector(gV.x, gV.y, (math.modf(f)))")); - checksClean(r); - } - template<> template<> - void allsltoslua_object::test<97>() - { - set_test_name("a local whose Luau name would hide what its reach names, which LSL kept apart, is named apart: Tapple's parameter " - "f2 beside the function f2, a local beside a function the mark gave its name, a parameter named as its own " - "function; a global the mark gave another's name; nothing where nothing is hidden"); - const ALLSLToSLua::Result r = convert("f1(string f2) {\n" - " llOwnerSay(f2);\n" - " f2();\n" - "}\n" - "\n" - "f2() {\n" - " llOwnerSay(\"world\");\n" - "}\n" - "\n" - "default { state_entry() {\n" - " f1(\"hello\");\n" - "} }\n"); - ensure("Tapple's, the parameter named apart: " + r.text, has(r, "local function f1(f2_)\n print(f2_)\n f2()\nend\n")); - ensure("the function as it was: " + r.text, has(r, "local f2\n") && has(r, "function f2()\n")); - checksClean(r); - - const ALLSLToSLua::Result detected = convert("integer detected() {\n" - " return 5;\n" - "}\n" - "\n" - "default\n" - "{\n" - " touch_start(integer total_number)\n" - " {\n" - " string detected = \"fritz\";\n" - "\n" - " llOwnerSay(llDetectedName(0));\n" - " llOwnerSay(detected);\n" - " llOwnerSay((string)(detected() * detected()));\n" - " }\n" - "}\n"); - ensure("Tapple's detected, the function marked: " + detected.text, has(detected, "local function detected_()")); - ensure("the local apart from it: " + detected.text, has(detected, "local detected_2 = \"fritz\"") && has(detected, "print(detected_2)")); - ensure("the function called: " + detected.text, has(detected, "print(tostring(detected_() * detected_()))")); - checksClean(detected); - - const ALLSLToSLua::Result own = convert("integer fact(integer fact) {\n" - " if (fact <= 1) return 1;\n" - " return fact * fact(fact - 1);\n" - "}\n" - "string table = \"t\";\n" - "string table_ = \"u\";\n" - "f3(string f2) { llOwnerSay(f2); }\n" - "f2() { llOwnerSay(\"f2\"); }\n" - "f4() { f2(); string f2 = \"x\"; llOwnerSay(f2); }\n" - "default { touch_start(integer n) {\n" - " integer was = 1;\n" - " integer k = was++ + was;\n" - " llOwnerSay(table + table_ + (string)(fact(n) + k));\n" - " f3(\"a\"); f4();\n" - "} }\n"); - ensure("a parameter named as its function: " + own.text, has(own, "local function fact(fact_)") && has(own, "return fact_ * fact(fact_ - 1)")); - ensure("a global the mark gave another's name: " + own.text, - has(own, "local table_2 = \"t\"\n") && has(own, "local table_ = \"u\"\n") && has(own, "print(`{table_2}{table_}{fact(n) + k}`)")); - ensure("nothing hidden, nothing renamed: " + own.text, has(own, "local function f3(f2)\n print(f2)\n")); - ensure("a call before the local, not in its reach: " + own.text, has(own, " f2()\n local f2 = \"x\"\n print(f2)\n")); - ensure("what a step was, under a name the script does not have: " + own.text, - has(own, "(function() local was2 = was; was += 1 return was2 end)() + was")); - checksClean(own); - } - - template<> template<> - void allsltoslua_object::test<98>() - { - set_test_name("a state's handlers in its table kept apart by a blank line where the LSL had them so, and together where not"); - const ALLSLToSLua::Result r = convert("default { state_entry() { state collect; } }\n" - "state collect {\n" - " changed(integer change) {\n" - " if (change & CHANGED_INVENTORY) {\n" - " llOwnerSay(\"c\");\n" - " }\n" - " }\n" - "\n" - " on_rez(integer param) {\n" - " llOwnerSay(\"r\");\n" - " }\n" - " touch_start(integer n) { llOwnerSay(\"t\"); }\n" - "\n" - " // listening\n" - " listen(integer c, string name, key id, string msg) { llOwnerSay(msg); }\n" - "}\n"); - ensure("Tapple's, apart: " + r.text, has(r, " end\n end,\n\n on_rez = function(param)\n")); - ensure("together: " + r.text, has(r, " print(\"r\")\n end,\n touch_start = function(detected)\n")); - ensure("one blank line over a comment over it: " + r.text, has(r, " end,\n\n -- listening\n listen = function(")); - ensure("none at the table's top: " + r.text, has(r, "states.collect = {\n changed = function(change)\n")); - checksClean(r); - } - - template<> template<> - void allsltoslua_object::test<99>() - { - set_test_name("LSL's right side first noted only where the order could show, naming what: not two sides that only read the " - "world, through a function of the script's too (Tapple's); a call that changes the world beside one that reads it; " - "a call that sets what the other side's function reads; not one beside a function that does not read it"); - const ALLSLToSLua::Result r = convert("key gSender;\n" - "string gCache;\n" - "string myInfo() { return (string)llGetKey(); }\n" - "string say() { llOwnerSay(\"x\"); return \"y\"; }\n" - "string cached() { return gCache; }\n" - "string fill() { gCache = \"z\"; return gCache; }\n" - "string other() { return \"o\"; }\n" - "default { touch_start(integer n) {\n" - " llOwnerSay(\"activeRequestor: \" + llKey2Name(gSender) + \"; llGetKey: \" + llKey2Name(myInfo()));\n" - " llOwnerSay(llKey2Name(gSender) + say());\n" - " llOwnerSay(cached() + fill());\n" - " llOwnerSay(other() + fill());\n" - "} }\n"); - ensure("Tapple's, two sides that only read: " + r.text, !noted(r, "SluaRightFirst") && !noted(r, "SluaRightFirstWorld", "myInfo()")); - ensure("a call that changes the world, named with the other: " + r.text, noted(r, "SluaRightFirstWorld", "say()") && - has(r, "-- LSL: LSL ran the right side of this before the left, and Luau runs the left first: say() may change what " + UNSCOPED_INFO("an empty body's, the line that opens it"); + CHECK(has(r, " == 2 then -- empty\n elseif b == 2 then -- empty too\n else\n a = true -- after a = 2\n end\n")); + UNSCOPED_INFO("an if on one line inside another"); + CHECK(has(r, " if b ~= 0 then print(\"x\") end -- inner, one line\n")); + { + INFO("a loop's body's"); + CHECK(has(r, " n -= 1 -- after the step\n end\n")); + CHECK(has(r, " while a do -- an empty loop\n end\n")); + } + UNSCOPED_INFO("a one-line if, as before"); + CHECK(has(r, " if n == 5 then print(\"h\") end -- after h\n")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a float cut to an integer is math.modf bracketed where its values spread", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("integer menuChannel;\n" + "vector gV;\n" + "integer cut(float f) { return (integer)f; }\n" + "default { touch_start(integer n) {\n" + " float f = llFrand(10);\n" + " menuChannel = (integer)llFrand(1000) - 10000;\n" + " integer x = (integer)f;\n" + " x = (integer)f;\n" + " x += (integer)f;\n" + " x = -(integer)f;\n" + " if ((integer)f == 3) llOwnerSay((string)((integer)f));\n" + " llOwnerSay(llList2CSV([(integer)f]));\n" + " gV.z = (integer)f;\n" + " llOwnerSay((string)(cut(f) + x + menuChannel) + (string)gV);\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("Tapple's"); + CHECK(has(r, "menuChannel = math.modf(ll.Frand(1000)) - 10000\n")); + UNSCOPED_INFO("a local"); + CHECK(has(r, "local x = math.modf(f)\n")); + UNSCOPED_INFO("assigned"); + CHECK(has(r, " x = math.modf(f)\n")); + UNSCOPED_INFO("added"); + CHECK(has(r, " x += math.modf(f)\n")); + UNSCOPED_INFO("its negative"); + CHECK(has(r, " x = -math.modf(f)\n")); + UNSCOPED_INFO("compared"); + CHECK(has(r, "if math.modf(f) == 3 then")); + UNSCOPED_INFO("an argument"); + CHECK(has(r, "print(tostring((math.modf(f))))")); + UNSCOPED_INFO("a list's item"); + CHECK(has(r, "{ (math.modf(f)) }")); + UNSCOPED_INFO("returned"); + CHECK(has(r, "return (math.modf(f))")); + UNSCOPED_INFO("a vector's part, an argument"); + CHECK(has(r, "gV = vector(gV.x, gV.y, (math.modf(f)))")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a local whose Luau name would hide another is named apart", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("f1(string f2) {\n" + " llOwnerSay(f2);\n" + " f2();\n" + "}\n" + "\n" + "f2() {\n" + " llOwnerSay(\"world\");\n" + "}\n" + "\n" + "default { state_entry() {\n" + " f1(\"hello\");\n" + "} }\n"); + CAPTURE(r.text); + UNSCOPED_INFO("Tapple's, the parameter named apart"); + CHECK(has(r, "local function f1(f2_)\n print(f2_)\n f2()\nend\n")); + { + INFO("the function as it was"); + CHECK(has(r, "local f2\n")); + CHECK(has(r, "function f2()\n")); + } + require_checks_clean(r); + + const ALLSLToSLua::Result detected = require_converted("integer detected() {\n" + " return 5;\n" + "}\n" + "\n" + "default\n" + "{\n" + " touch_start(integer total_number)\n" + " {\n" + " string detected = \"fritz\";\n" + "\n" + " llOwnerSay(llDetectedName(0));\n" + " llOwnerSay(detected);\n" + " llOwnerSay((string)(detected() * detected()));\n" + " }\n" + "}\n"); + CAPTURE(detected.text); + UNSCOPED_INFO("Tapple's detected, the function marked"); + CHECK(has(detected, "local function detected_()")); + { + INFO("the local apart from it"); + CHECK(has(detected, "local detected_2 = \"fritz\"")); + CHECK(has(detected, "print(detected_2)")); + } + UNSCOPED_INFO("the function called"); + CHECK(has(detected, "print(tostring(detected_() * detected_()))")); + require_checks_clean(detected); + + const ALLSLToSLua::Result own = require_converted("integer fact(integer fact) {\n" + " if (fact <= 1) return 1;\n" + " return fact * fact(fact - 1);\n" + "}\n" + "string table = \"t\";\n" + "string table_ = \"u\";\n" + "f3(string f2) { llOwnerSay(f2); }\n" + "f2() { llOwnerSay(\"f2\"); }\n" + "f4() { f2(); string f2 = \"x\"; llOwnerSay(f2); }\n" + "default { touch_start(integer n) {\n" + " integer was = 1;\n" + " integer k = was++ + was;\n" + " llOwnerSay(table + table_ + (string)(fact(n) + k));\n" + " f3(\"a\"); f4();\n" + "} }\n"); + CAPTURE(own.text); + { + INFO("a parameter named as its function"); + CHECK(has(own, "local function fact(fact_)")); + CHECK(has(own, "return fact_ * fact(fact_ - 1)")); + } + { + INFO("a global the mark gave another's name"); + CHECK(has(own, "local table_2 = \"t\"\n")); + CHECK(has(own, "local table_ = \"u\"\n")); + CHECK(has(own, "print(`{table_2}{table_}{fact(n) + k}`)")); + } + UNSCOPED_INFO("nothing hidden, nothing renamed"); + CHECK(has(own, "local function f3(f2)\n print(f2)\n")); + UNSCOPED_INFO("a call before the local, not in its reach"); + CHECK(has(own, " f2()\n local f2 = \"x\"\n print(f2)\n")); + UNSCOPED_INFO("what a step was, under a name the script does not have"); + CHECK(has(own, "(function() local was2 = was; was += 1 return was2 end)() + was")); + require_checks_clean(own); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a state's handlers kept apart by blank lines only where the LSL had them", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("default { state_entry() { state collect; } }\n" + "state collect {\n" + " changed(integer change) {\n" + " if (change & CHANGED_INVENTORY) {\n" + " llOwnerSay(\"c\");\n" + " }\n" + " }\n" + "\n" + " on_rez(integer param) {\n" + " llOwnerSay(\"r\");\n" + " }\n" + " touch_start(integer n) { llOwnerSay(\"t\"); }\n" + "\n" + " // listening\n" + " listen(integer c, string name, key id, string msg) { llOwnerSay(msg); }\n" + "}\n"); + CAPTURE(r.text); + UNSCOPED_INFO("Tapple's, apart"); + CHECK(has(r, " end\n end,\n\n on_rez = function(param)\n")); + UNSCOPED_INFO("together"); + CHECK(has(r, " print(\"r\")\n end,\n touch_start = function(detected)\n")); + UNSCOPED_INFO("one blank line over a comment over it"); + CHECK(has(r, " end,\n\n -- listening\n listen = function(")); + UNSCOPED_INFO("none at the table's top"); + CHECK(has(r, "states.collect = {\n changed = function(change)\n")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: LSL's right-side-first order noted only where it could show", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("key gSender;\n" + "string gCache;\n" + "string myInfo() { return (string)llGetKey(); }\n" + "string say() { llOwnerSay(\"x\"); return \"y\"; }\n" + "string cached() { return gCache; }\n" + "string fill() { gCache = \"z\"; return gCache; }\n" + "string other() { return \"o\"; }\n" + "default { touch_start(integer n) {\n" + " llOwnerSay(\"activeRequestor: \" + llKey2Name(gSender) + \"; llGetKey: \" + llKey2Name(myInfo()));\n" + " llOwnerSay(llKey2Name(gSender) + say());\n" + " llOwnerSay(cached() + fill());\n" + " llOwnerSay(other() + fill());\n" + "} }\n"); + CAPTURE(r.text); + { + INFO("Tapple's, two sides that only read"); + CHECK_FALSE(noted(r, "SluaRightFirst")); + CHECK_FALSE(noted(r, "SluaRightFirstWorld", "myInfo()")); + } + { + INFO("a call that changes the world, named with the other"); + CHECK(noted(r, "SluaRightFirstWorld", "say()")); + CHECK(has(r, "-- LSL: LSL ran the right side of this before the left, and Luau runs the left first: say() may change what " "llKey2Name() does or finds outside the script.\n print(ll.Key2Name(gSender) .. say())")); - ensure("a call that sets what the other's reads: " + r.text, noted(r, "SluaRightFirstCall", "fill()") && - has(r, "fill() sets gCache, which the other side reads or sets too.\n print(cached() .. fill())")); - ensure("noted twice, no more: " + r.text, count(r, "LSL ran the right side") == 2); - checksClean(r); + } + { + INFO("a call that sets what the other's reads"); + CHECK(noted(r, "SluaRightFirstCall", "fill()")); + CHECK(has(r, "fill() sets gCache, which the other side reads or sets too.\n print(cached() .. fill())")); + } + UNSCOPED_INFO("noted twice, no more"); + CHECK(count(r, "LSL ran the right side") == 2); + require_checks_clean(r); } - template<> template<> - void allsltoslua_object::test<100>() + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: with keyTextAny off text for a key passes bare and the checker marks it", "[ALLSLToSLua]") { - set_test_name("text given where ll takes a key, with keyTextAny off, as it is with no :: any (Tapple's), the note giving the " - "ways to see to it; what the type checker then marks, where it knows the text for text"); const std::string lsl = "key inventorySender;\n" "string myInfo() { return (string)llGetKey(); }\n" "default { touch_start(integer n) {\n" @@ -3335,11 +4352,21 @@ namespace tut ALLSLToSLua::Options o; o.keyTextAny = false; const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, o); - ensure("converted", r.converted); - ensure("as it is: " + r.text, has(r, "ll.Key2Name(myInfo()))") && !has(r, "myInfo() :: any")); - ensure("a UUID written out, still a uuid: " + r.text, has(r, "ll.Key2Name(uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\"))")); - ensure("the note with the ways: " + r.text, noted(r, "SluaKeyAsTextBare") && !noted(r, "SluaKeyAsText") && - has(r, "`:: any` quiets it, uuid() stops the script on text that is no UUID")); + CAPTURE(r.text); + CHECK(r.converted); + { + INFO("as it is"); + CHECK(has(r, "ll.Key2Name(myInfo()))")); + CHECK_FALSE(has(r, "myInfo() :: any")); + } + UNSCOPED_INFO("a UUID written out, still a uuid"); + CHECK(has(r, "ll.Key2Name(uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\"))")); + { + INFO("the note with the ways"); + CHECK(noted(r, "SluaKeyAsTextBare")); + CHECK_FALSE(noted(r, "SluaKeyAsText")); + CHECK(has(r, "`:: any` quiets it, uuid() stops the script on text that is no UUID")); + } // What the checker marks as an error, each by its line. const auto marked = [&](const std::string& slua) { std::string said; @@ -3352,7 +4379,8 @@ namespace tut // The line the text for a key is on, as marked says it. const auto lineOf = [](const std::string& slua) { const size_t at = slua.find("ll.Key2Name(myInfo())"); - ensure("written: " + slua, at != std::string::npos); + CAPTURE(slua); + REQUIRE(at != std::string::npos); return llformat("[%d] ", static_cast(std::count(slua.begin(), slua.begin() + at, '\n')) + 1); }; // Nonstrict, as the grid has it, takes an untyped function's answer @@ -3364,65 +4392,95 @@ namespace tut o.types = true; const ALLSLToSLua::Result typed = ALLSLToSLua::convert(lsl, o); const std::string said = marked(typed.text); - ensure("typed, the text for a key marked, alone:\n" + said + "---\n" + typed.text, - said.rfind(lineOf(typed.text), 0) == 0 && std::count(said.begin(), said.end(), '\n') == 1); + { + INFO("typed, the text for a key marked, alone"); + CAPTURE(said, typed.text); + CHECK(said.rfind(lineOf(typed.text), 0) == 0); + CHECK(std::count(said.begin(), said.end(), '\n') == 1); + } const std::string strict = "--!strict\n" + r.text; const std::string loud = marked(strict); - ensure("strict, the same:\n" + loud + "---\n" + strict, loud.rfind(lineOf(strict), 0) == 0 && std::count(loud.begin(), loud.end(), '\n') == 1); - } - } - template<> template<> - void allsltoslua_object::test<101>() - { - set_test_name("a key given no text holds text, as LSL's did (Tapple's): compared with \"\", sent over a link message, made text " - "and asked its truth as LSL had it; a key that is a uuid compared with no text or a UUID written out as a uuid, " - "other text and NULL_KEY as text"); - const ALLSLToSLua::Result r = convert("key gUser = \"\";\n" - "default {\n" - " state_entry() {\n" - " key k = \"\";\n" - " llOwnerSay((string)(k == \"\"));\n" - " }\n" - " touch_start(integer n) {\n" - " if (gUser) llOwnerSay(\"busy\");\n" - " gUser = llDetectedKey(0);\n" - " llMessageLinked(LINK_SET, 0, \"\", gUser);\n" - " gUser = \"\";\n" - " llMessageLinked(LINK_SET, 0, \"\", gUser);\n" - " llOwnerSay(\"user: \" + (string)gUser);\n" - " }\n" - " link_message(integer s, integer num, string m, key id) {\n" - " if (id == \"\") llOwnerSay(\"nobody\");\n" - " if (id) llOwnerSay(\"someone\");\n" - " llOwnerSay(llList2CSV([id, gUser]));\n" - " key d;\n" - " if (d == \"\") llOwnerSay(\"no key yet\");\n" - " if (\"\" != d) llOwnerSay(\"some\");\n" - " if (d == \"a2e76fcd-9360-4f6d-a924-938f923df11d\") llOwnerSay(\"that one\");\n" - " if (d == \"none\") llOwnerSay(\"text\");\n" - " if (NULL_KEY != \"\") llOwnerSay(\"NULL_KEY has text\");\n" - " }\n" - "}\n"); - ensure("Tapple's, text: " + r.text, has(r, "local k = \"\"") && has(r, "print(tostring(if k == \"\" then 1 else 0))")); - ensure("a global given no text, text: " + r.text, has(r, "local gUser = \"\"") && has(r, "gUser = tostring(detected[1]:getKey())")); - ensure("sent as text, no text and all: " + r.text, count(r, "ll.MessageLinked(LINK_SET, 0, \"\", gUser)") == 2); - ensure("made text as it is: " + r.text, has(r, "print(\"user: \" .. gUser)")); - ensure("its truth as LSL's key's: " + r.text, has(r, "if (touuid(gUser) or NULL_KEY).istruthy then")); - ensure("the id, text: " + r.text, has(r, "if id == \"\" then") && has(r, "if (touuid(id) or NULL_KEY).istruthy then")); - ensure("keys held as text, in a list as text: " + r.text, has(r, "{ id, gUser }") && !has(r, "{ uuid(")); - ensure("a uuid against no text, a uuid: " + r.text, has(r, "local d = NULL_KEY") && has(r, "if d == uuid(\"\") then") && - has(r, "if uuid(\"\") ~= d then")); - ensure("a UUID written out: " + r.text, has(r, "if d == uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\") then")); - ensure("other text, as text: " + r.text, has(r, "if tostring(d) == \"none\" then")); - ensure("NULL_KEY, as LSL's text: " + r.text, has(r, "if tostring(NULL_KEY) ~= \"\" then")); - checksClean(r); - } - template<> template<> - void allsltoslua_object::test<102>() - { - set_test_name("with uuidKeys off, every key of the script's own held as text, as LSL held them (Tapple's preference): its " - "variables, its functions' and handlers' parameters, what its functions give, a cast to a key; what SLua gives as " - "a uuid made text, text given ll as it is; closeToLSL the same"); + { + INFO("strict, the same"); + CAPTURE(loud, strict); + CHECK(loud.rfind(lineOf(strict), 0) == 0); + CHECK(std::count(loud.begin(), loud.end(), '\n') == 1); + } + } + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a key given no text holds text as LSL's did; a uuid key compared as one", "[ALLSLToSLua]") + { + const ALLSLToSLua::Result r = require_converted("key gUser = \"\";\n" + "default {\n" + " state_entry() {\n" + " key k = \"\";\n" + " llOwnerSay((string)(k == \"\"));\n" + " }\n" + " touch_start(integer n) {\n" + " if (gUser) llOwnerSay(\"busy\");\n" + " gUser = llDetectedKey(0);\n" + " llMessageLinked(LINK_SET, 0, \"\", gUser);\n" + " gUser = \"\";\n" + " llMessageLinked(LINK_SET, 0, \"\", gUser);\n" + " llOwnerSay(\"user: \" + (string)gUser);\n" + " }\n" + " link_message(integer s, integer num, string m, key id) {\n" + " if (id == \"\") llOwnerSay(\"nobody\");\n" + " if (id) llOwnerSay(\"someone\");\n" + " llOwnerSay(llList2CSV([id, gUser]));\n" + " key d;\n" + " if (d == \"\") llOwnerSay(\"no key yet\");\n" + " if (\"\" != d) llOwnerSay(\"some\");\n" + " if (d == \"a2e76fcd-9360-4f6d-a924-938f923df11d\") llOwnerSay(\"that one\");\n" + " if (d == \"none\") llOwnerSay(\"text\");\n" + " if (NULL_KEY != \"\") llOwnerSay(\"NULL_KEY has text\");\n" + " }\n" + "}\n"); + CAPTURE(r.text); + { + INFO("Tapple's, text"); + CHECK(has(r, "local k = \"\"")); + CHECK(has(r, "print(tostring(if k == \"\" then 1 else 0))")); + } + { + INFO("a global given no text, text"); + CHECK(has(r, "local gUser = \"\"")); + CHECK(has(r, "gUser = tostring(detected[1]:getKey())")); + } + UNSCOPED_INFO("sent as text, no text and all"); + CHECK(count(r, "ll.MessageLinked(LINK_SET, 0, \"\", gUser)") == 2); + UNSCOPED_INFO("made text as it is"); + CHECK(has(r, "print(\"user: \" .. gUser)")); + UNSCOPED_INFO("its truth as LSL's key's"); + CHECK(has(r, "if (touuid(gUser) or NULL_KEY).istruthy then")); + { + INFO("the id, text"); + CHECK(has(r, "if id == \"\" then")); + CHECK(has(r, "if (touuid(id) or NULL_KEY).istruthy then")); + } + { + INFO("keys held as text, in a list as text"); + CHECK(has(r, "{ id, gUser }")); + CHECK_FALSE(has(r, "{ uuid(")); + } + { + INFO("a uuid against no text, a uuid"); + CHECK(has(r, "local d = NULL_KEY")); + CHECK(has(r, "if d == uuid(\"\") then")); + CHECK(has(r, "if uuid(\"\") ~= d then")); + } + UNSCOPED_INFO("a UUID written out"); + CHECK(has(r, "if d == uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\") then")); + UNSCOPED_INFO("other text, as text"); + CHECK(has(r, "if tostring(d) == \"none\" then")); + UNSCOPED_INFO("NULL_KEY, as LSL's text"); + CHECK(has(r, "if tostring(NULL_KEY) ~= \"\" then")); + require_checks_clean(r); + } + + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: with uuidKeys off the script's own keys are held as text as in LSL", "[ALLSLToSLua]") + { const std::string lsl = "key gOwner;\n" "key pick() { return llGetOwner(); }\n" "integer mine(key who) { return who == gOwner; }\n" @@ -3447,26 +4505,70 @@ namespace tut o.uuidKeys = false; o.types = true; const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, o); - ensure("converted: " + (r.problems.empty() ? std::string() : r.problems.front().message), r.converted); - ensure("Tapple's: " + r.text, has(r, "local k: string = \"\"\n") && has(r, "print(tostring(if k == \"\" then 1 else 0))")); - ensure("a global, LSL's default: " + r.text, has(r, "local gOwner: string = \"\"\n") && has(r, "gOwner = tostring(ll.GetOwner())")); - ensure("what a function gives: " + r.text, has(r, "local function pick(): string\n return tostring(ll.GetOwner())")); - ensure("a parameter: " + r.text, has(r, "local function mine(who: string): boolean") && has(r, "who == gOwner")); - ensure("a cast, the text as it is: " + r.text, has(r, "local cast: string = s\n") && has(r, "print(cast .. \"\")") && !has(r, "uuid(s)") && !has(r, "uuid(\"\")")); - ensure("its truth as LSL's: " + r.text, has(r, "if (touuid(gOwner) or NULL_KEY).istruthy then")); - ensure("given ll as it is: " + r.text, has(r, "ll.Dialog(gOwner :: any, \"hi\"")); - ensure("against NULL_KEY, as text: " + r.text, has(r, "gOwner ~= tostring(NULL_KEY)")); - ensure("a handler's key, made text as it begins: " + r.text, has(r, "function(c: number, n: string, idKey: uuid, m: string)") && - has(r, "local id: string = tostring(idKey)") && has(r, "if id == gOwner then")); - ensure("in a list, text as it is: " + r.text, has(r, "local values: { any } = { odd, k, tostring(NULL_KEY) }") && !has(r, "{ uuid(")); - ensure("nothing said of NULL_KEY: " + r.text, !noted(r, "SluaKeyEmpty")); - checksClean(r); + CAPTURE(r.text); + UNSCOPED_INFO("converted: " + (r.problems.empty() ? std::string() : r.problems.front().message)); + CHECK(r.converted); + { + INFO("Tapple's"); + CHECK(has(r, "local k: string = \"\"\n")); + CHECK(has(r, "print(tostring(if k == \"\" then 1 else 0))")); + } + { + INFO("a global, LSL's default"); + CHECK(has(r, "local gOwner: string = \"\"\n")); + CHECK(has(r, "gOwner = tostring(ll.GetOwner())")); + } + UNSCOPED_INFO("what a function gives"); + CHECK(has(r, "local function pick(): string\n return tostring(ll.GetOwner())")); + { + INFO("a parameter"); + CHECK(has(r, "local function mine(who: string): boolean")); + CHECK(has(r, "who == gOwner")); + } + { + INFO("a cast, the text as it is"); + CHECK(has(r, "local cast: string = s\n")); + CHECK(has(r, "print(cast .. \"\")")); + CHECK_FALSE(has(r, "uuid(s)")); + CHECK_FALSE(has(r, "uuid(\"\")")); + } + UNSCOPED_INFO("its truth as LSL's"); + CHECK(has(r, "if (touuid(gOwner) or NULL_KEY).istruthy then")); + UNSCOPED_INFO("given ll as it is"); + CHECK(has(r, "ll.Dialog(gOwner :: any, \"hi\"")); + UNSCOPED_INFO("against NULL_KEY, as text"); + CHECK(has(r, "gOwner ~= tostring(NULL_KEY)")); + { + INFO("a handler's key, made text as it begins"); + CHECK(has(r, "function(c: number, n: string, idKey: uuid, m: string)")); + CHECK(has(r, "local id: string = tostring(idKey)")); + CHECK(has(r, "if id == gOwner then")); + } + { + INFO("in a list, text as it is"); + CHECK(has(r, "local values: { any } = { odd, k, tostring(NULL_KEY) }")); + CHECK_FALSE(has(r, "{ uuid(")); + } + UNSCOPED_INFO("nothing said of NULL_KEY"); + CHECK_FALSE(noted(r, "SluaKeyEmpty")); + require_checks_clean(r); const ALLSLToSLua::Result close = ALLSLToSLua::convert(lsl, ALLSLToSLua::Options::closeToLSL()); - ensure("closeToLSL the same: " + close.text, close.converted && has(close, "local gOwner = \"\"\n") && has(close, "local cast = s\n")); - checksClean(close); + CAPTURE(close.text); + { + INFO("closeToLSL the same"); + CHECK(close.converted); + CHECK(has(close, "local gOwner = \"\"\n")); + CHECK(has(close, "local cast = s\n")); + } + require_checks_clean(close); - const ALLSLToSLua::Result uuids = convert(lsl); - ensure("uuids by default: " + uuids.text, has(uuids, "local gOwner = NULL_KEY\n") && has(uuids, "local cast = uuid(s)")); + const ALLSLToSLua::Result uuids = require_converted(lsl); + CAPTURE(uuids.text); + { + INFO("uuids by default"); + CHECK(has(uuids, "local gOwner = NULL_KEY\n")); + CHECK(has(uuids, "local cast = uuid(s)")); + } } } diff --git a/indra/alscript/tests/alluauconfig_test.cpp b/indra/alscript/tests/alluauconfig_test.cpp index 3ba1ed2935..f26727fe6e 100644 --- a/indra/alscript/tests/alluauconfig_test.cpp +++ b/indra/alscript/tests/alluauconfig_test.cpp @@ -30,92 +30,155 @@ #include "lluuid.h" -#include "../test/lltut.h" +#include #include -namespace tut +namespace { - struct alluauconfig_data + TEST_CASE("alluauconfig: aliases by lower-case name with their paths and the mode where it is said", "[alluauconfig]") { - }; - - typedef test_group alluauconfig_group; - typedef alluauconfig_group::object alluauconfig_object; - alluauconfig_group alluauconfig_instance("alluauconfig"); - - template<> template<> - void alluauconfig_object::test<1>() - { - set_test_name("aliases come out by lower-case name with their paths, and the mode where it is said"); ALLuauConfig config; std::string error; - ensure("parses: " + error, ALLuauConfig::parse("{\n \"languageMode\": \"strict\",\n \"aliases\": { \"Lib\": \"./lib\", \"shared\": \"../shared\", \"abs\": \"/opt/lua\" }\n}\n", config, error)); - ensure_equals("three aliases", config.aliases.size(), size_t(3)); - ensure_equals("by lower-case name", config.aliases["lib"], std::string("./lib")); - ensure_equals("a path up", config.aliases["shared"], std::string("../shared")); - ensure_equals("an absolute one", config.aliases["abs"], std::string("/opt/lua")); - ensure_equals("the mode", config.mode, std::string("strict")); - ensure("no mode where none is said", ALLuauConfig::parse("{ \"aliases\": {} }", config, error) && config.mode.empty()); - ensure("not a configuration", !ALLuauConfig::parse("{ \"aliases\": 3 }", config, error) && !error.empty()); - ensure("nor an alias Luau refuses", !ALLuauConfig::parse("{ \"aliases\": { \"bad name\": \"x\" } }", config, error)); + REQUIRE(ALLuauConfig::parse("{\n \"languageMode\": \"strict\",\n \"aliases\": { \"Lib\": \"./lib\", \"shared\": \"../shared\", \"abs\": \"/opt/lua\" }\n}\n", config, error)); + REQUIRE(config.aliases.size() == size_t(3)); + UNSCOPED_INFO("by lower-case name"); + CHECK(config.aliases["lib"] == std::string("./lib")); + UNSCOPED_INFO("a path up"); + CHECK(config.aliases["shared"] == std::string("../shared")); + UNSCOPED_INFO("an absolute one"); + CHECK(config.aliases["abs"] == std::string("/opt/lua")); + CHECK(config.mode == std::string("strict")); + { + INFO("no mode where none is said"); + CHECK(ALLuauConfig::parse("{ \"aliases\": {} }", config, error)); + REQUIRE(config.mode.empty()); + } + { + INFO("not a configuration"); + CHECK_FALSE(ALLuauConfig::parse("{ \"aliases\": 3 }", config, error)); + REQUIRE_FALSE(error.empty()); + } + UNSCOPED_INFO("nor an alias Luau refuses"); + CHECK_FALSE(ALLuauConfig::parse("{ \"aliases\": { \"bad name\": \"x\" } }", config, error)); } - template<> template<> - void alluauconfig_object::test<2>() + TEST_CASE("alluauconfig: aliasOf reads the @ name up to the slash; absolute paths and studio aliases", "[alluauconfig]") { - set_test_name("a require name's alias is what follows the @ up to the slash, and absolute paths are known on both platforms"); std::string alias, rest; - ensure("found", ALLuauConfig::aliasOf("@Lib/util/strings", alias, rest)); - ensure_equals("lower case", alias, std::string("lib")); - ensure_equals("the rest", rest, std::string("util/strings")); - ensure("the alias alone", ALLuauConfig::aliasOf("@lib", alias, rest) && alias == "lib" && rest.empty()); - ensure("not without the @", !ALLuauConfig::aliasOf("lib/util", alias, rest)); - ensure("not an empty one", !ALLuauConfig::aliasOf("@/util", alias, rest)); - ensure("absolute on posix", ALLuauConfig::absolute("/opt/lua")); - ensure("absolute on windows", ALLuauConfig::absolute("C:\\lua") && ALLuauConfig::absolute("c:/lua")); - ensure("relative", !ALLuauConfig::absolute("./lib") && !ALLuauConfig::absolute("lib")); + REQUIRE(ALLuauConfig::aliasOf("@Lib/util/strings", alias, rest)); + UNSCOPED_INFO("lower case"); + CHECK(alias == std::string("lib")); + CHECK(rest == std::string("util/strings")); + { + INFO("the alias alone"); + CHECK(ALLuauConfig::aliasOf("@lib", alias, rest)); + CHECK(alias == "lib"); + REQUIRE(rest.empty()); + } + UNSCOPED_INFO("not without the @"); + CHECK_FALSE(ALLuauConfig::aliasOf("lib/util", alias, rest)); + UNSCOPED_INFO("not an empty one"); + CHECK_FALSE(ALLuauConfig::aliasOf("@/util", alias, rest)); + UNSCOPED_INFO("absolute on posix"); + CHECK(ALLuauConfig::absolute("/opt/lua")); + { + INFO("absolute on windows"); + CHECK(ALLuauConfig::absolute("C:\\lua")); + CHECK(ALLuauConfig::absolute("c:/lua")); + } + { + INFO("relative"); + CHECK_FALSE(ALLuauConfig::absolute("./lib")); + CHECK_FALSE(ALLuauConfig::absolute("lib")); + } // Script Studio's own aliases: what Luau takes, not self, not sl-*. - ensure("a studio alias's name", ALLuauConfig::studioAliasName("lib") && ALLuauConfig::studioAliasName("My_Lib-2.x")); - ensure("not self, in any case", !ALLuauConfig::studioAliasName("self") && !ALLuauConfig::studioAliasName("Self")); - ensure("not sl-*", !ALLuauConfig::studioAliasName("sl-std") && !ALLuauConfig::studioAliasName("SL-x")); - ensure("nothing Luau refuses", !ALLuauConfig::studioAliasName("") && !ALLuauConfig::studioAliasName("a/b") && - !ALLuauConfig::studioAliasName("a b") && !ALLuauConfig::studioAliasName("..")); - ensure_equals("a folder's name made one", ALLuauConfig::studioAliasFor("My Library", {}), std::string("my-library")); - ensure_equals("a number where it is taken, in any case", ALLuauConfig::studioAliasFor("lib", { "LIB", "lib2" }), std::string("lib3")); - ensure_equals("one that cannot be: lib", ALLuauConfig::studioAliasFor("self", {}), std::string("lib")); + { + INFO("a studio alias's name"); + CHECK(ALLuauConfig::studioAliasName("lib")); + CHECK(ALLuauConfig::studioAliasName("My_Lib-2.x")); + } + { + INFO("not self, in any case"); + CHECK_FALSE(ALLuauConfig::studioAliasName("self")); + CHECK_FALSE(ALLuauConfig::studioAliasName("Self")); + } + { + INFO("not sl-*"); + CHECK_FALSE(ALLuauConfig::studioAliasName("sl-std")); + CHECK_FALSE(ALLuauConfig::studioAliasName("SL-x")); + } + { + INFO("nothing Luau refuses"); + CHECK_FALSE(ALLuauConfig::studioAliasName("")); + CHECK_FALSE(ALLuauConfig::studioAliasName("a/b")); + CHECK_FALSE(ALLuauConfig::studioAliasName("a b")); + CHECK_FALSE(ALLuauConfig::studioAliasName("..")); + } + UNSCOPED_INFO("a folder's name made one"); + CHECK(ALLuauConfig::studioAliasFor("My Library", {}) == std::string("my-library")); + UNSCOPED_INFO("a number where it is taken, in any case"); + CHECK(ALLuauConfig::studioAliasFor("lib", { "LIB", "lib2" }) == std::string("lib3")); + UNSCOPED_INFO("one that cannot be: lib"); + CHECK(ALLuauConfig::studioAliasFor("self", {}) == std::string("lib")); } - template<> template<> - void alluauconfig_object::test<3>() + + TEST_CASE("alluauconfig: lints and globals come out as Luau reads them with its default lints", "[alluauconfig]") { - set_test_name("the lints and the globals come out as Luau reads them, the lints as its defaults where the file says nothing"); ALLuauConfig config; std::string error; const ALLuauConfig defaults; - ensure("parses: " + error, ALLuauConfig::parse("{\n \"lint\": { \"LocalUnused\": false, \"LocalShadow\": true },\n \"globals\": [\"Alchemy\", \"grid\"]\n}\n", config, error)); - ensure_equals("two globals", config.globals.size(), size_t(2)); - ensure_equals("in order", config.globals[0], std::string("Alchemy")); + REQUIRE(ALLuauConfig::parse("{\n \"lint\": { \"LocalUnused\": false, \"LocalShadow\": true },\n \"globals\": [\"Alchemy\", \"grid\"]\n}\n", config, error)); + REQUIRE(config.globals.size() == size_t(2)); + UNSCOPED_INFO("in order"); + CHECK(config.globals[0] == std::string("Alchemy")); // LocalUnused is code 7 and LocalShadow code 4 in Luau's numbering; // the second is off by default. - ensure("LocalUnused off", (config.lints & (1ull << 7)) == 0); - ensure("LocalShadow on", (config.lints & (1ull << 4)) != 0); - ensure("the rest as they were", (config.lints & ~((1ull << 7) | (1ull << 4))) == (defaults.lints & ~((1ull << 7) | (1ull << 4)))); - ensure("none fatal, not every lint an error", config.fatalLints == 0 && !config.lintErrors); - ensure("said nothing: the defaults", ALLuauConfig::parse("{}", config, error) && config.lints == defaults.lints && config.fatalLints == 0 && config.globals.empty()); - ensure("every lint an error where asked", ALLuauConfig::parse("{ \"lintErrors\": true }", config, error) && config.lintErrors); + UNSCOPED_INFO("LocalUnused off"); + CHECK((config.lints & (1ull << 7)) == 0); + UNSCOPED_INFO("LocalShadow on"); + CHECK((config.lints & (1ull << 4)) != 0); + UNSCOPED_INFO("the rest as they were"); + CHECK((config.lints & ~((1ull << 7) | (1ull << 4))) == (defaults.lints & ~((1ull << 7) | (1ull << 4)))); + { + INFO("none fatal, not every lint an error"); + CHECK(config.fatalLints == 0); + CHECK_FALSE(config.lintErrors); + } + { + INFO("said nothing: the defaults"); + CHECK(ALLuauConfig::parse("{}", config, error)); + CHECK(config.lints == defaults.lints); + CHECK(config.fatalLints == 0); + REQUIRE(config.globals.empty()); + } + { + INFO("every lint an error where asked"); + CHECK(ALLuauConfig::parse("{ \"lintErrors\": true }", config, error)); + CHECK(config.lintErrors); + } } - template<> template<> - void alluauconfig_object::test<4>() + TEST_CASE("alluauconfig: a .luaurc over a scripter's own lints and mode overrides only what it says", "[alluauconfig]") { - set_test_name("a .luaurc over a scripter's own lints and mode overrides only what it says"); const uint64_t unused = ALLuauConfig::lintBit("LocalUnused"); const uint64_t function = ALLuauConfig::lintBit("FunctionUnused"); const uint64_t shadow = ALLuauConfig::lintBit("LocalShadow"); - ensure("the lints have bits", unused && function && shadow && unused != function); - ensure("an unknown lint has none", ALLuauConfig::lintBit("NoSuchLint") == 0); - ensure("every lint is named", ALLuauConfig::lintNames().size() > 20 && ALLuauConfig::lintNames().front() == "UnknownGlobal"); + { + INFO("the lints have bits"); + CHECK(unused); + CHECK(function); + CHECK(shadow); + CHECK(unused != function); + } + UNSCOPED_INFO("an unknown lint has none"); + CHECK(ALLuauConfig::lintBit("NoSuchLint") == 0); + { + INFO("every lint is named"); + REQUIRE(ALLuauConfig::lintNames().size() > 20); + CHECK(ALLuauConfig::lintNames().front() == "UnknownGlobal"); + } ALLuauConfig base; base.lints &= ~unused; // the scripter turned it off @@ -123,41 +186,61 @@ namespace tut base.mode = "strict"; ALLuauConfig config; std::string error; - ensure("an empty file", ALLuauConfig::parse("{}", config, error, &base)); - ensure("keeps the lints off", (config.lints & unused) == 0 && (config.lints & function) != 0); - ensure("and the errors", (config.fatalLints & shadow) != 0); - ensure_equals("and the mode", config.mode, std::string("strict")); + UNSCOPED_INFO("an empty file"); + REQUIRE(ALLuauConfig::parse("{}", config, error, &base)); + { + INFO("keeps the lints off"); + CHECK((config.lints & unused) == 0); + CHECK((config.lints & function) != 0); + } + UNSCOPED_INFO("and the errors"); + CHECK((config.fatalLints & shadow) != 0); + UNSCOPED_INFO("and the mode"); + CHECK(config.mode == std::string("strict")); - ensure("a file saying one lint", ALLuauConfig::parse("{ \"lint\": { \"LocalUnused\": true, \"FunctionUnused\": false }, \"languageMode\": \"nonstrict\" }", config, error, &base)); - ensure("turns that one on", (config.lints & unused) != 0); - ensure("and that one off", (config.lints & function) == 0); - ensure("the rest as the scripter had them", (config.fatalLints & shadow) != 0); - ensure_equals("its mode over theirs", config.mode, std::string("nonstrict")); + UNSCOPED_INFO("a file saying one lint"); + CHECK(ALLuauConfig::parse("{ \"lint\": { \"LocalUnused\": true, \"FunctionUnused\": false }, \"languageMode\": \"nonstrict\" }", config, error, &base)); + UNSCOPED_INFO("turns that one on"); + CHECK((config.lints & unused) != 0); + UNSCOPED_INFO("and that one off"); + CHECK((config.lints & function) == 0); + UNSCOPED_INFO("the rest as the scripter had them"); + CHECK((config.fatalLints & shadow) != 0); + UNSCOPED_INFO("its mode over theirs"); + CHECK(config.mode == std::string("nonstrict")); - ensure("without a base, Luau's defaults", ALLuauConfig::parse("{}", config, error) && (config.lints & unused) != 0 && config.mode.empty()); + { + INFO("without a base, Luau's defaults"); + CHECK(ALLuauConfig::parse("{}", config, error)); + CHECK((config.lints & unused) != 0); + REQUIRE(config.mode.empty()); + } } - template<> template<> - void alluauconfig_object::test<5>() + TEST_CASE("alluauconfig: a file read over another adds to the globals and overrides the aliases", "[alluauconfig]") { - set_test_name("a file read over another adds its globals to the other's, and its aliases over the other's, as Luau reads a chain of them"); ALLuauConfig outer; std::string error; - ensure("the outer file", ALLuauConfig::parse("{ \"globals\": [\"shared\"], \"aliases\": { \"lib\": \"./lib\", \"util\": \"./util\" } }", outer, error)); + UNSCOPED_INFO("the outer file"); + REQUIRE(ALLuauConfig::parse("{ \"globals\": [\"shared\"], \"aliases\": { \"lib\": \"./lib\", \"util\": \"./util\" } }", outer, error)); ALLuauConfig inner; - ensure("the inner over it", ALLuauConfig::parse("{ \"globals\": [\"mine\"], \"aliases\": { \"lib\": \"./mylib\" } }", inner, error, &outer)); - ensure("both globals", inner.globals == std::vector{ "shared", "mine" }); - ensure_equals("the nearer alias", inner.aliases["lib"], std::string("./mylib")); - ensure_equals("the outer's kept", inner.aliases["util"], std::string("./util")); + UNSCOPED_INFO("the inner over it"); + REQUIRE(ALLuauConfig::parse("{ \"globals\": [\"mine\"], \"aliases\": { \"lib\": \"./mylib\" } }", inner, error, &outer)); + UNSCOPED_INFO("both globals"); + CHECK(inner.globals == std::vector{ "shared", "mine" }); + UNSCOPED_INFO("the nearer alias"); + CHECK(inner.aliases["lib"] == std::string("./mylib")); + UNSCOPED_INFO("the outer's kept"); + CHECK(inner.aliases["util"] == std::string("./util")); ALLuauConfig quiet; - ensure("one saying nothing of them", ALLuauConfig::parse("{}", quiet, error, &outer)); - ensure("keeps them", quiet.globals == std::vector{ "shared" }); + UNSCOPED_INFO("one saying nothing of them"); + REQUIRE(ALLuauConfig::parse("{}", quiet, error, &outer)); + UNSCOPED_INFO("keeps them"); + CHECK(quiet.globals == std::vector{ "shared" }); } - template<> template<> - void alluauconfig_object::test<6>() + TEST_CASE("alluauconfig: parseChain: the nearest file to say a thing wins and globals add up", "[alluauconfig]") { - set_test_name("a chain of files, nearest first, read from the top down: the nearest to say a thing wins, and globals add up"); const std::string root = "{ \"languageMode\": \"strict\", \"globals\": [\"a\"], \"lint\": { \"LocalUnused\": false }, " "\"aliases\": { \"lib\": \"./rootlib\", \"root\": \"./r\" } }"; const std::string broken = "{ not a configuration"; @@ -166,64 +249,108 @@ namespace tut const std::vector chain{ nearby, middle, broken, root }; ALLuauConfig merged; - ensure("parsed", ALLuauConfig::parseChain(chain, merged)); - ensure_equals("the nearest mode", merged.mode, std::string("nonstrict")); - ensure("globals from every file, the furthest first", merged.globals == std::vector{ "a", "b", "c" }); + REQUIRE(ALLuauConfig::parseChain(chain, merged)); + UNSCOPED_INFO("the nearest mode"); + CHECK(merged.mode == std::string("nonstrict")); + UNSCOPED_INFO("globals from every file, the furthest first"); + CHECK(merged.globals == std::vector{ "a", "b", "c" }); const uint64_t local_unused = ALLuauConfig::lintBit("LocalUnused"); const uint64_t function_unused = ALLuauConfig::lintBit("FunctionUnused"); - ensure("a lint the root turned off and the nearest on", (merged.lints & local_unused) != 0); - ensure("one the middle turned off, off", (merged.lints & function_unused) == 0); - ensure_equals("an alias the middle says over the root's", merged.aliases["lib"], std::string("./midlib")); - ensure_equals("the root's own kept", merged.aliases["root"], std::string("./r")); + UNSCOPED_INFO("a lint the root turned off and the nearest on"); + CHECK((merged.lints & local_unused) != 0); + UNSCOPED_INFO("one the middle turned off, off"); + CHECK((merged.lints & function_unused) == 0); + UNSCOPED_INFO("an alias the middle says over the root's"); + CHECK(merged.aliases["lib"] == std::string("./midlib")); + UNSCOPED_INFO("the root's own kept"); + CHECK(merged.aliases["root"] == std::string("./r")); ALLuauConfig base; base.mode = "strict"; ALLuauConfig none; - ensure("none that parse is no configuration", !ALLuauConfig::parseChain({ broken }, none, &base)); - ensure_equals("and leaves the base", none.mode, std::string("strict")); + UNSCOPED_INFO("none that parse is no configuration"); + REQUIRE_FALSE(ALLuauConfig::parseChain({ broken }, none, &base)); + UNSCOPED_INFO("and leaves the base"); + CHECK(none.mode == std::string("strict")); } - template<> template<> - void alluauconfig_object::test<7>() + TEST_CASE("alluauconfig: the studio's lints in a .luaurc are read apart from Luau's and * covers them", "[alluauconfig]") { - set_test_name("the studio's own lints in a .luaurc: taken out for Luau and said for the studio, first, last or alone, with the rest read whole; * says for them too"); const uint64_t compound = ALScriptLintPass::bit("SlCompoundAssign"); - ensure("a rule of the table", compound != 0 && (ALScriptLintPass::defaults() & compound) != 0); + { + INFO("a rule of the table"); + CHECK(compound != 0); + CHECK((ALScriptLintPass::defaults() & compound) != 0); + } ALLuauConfig out; std::string error; - ensure("beside Luau's: " + error, - ALLuauConfig::parse("{ \"languageMode\": \"strict\", \"lint\": { \"LocalUnused\": false, \"SlCompoundAssign\": false } }", out, error)); - ensure("the rest read", out.mode == "strict" && (out.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); - ensure("ours off", (out.slLints & compound) == 0); - ensure("alone: " + error, ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": false } }", out, error) && (out.slLints & compound) == 0); - ensure("first: " + error, ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": false, \"LocalUnused\": false }, \"globals\": [\"g\"] }", out, error) && - (out.slLints & compound) == 0 && out.globals == std::vector{ "g" } && - (out.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); - ensure("with // comments, over lines: " + error, - ALLuauConfig::parse("{\n // c style\n \"lint\": {\n \"LocalUnused\": true,\n \"SlCompoundAssign\": false\n }\n}", out, error) && - (out.slLints & compound) == 0); - ensure("* for ours too: " + error, ALLuauConfig::parse("{ \"lint\": { \"*\": false } }", out, error) && out.slLints == 0 && - (out.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); - ensure("then one on: " + error, - ALLuauConfig::parse("{ \"lint\": { \"*\": false, \"SlCompoundAssign\": true } }", out, error) && out.slLints == compound); - ensure("an Sl name that is none", !ALLuauConfig::parse("{ \"lint\": { \"SlNope\": true } }", out, error) && error == "Unknown lint SlNope"); - ensure("a value that is none", !ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": \"fatal\" } }", out, error) && - error == "Bad setting 'fatal'. Valid options are true and false"); - ensure("Luau's words on the rest keep their lines: " + error, - !ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": false },\n \"globals\": 3 }", out, error) && error.find("line 2") != std::string::npos); + UNSCOPED_INFO("beside Luau's"); + REQUIRE(ALLuauConfig::parse("{ \"languageMode\": \"strict\", \"lint\": { \"LocalUnused\": false, \"SlCompoundAssign\": false } }", out, error)); + { + INFO("the rest read"); + CHECK(out.mode == "strict"); + CHECK((out.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); + } + UNSCOPED_INFO("ours off"); + CHECK((out.slLints & compound) == 0); + { + INFO("alone"); + CHECK(ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": false } }", out, error)); + CHECK((out.slLints & compound) == 0); + } + { + INFO("first"); + CHECK(ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": false, \"LocalUnused\": false }, \"globals\": [\"g\"] }", out, error)); + CHECK((out.slLints & compound) == 0); + CHECK(out.globals == std::vector{ "g" }); + CHECK((out.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); + } + { + INFO("with // comments, over lines"); + CHECK(ALLuauConfig::parse("{\n // c style\n \"lint\": {\n \"LocalUnused\": true,\n \"SlCompoundAssign\": false\n }\n}", out, error)); + CHECK((out.slLints & compound) == 0); + } + { + INFO("* for ours too"); + CHECK(ALLuauConfig::parse("{ \"lint\": { \"*\": false } }", out, error)); + CHECK(out.slLints == 0); + CHECK((out.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); + } + { + INFO("then one on"); + CHECK(ALLuauConfig::parse("{ \"lint\": { \"*\": false, \"SlCompoundAssign\": true } }", out, error)); + CHECK(out.slLints == compound); + } + { + INFO("an Sl name that is none"); + CHECK_FALSE(ALLuauConfig::parse("{ \"lint\": { \"SlNope\": true } }", out, error)); + CHECK(error == "Unknown lint SlNope"); + } + { + INFO("a value that is none"); + CHECK_FALSE(ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": \"fatal\" } }", out, error)); + CHECK(error == "Bad setting 'fatal'. Valid options are true and false"); + } + { + INFO("Luau's words on the rest keep their lines"); + CHECK_FALSE(ALLuauConfig::parse("{ \"lint\": { \"SlCompoundAssign\": false },\n \"globals\": 3 }", out, error)); + REQUIRE(error.find("line 2") != std::string::npos); + } ALLuauConfig base; base.slLints = 0; base.slFatalLints = compound; - ensure("the base's where the file does not say: " + error, - ALLuauConfig::parse("{ \"languageMode\": \"strict\" }", out, error, &base) && out.slLints == 0 && out.slFatalLints == compound); + { + INFO("the base's where the file does not say"); + CHECK(ALLuauConfig::parse("{ \"languageMode\": \"strict\" }", out, error, &base)); + CHECK(out.slLints == 0); + CHECK(out.slFatalLints == compound); + } } - template<> template<> - void alluauconfig_object::test<8>() + TEST_CASE("alluauconfig: a .config.luau's luau table reads as the .luaurc that says the same", "[alluauconfig]") { - set_test_name("a .config.luau is run and its luau table read as the .luaurc that says the same: aliases, mode, lints and globals, the studio's lints among them"); const char* const source = "local root = './lib'\n" "local lints = { LocalUnused = false, SlCompoundAssign = false }\n" @@ -239,122 +366,223 @@ namespace tut " other = { tool = true },\n" "}\n"; std::string json, error; - ensure("ran: " + error, ALLuauConfigScript::asLuaurc(source, json, error)); + REQUIRE(ALLuauConfigScript::asLuaurc(source, json, error)); ALLuauConfig config; - ensure("and parses as a .luaurc: " + error + " in " + json, ALLuauConfig::parse(json, config, error)); - ensure_equals("its mode", config.mode, std::string("strict")); - ensure("its aliases, by name in lower case", config.aliases.size() == 2 && config.aliases["lib"] == "./lib" && config.aliases["shared"] == "./lib/shared"); - ensure("its globals in order", config.globals == std::vector{ "Alchemy", "grid" }); - ensure("Luau's lint off", (config.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); - ensure("and the studio's", (config.slLints & ALScriptLintPass::bit("SlCompoundAssign")) == 0); - ensure("every lint an error", config.lintErrors); + UNSCOPED_INFO("and parses as a .luaurc: " + error + " in " + json); + REQUIRE(ALLuauConfig::parse(json, config, error)); + CHECK(config.mode == std::string("strict")); + { + INFO("its aliases, by name in lower case"); + REQUIRE(config.aliases.size() == 2); + CHECK(config.aliases["lib"] == "./lib"); + CHECK(config.aliases["shared"] == "./lib/shared"); + } + UNSCOPED_INFO("its globals in order"); + CHECK(config.globals == std::vector{ "Alchemy", "grid" }); + UNSCOPED_INFO("Luau's lint off"); + CHECK((config.lints & ALLuauConfig::lintBit("LocalUnused")) == 0); + UNSCOPED_INFO("and the studio's"); + CHECK((config.slLints & ALScriptLintPass::bit("SlCompoundAssign")) == 0); + UNSCOPED_INFO("every lint an error"); + CHECK(config.lintErrors); - ensure("no luau table: a configuration that says nothing", ALLuauConfigScript::asLuaurc("return { other = 1 }", json, error) && json == "{}"); - ensure("an empty one the same", ALLuauConfigScript::asLuaurc("return { luau = {} }", json, error) && - ALLuauConfig::parse(json, config, error) && config.aliases.empty()); - ensure("no globals: an empty list", ALLuauConfigScript::asLuaurc("return { luau = { globals = {} } }", json, error) && - ALLuauConfig::parse(json, config, error) && config.globals.empty()); + { + INFO("no luau table: a configuration that says nothing"); + CHECK(ALLuauConfigScript::asLuaurc("return { other = 1 }", json, error)); + CHECK(json == "{}"); + } + { + INFO("an empty one the same"); + CHECK(ALLuauConfigScript::asLuaurc("return { luau = {} }", json, error)); + CHECK(ALLuauConfig::parse(json, config, error)); + REQUIRE(config.aliases.empty()); + } + { + INFO("no globals: an empty list"); + CHECK(ALLuauConfigScript::asLuaurc("return { luau = { globals = {} } }", json, error)); + CHECK(ALLuauConfig::parse(json, config, error)); + REQUIRE(config.globals.empty()); + } // A .luaurc's strings are taken byte for byte: a backslash is kept // as it is, and what one cannot hold is refused. - ensure("a backslash kept: " + error, ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'C:\\\\lua\\\\lib' } } }", json, error) && - ALLuauConfig::parse(json, config, error) && config.aliases["q"] == "C:\\lua\\lib"); - ensure("not a quote", !ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'a\"b' } } }", json, error) && - error.find("may not hold a quote") != std::string::npos); - ensure("nor a break", !ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'a\\nb' } } }", json, error)); - ensure("nor a backslash at the end", !ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'C:\\\\lua\\\\' } } }", json, error)); - ensure("nor in a name", !ALLuauConfigScript::asLuaurc("return { luau = { aliases = { ['a\"'] = 'x' } } }", json, error)); + { + INFO("a backslash kept"); + CHECK(ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'C:\\\\lua\\\\lib' } } }", json, error)); + CHECK(ALLuauConfig::parse(json, config, error)); + CHECK(config.aliases["q"] == "C:\\lua\\lib"); + } + { + INFO("not a quote"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'a\"b' } } }", json, error)); + REQUIRE(error.find("may not hold a quote") != std::string::npos); + } + UNSCOPED_INFO("nor a break"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'a\\nb' } } }", json, error)); + UNSCOPED_INFO("nor a backslash at the end"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { aliases = { q = 'C:\\\\lua\\\\' } } }", json, error)); + UNSCOPED_INFO("nor in a name"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { aliases = { ['a\"'] = 'x' } } }", json, error)); // Luau's own judgement of what the values say. - ensure("a mode that is none: Luau says so", ALLuauConfigScript::asLuaurc("return { luau = { languagemode = 'loose' } }", json, error) && - !ALLuauConfig::parse(json, config, error)); + { + INFO("a mode that is none: Luau says so"); + CHECK(ALLuauConfigScript::asLuaurc("return { luau = { languagemode = 'loose' } }", json, error)); + CHECK_FALSE(ALLuauConfig::parse(json, config, error)); + } } - template<> template<> - void alluauconfig_object::test<9>() + TEST_CASE("alluauconfig: a .config.luau that fails says why; and it cannot reach Eris", "[alluauconfig]") { - set_test_name("a .config.luau that fails says why: not compiling, an error, a yield, the wrong return, a value no configuration holds, too long, too much memory, " - "too much moved at once; and it cannot reach Eris"); std::string json, error; - ensure("does not compile", !ALLuauConfigScript::asLuaurc("return {", json, error) && !error.empty()); - ensure("an error, in its words", !ALLuauConfigScript::asLuaurc("error('no config here')", json, error) && error.find("no config here") != std::string::npos); - ensure_equals("a yield", (ALLuauConfigScript::asLuaurc("coroutine.yield()", json, error), error), std::string("configuration execution cannot yield")); - ensure_equals("nothing returned", (ALLuauConfigScript::asLuaurc("local x = 1", json, error), error), std::string("configuration must return exactly one value")); - ensure_equals("two returned", (ALLuauConfigScript::asLuaurc("return {}, {}", json, error), error), std::string("configuration must return exactly one value")); - ensure_equals("not a table", (ALLuauConfigScript::asLuaurc("return 'x'", json, error), error), std::string("configuration did not return a table")); - ensure_equals("luau not a table", (ALLuauConfigScript::asLuaurc("return { luau = 3 }", json, error), error), - std::string("configuration value for key \"luau\" must be a table")); - ensure("a function where a value goes", !ALLuauConfigScript::asLuaurc("return { luau = { aliases = { x = print } } }", json, error) && - error.find("must be strings, booleans") != std::string::npos); + { + INFO("does not compile"); + REQUIRE_FALSE(ALLuauConfigScript::asLuaurc("return {", json, error)); + REQUIRE_FALSE(error.empty()); + } + { + INFO("an error, in its words"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("error('no config here')", json, error)); + REQUIRE(error.find("no config here") != std::string::npos); + } + UNSCOPED_INFO("a yield"); + CHECK((ALLuauConfigScript::asLuaurc("coroutine.yield()", json, error), error) == std::string("configuration execution cannot yield")); + UNSCOPED_INFO("nothing returned"); + CHECK((ALLuauConfigScript::asLuaurc("local x = 1", json, error), error) == std::string("configuration must return exactly one value")); + UNSCOPED_INFO("two returned"); + CHECK((ALLuauConfigScript::asLuaurc("return {}, {}", json, error), error) == std::string("configuration must return exactly one value")); + UNSCOPED_INFO("not a table"); + CHECK((ALLuauConfigScript::asLuaurc("return 'x'", json, error), error) == std::string("configuration did not return a table")); + UNSCOPED_INFO("luau not a table"); + CHECK((ALLuauConfigScript::asLuaurc("return { luau = 3 }", json, error), error) == std::string("configuration value for key \"luau\" must be a table")); + { + INFO("a function where a value goes"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { aliases = { x = print } } }", json, error)); + REQUIRE(error.find("must be strings, booleans") != std::string::npos); + } // A .luaurc holds no numbers, and Luau's reading of one would stop // at it saying nothing of where: refused here, by its key. - ensure_equals("a number, by its key", (ALLuauConfigScript::asLuaurc("return { luau = { lint = { LocalUnused = 0 } } }", json, error), error), + UNSCOPED_INFO("a number, by its key"); + CHECK((ALLuauConfigScript::asLuaurc("return { luau = { lint = { LocalUnused = 0 } } }", json, error), error) == std::string("configuration value for key \"LocalUnused\" must be a string or a boolean, not a number")); - ensure("in an array, by its place", !ALLuauConfigScript::asLuaurc("return { luau = { globals = { 'a', 2 } } }", json, error) && - error.find("\"globals[2]\"") != std::string::npos); - ensure("at the top", !ALLuauConfigScript::asLuaurc("return { luau = { linterrors = 1 } }", json, error) && - error.find("\"linterrors\"") != std::string::npos); - ensure("an array with a hole", !ALLuauConfigScript::asLuaurc("return { luau = { globals = { [1] = 'a', [3] = 'b' } } }", json, error) && - error.find("invalid numeric key") != std::string::npos); - ensure("a table in itself, nesting forever", !ALLuauConfigScript::asLuaurc("local t = {} t.t = t return { luau = { aliases = t } }", json, error) && - error.find("nest too deeply") != std::string::npos); + { + INFO("in an array, by its place"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { globals = { 'a', 2 } } }", json, error)); + REQUIRE(error.find("\"globals[2]\"") != std::string::npos); + } + { + INFO("at the top"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { linterrors = 1 } }", json, error)); + REQUIRE(error.find("\"linterrors\"") != std::string::npos); + } + { + INFO("an array with a hole"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { globals = { [1] = 'a', [3] = 'b' } } }", json, error)); + REQUIRE(error.find("invalid numeric key") != std::string::npos); + } + { + INFO("a table in itself, nesting forever"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("local t = {} t.t = t return { luau = { aliases = t } }", json, error)); + REQUIRE(error.find("nest too deeply") != std::string::npos); + } // No metamethod runs as what it returned is read. - ensure("an __index that would loop is never asked", - ALLuauConfigScript::asLuaurc("return setmetatable({}, { __index = function() while true do end end })", json, error) && json == "{}"); + { + INFO("an __index that would loop is never asked"); + CHECK(ALLuauConfigScript::asLuaurc("return setmetatable({}, { __index = function() while true do end end })", json, error)); + CHECK(json == "{}"); + } // A run stopped by the deadline is kept a while, by its text: a text // of this run's own is run, not answered from what another test, or // this one before, asked. const std::string forever = "while true do end -- " + LLUUID::generateNewID().asString(); const auto started = std::chrono::steady_clock::now(); - ensure_equals("a loop that never ends, stopped", (ALLuauConfigScript::asLuaurc(forever, json, error), error), - std::string("configuration execution timed out")); + UNSCOPED_INFO("a loop that never ends, stopped"); + CHECK((ALLuauConfigScript::asLuaurc(forever, json, error), error) == std::string("configuration execution timed out")); const F64 took = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); - ensure(llformat("stopped at its time, not long after: %.2f s", took), took >= ALLuauConfigScript::MOST_SECONDS && took < ALLuauConfigScript::MOST_SECONDS + 2.0); - ensure("asked again at once, kept for a while: no second wait", (ALLuauConfigScript::asLuaurc(forever, json, error), - std::chrono::duration(std::chrono::steady_clock::now() - started).count() < took + 0.1)); + { + INFO("stopped at its time, not long after"); + CHECK(took >= ALLuauConfigScript::MOST_SECONDS); + CHECK(took < ALLuauConfigScript::MOST_SECONDS + 2.0); + } + UNSCOPED_INFO("asked again at once, kept for a while: no second wait"); + CHECK((ALLuauConfigScript::asLuaurc(forever, json, error), + std::chrono::duration(std::chrono::steady_clock::now() - started).count() < took + 0.1)); // table.move's copy is a loop in C the deadline does not reach, and // nils moved into a table that holds none take no memory: held to so // many elements, at once. const auto moving = std::chrono::steady_clock::now(); - ensure("two billion nils moved, refused at once: " + error, - !ALLuauConfigScript::asLuaurc("table.move({}, 1, 2147483646, 2) return {}", json, error) && error.find("table.move") != std::string::npos && - std::chrono::duration(std::chrono::steady_clock::now() - moving).count() < ALLuauConfigScript::MOST_SECONDS); - ensure("a move within it, as Luau's: " + error, - ALLuauConfigScript::asLuaurc("return { luau = { globals = table.move({ 'a', 'b' }, 1, 2, 2, { 'z' }) } }", json, error) && - json == "{\"globals\": [\"z\", \"a\", \"b\"]}"); - ensure("memory past the most, refused", - !ALLuauConfigScript::asLuaurc("local t = {} for i = 1, 1e7 do t[i] = string.rep('x', 1000) .. i end return t", json, error) && - error.find("memory") != std::string::npos); - ensure("one string past it at once, refused", !ALLuauConfigScript::asLuaurc("return { luau = { x = string.rep('x', 64 * 1024 * 1024) } }", json, error) && - error.find("memory") != std::string::npos); + { + INFO("two billion nils moved, refused at once"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("table.move({}, 1, 2147483646, 2) return {}", json, error)); + REQUIRE(error.find("table.move") != std::string::npos); + CHECK(std::chrono::duration(std::chrono::steady_clock::now() - moving).count() < ALLuauConfigScript::MOST_SECONDS); + } + { + INFO("a move within it, as Luau's"); + CHECK(ALLuauConfigScript::asLuaurc("return { luau = { globals = table.move({ 'a', 'b' }, 1, 2, 2, { 'z' }) } }", json, error)); + CHECK(json == "{\"globals\": [\"z\", \"a\", \"b\"]}"); + } + { + INFO("memory past the most, refused"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("local t = {} for i = 1, 1e7 do t[i] = string.rep('x', 1000) .. i end return t", json, error)); + REQUIRE(error.find("memory") != std::string::npos); + } + { + INFO("one string past it at once, refused"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc("return { luau = { x = string.rep('x', 64 * 1024 * 1024) } }", json, error)); + REQUIRE(error.find("memory") != std::string::npos); + } // Second Life's VM opens Eris as `ares`; a configuration is run // without it. The rest of Luau's libraries are there. - ensure("no ares: " + error, ALLuauConfigScript::asLuaurc("return { luau = { languagemode = ares and 'strict' or 'nonstrict' } }", json, error) && - json.find("nonstrict") != std::string::npos); - ensure("Luau's libraries: " + error, - ALLuauConfigScript::asLuaurc("return { luau = { globals = { string.upper('a'), tostring(math.max(1, 2)), table.concat({ 'x', 'y' }), buffer and 'b' or 'none' } } }", - json, error) && - json == "{\"globals\": [\"A\", \"2\", \"xy\", \"b\"]}"); + { + INFO("no ares"); + CHECK(ALLuauConfigScript::asLuaurc("return { luau = { languagemode = ares and 'strict' or 'nonstrict' } }", json, error)); + REQUIRE(json.find("nonstrict") != std::string::npos); + } + { + INFO("Luau's libraries"); + CHECK(ALLuauConfigScript::asLuaurc("return { luau = { globals = { string.upper('a'), tostring(math.max(1, 2)), table.concat({ 'x', 'y' }), buffer and 'b' or 'none' } } }", + json, error)); + CHECK(json == "{\"globals\": [\"A\", \"2\", \"xy\", \"b\"]}"); + } } - template<> template<> - void alluauconfig_object::test<10>() + TEST_CASE("alluauconfig: a .config.luau's run leaves no JSON on failure and no error on success", "[alluauconfig]") { - set_test_name("a .config.luau's run says only what it came to: no JSON with a failure, begun or not, no error with a success, run or kept"); const char* const failing = "return { luau = { languagemode = 'strict', lint = { LocalUnused = 0 } } }"; std::string json = "from before", error; - ensure("a failure part way through the writing", !ALLuauConfigScript::asLuaurc(failing, json, error) && !error.empty()); - ensure_equals("leaves no JSON", json, std::string()); + { + INFO("a failure part way through the writing"); + REQUIRE_FALSE(ALLuauConfigScript::asLuaurc(failing, json, error)); + REQUIRE_FALSE(error.empty()); + } + UNSCOPED_INFO("leaves no JSON"); + CHECK(json == std::string()); json = "from before"; - ensure("asked again, kept", !ALLuauConfigScript::asLuaurc(failing, json, error) && !error.empty()); - ensure_equals("and no JSON still", json, std::string()); + { + INFO("asked again, kept"); + CHECK_FALSE(ALLuauConfigScript::asLuaurc(failing, json, error)); + REQUIRE_FALSE(error.empty()); + } + UNSCOPED_INFO("and no JSON still"); + CHECK(json == std::string()); const char* const working = "return { luau = { languagemode = 'strict' } }"; error = "from before"; - ensure("a success: " + json, ALLuauConfigScript::asLuaurc(working, json, error) && json == "{\"languageMode\": \"strict\"}"); - ensure_equals("leaves no error", error, std::string()); + { + INFO("a success"); + CHECK(ALLuauConfigScript::asLuaurc(working, json, error)); + CHECK(json == "{\"languageMode\": \"strict\"}"); + } + UNSCOPED_INFO("leaves no error"); + CHECK(error == std::string()); error = "from before"; - ensure("asked again, kept", ALLuauConfigScript::asLuaurc(working, json, error) && json == "{\"languageMode\": \"strict\"}"); - ensure_equals("and no error still", error, std::string()); + { + INFO("asked again, kept"); + CHECK(ALLuauConfigScript::asLuaurc(working, json, error)); + CHECK(json == "{\"languageMode\": \"strict\"}"); + } + UNSCOPED_INFO("and no error still"); + CHECK(error == std::string()); } } diff --git a/indra/alscript/tests/alluaufragment_test.cpp b/indra/alscript/tests/alluaufragment_test.cpp index e20d55976d..ffec37d75f 100644 --- a/indra/alscript/tests/alluaufragment_test.cpp +++ b/indra/alscript/tests/alluaufragment_test.cpp @@ -29,7 +29,7 @@ #include "../lsl/allsltoslua.h" #include "../preprocessor/alpreprocessor.h" -#include "../test/lltut.h" +#include #include "llsdserialize.h" #include "Luau/Allocator.h" @@ -55,7 +55,7 @@ // warm job (ALLuauService::warm). Where the fragment declines, the whole // script answers both, and the two agree by construction; what is compared // is where it answers. -namespace tut +namespace { struct alluaufragment_data { @@ -495,44 +495,46 @@ namespace tut } } - void report(const char* corpus, const Tally& tally, bool answers = true) + void require_answers_as_whole(const char* corpus, const Tally& tally, bool answers = true) { std::printf("alluaufragment, %s solver, %s: %zu places, %zu answered over a fragment; told apart by a type beside a name %zu, " "by which fit %zu, by a few names %zu, by a signature's parameters %zu\n", newSolver ? "new" : "old", corpus, tally.asked, tally.answered, tally.types, tally.fits, tally.few, tally.generics); - ensure(std::string(corpus) + ": the fragment answers as the whole script does:\n" + tally.faults, tally.faults.empty()); + CAPTURE(corpus); + UNSCOPED_INFO("the fragment answers as the whole script does:\n" + tally.faults); + REQUIRE(tally.faults.empty()); if (answers) { - ensure(std::string(corpus) + ": most places answered over a fragment", tally.answered * 2 > tally.asked); + UNSCOPED_INFO("most places answered over a fragment"); + REQUIRE(tally.answered * 2 > tally.asked); } } }; - typedef test_group alluaufragment_group; - typedef alluaufragment_group::object alluaufragment_object; - alluaufragment_group alluaufragment_instance("alluaufragment"); - - template<> template<> - void alluaufragment_object::test<1>() + TEST_CASE_METHOD(alluaufragment_data, "alluaufragment: over the studio's templates the fragment answers as the whole script does", "[alluaufragment]") { - set_test_name("over the studio's templates, the fragment answers as the whole script does"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); llifstream in(std::string(AL_SCRIPT_TEMPLATES_DIR) + "/slua.xml", std::ios::in | std::ios::binary); LLSD templates; - ensure("read", in.is_open() && LLSDSerialize::fromXML(templates, in) != LLSDParser::PARSE_FAILURE && templates.isArray()); + { + INFO("read"); + CHECK(in.is_open()); + CHECK(LLSDSerialize::fromXML(templates, in) != LLSDParser::PARSE_FAILURE); + REQUIRE(templates.isArray()); + } Tally tally; for (LLSD::array_const_iterator it = templates.beginArray(); it != templates.endArray(); ++it) { ask((*it)["name"].asString(), probesOf((*it)["body"].asString(), 12), tally); } - report("the templates", tally); + require_answers_as_whole("the templates", tally); } - template<> template<> - void alluaufragment_object::test<2>() + TEST_CASE_METHOD(alluaufragment_data, "alluaufragment: over Tailslide's scripts as SLua the fragment answers as the whole does", "[alluaufragment]") { - set_test_name("over Tailslide's test scripts written again as SLua, the fragment answers as the whole script does"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string dir = std::string(AL_ALSCRIPT_TEST_DIR) + "/tailslide/scripts"; std::vector files; for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) @@ -543,11 +545,13 @@ namespace tut } } std::sort(files.begin(), files.end()); - ensure("the scripts: " + dir, files.size() > 100); + UNSCOPED_INFO("the scripts: " + dir); + REQUIRE(files.size() > 100); // The converter reads Tailslide's table of LSL's builtins, which the // LSL service fills for the whole process. ALLSLService lsl; - ensure("builtins loaded: " + error, lsl.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error)); + UNSCOPED_INFO("builtins loaded: " + error); + CHECK(lsl.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error)); Tally tally; for (const std::string& name : files) { @@ -557,14 +561,13 @@ namespace tut ask(name, probesOf(converted.text, 6), tally); } } - report("Tailslide's scripts", tally); + require_answers_as_whole("Tailslide's scripts", tally); } - template<> template<> - void alluaufragment_object::test<3>() + TEST_CASE_METHOD(alluaufragment_data, "alluaufragment: over an expanded script with modules the fragment answers as the whole does", "[alluaufragment]") { - set_test_name("over a script the preprocessor expanded, its requires apart as the studio checks them, the fragment answers as the whole script does, a module changed since the last check or not"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); namespace fs = std::filesystem; const fs::path dir = fs::path(AL_ALSCRIPT_TEST_DIR) / "preprocessor"; ALPreprocessor::Options options; @@ -584,7 +587,11 @@ namespace tut return ALPreprocessor::Found::Yes; }; const ALPreprocessor::Result made = ALPreprocessor::run(readFile((dir / "test_require.luau").string()), options); - ensure("expanded", made.problems.empty() && !made.apart.script.text.empty()); + { + INFO("expanded"); + REQUIRE(made.problems.empty()); + REQUIRE_FALSE(made.apart.script.text.empty()); + } ALLuauService::Modules modules; for (const ALPreprocessor::Result::Piece& piece : made.apart.modules) { @@ -594,12 +601,12 @@ namespace tut { modules.reaches.push_back({ resolved.from, resolved.name, resolved.path }); } - ensure("its modules", modules.modules.size() == 2); + REQUIRE(modules.modules.size() == 2); fragment.setModules(modules); whole.setModules(modules); Tally tally; ask("test_require", probesOf(made.apart.script.text, 12), tally); - report("an expanded script", tally); + require_answers_as_whole("an expanded script", tally); // A module changed since the last check: util, which had a function // since taken out. ALLuauService::Modules before = modules; @@ -620,17 +627,17 @@ namespace tut fragment.setModules(modules); ask("test_require, util changed", { probe }, changed, /*settled*/ true); } - report("an expanded script, a module changed since", changed, false); - ensure_equals("a module changed since: every place asked of the whole script", changed.answered, size_t(0)); + require_answers_as_whole("an expanded script, a module changed since", changed, false); + UNSCOPED_INFO("a module changed since: every place asked of the whole script"); + CHECK(changed.answered == size_t(0)); fragment.setModules({}); whole.setModules({}); } - template<> template<> - void alluaufragment_object::test<4>() + TEST_CASE_METHOD(alluaufragment_data, "alluaufragment: a line opened above the statement typed makes the whole script answer", "[alluaufragment]") { - set_test_name("a line opened above the statement being typed since the last check: Luau would start the fragment where the text has no such place, and the whole script answers instead"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); // Return pressed at the start of `print(a)`, then `.` typed in it // before another check: the last check's statement began at the // fifth column of a line that is empty now. @@ -642,15 +649,16 @@ namespace tut fragment.warm(base); const size_t before = fragment.fragmentsChecked(); const Told told = judged(fragment.complete(text, 3, 12), whole.complete(text, 3, 12)); - ensure_equals("asked of the whole script", fragment.fragmentsChecked(), before); - ensure("and answered as it is:" + told.faults, told.faults.empty()); + UNSCOPED_INFO("asked of the whole script"); + CHECK(fragment.fragmentsChecked() == before); + UNSCOPED_INFO("and answered as it is:" + told.faults); + REQUIRE(told.faults.empty()); } - template<> template<> - void alluaufragment_object::test<5>() + TEST_CASE_METHOD(alluaufragment_data, "alluaufragment: a handler stub is offered at an overloaded callee's argument but not within it", "[alluaufragment]") { - set_test_name("an overloaded callee's handler is offered written out where its argument is being written, and not in a field of one, a table in one or a type asserted of one: over a fragment as over the whole script"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const auto stubbed = [](const std::vector& found) { return std::any_of(found.begin(), found.end(), [](const ALScriptCompletion& c) { return !c.snippet.empty(); }); }; @@ -677,16 +685,18 @@ namespace tut const std::string text = base + place.line + "\n"; fragment.check(base); fragment.warm(base); - ensure_equals(std::string(place.line) + ": over a fragment", stubbed(fragment.complete(text, 1, place.column)), place.offered); - ensure_equals(std::string(place.line) + ": over the whole script", stubbed(whole.complete(text, 1, place.column)), place.offered); + CAPTURE(place.line); + UNSCOPED_INFO("over a fragment"); + REQUIRE(stubbed(fragment.complete(text, 1, place.column)) == place.offered); + UNSCOPED_INFO("over the whole script"); + REQUIRE(stubbed(whole.complete(text, 1, place.column)) == place.offered); } } - template<> template<> - void alluaufragment_object::test<6>() + TEST_CASE_METHOD(alluaufragment_data, "alluaufragment: a local declared again in one scope is the one in effect over a fragment", "[alluaufragment]") { - set_test_name("a local declared again in its own scope is the declaration in effect, offered and called, over a fragment as over the whole script, wherever the parser put each"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); // `test` a function, then a number in the same scope, and a line // typed below since the last check: the name alone, and a third // `test` initialized with it. Luau keeps both declarations among the @@ -736,8 +746,9 @@ namespace tut ask(above + place.checked, place.typed, [&](ALLuauService& service, const std::string& text, S32 line, const char* over) { const std::vector found = service.complete(text, line, place.column); const auto test = std::find_if(found.begin(), found.end(), [](const ALScriptCompletion& c) { return c.text == "test"; }); - ensure(llformat("round %d, %s, over %s: the number", round, place.typed, over), - test != found.end() && test->kind == ALScriptSymbolKind::Variable); + CAPTURE(round, place.typed, over); + REQUIRE(test != found.end()); + REQUIRE(test->kind == ALScriptSymbolKind::Variable); }); } // A function of two strings declared over one of a number: a @@ -745,10 +756,12 @@ namespace tut ask(above + "local function call(n: number)\nend\nlocal call = function(s: string, t: string)\nend\n", "call(\"x\", \"y\")", [&](ALLuauService& service, const std::string& text, S32 line, const char* over) { const ALScriptSignature found = service.signature(text, line, 5); - ensure(llformat("round %d, a call, over %s: the two strings, not %s", round, over, found.label.c_str()), - found.found && found.parameters.size() == 2); + CAPTURE(round, over, found.label); + REQUIRE(found.found); + REQUIRE(found.parameters.size() == 2); }); } - ensure("answered over a fragment", answered * 2 > asked); + UNSCOPED_INFO("answered over a fragment"); + CHECK(answered * 2 > asked); } } diff --git a/indra/alscript/tests/alluaunavigation_test.cpp b/indra/alscript/tests/alluaunavigation_test.cpp index 2b79ceef10..8857ddf2d3 100644 --- a/indra/alscript/tests/alluaunavigation_test.cpp +++ b/indra/alscript/tests/alluaunavigation_test.cpp @@ -27,7 +27,7 @@ #include "../luau/alluauservice.h" #include "../luau/alluauconfig.h" -#include "../test/lltut.h" +#include #include @@ -94,7 +94,7 @@ namespace } } -namespace tut +namespace { struct alluaunavigation_data { @@ -133,123 +133,158 @@ namespace tut } }; - typedef test_group alluaunavigation_group; - typedef alluaunavigation_group::object alluaunavigation_object; - alluaunavigation_group alluaunavigation_instance("alluaunavigation"); - - template<> template<> - void alluaunavigation_object::test<1>() + TEST_CASE_METHOD(alluaunavigation_data, "alluaunavigation: hover and references place a module's field in the module's own lines", "[alluaunavigation]") { - set_test_name("a field a module declares is declared there: hover and references say the module's key and its place in the module's lines"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); open("a", true); ALScriptReferences refs = service.references(SCRIPT_A, 5, 12); // `twice` in `util.twice(2)` - ensure("found", refs.found && refs.name == "twice"); - ensure(llformat("a function, not kind %d", (int)refs.kind), refs.kind == ALScriptSymbolKind::Function); - ensure_equals("declared in the module", refs.definitionFile, std::string(UTIL)); - ensure_equals("at `function M.twice`", where(refs.definition), std::string("3:11")); - ensure("the module's to rename", refs.hasDefinition && refs.renamable); + REQUIRE(refs.found); + CHECK(refs.name == "twice"); + CHECK(refs.kind == ALScriptSymbolKind::Function); + UNSCOPED_INFO("declared in the module"); + CHECK(refs.definitionFile == std::string(UTIL)); + UNSCOPED_INFO("at `function M.twice`"); + CHECK(where(refs.definition) == std::string("3:11")); + { + INFO("the module's to rename"); + CHECK(refs.hasDefinition); + CHECK(refs.renamable); + } const ALScriptHover hover = service.hover(SCRIPT_A, 5, 12); - ensure("hover: the module's", hover.found && hover.hasDefinition && hover.definitionFile == UTIL); - ensure_equals("hover: the same place", llformat("%d:%d", hover.definitionLine, hover.definitionColumn), std::string("3:11")); + { + INFO("hover: the module's"); + REQUIRE(hover.found); + CHECK(hover.hasDefinition); + CHECK(hover.definitionFile == UTIL); + } + UNSCOPED_INFO("hover: the same place"); + CHECK(llformat("%d:%d", hover.definitionLine, hover.definitionColumn) == std::string("3:11")); refs = service.references(SCRIPT_A, 6, 12); // `limit`, assigned - ensure_equals("a field assigned: declared where it is assigned", refs.definitionFile + "@" + where(refs.definition), - std::string(UTIL) + "@6:2"); + UNSCOPED_INFO("a field assigned: declared where it is assigned"); + CHECK(refs.definitionFile + "@" + where(refs.definition) == std::string(UTIL) + "@6:2"); refs = service.references(SCRIPT_A, 5, 29); // `double`, a module that requires the other - ensure_equals("each module its own key", refs.definitionFile + "@" + where(refs.definition), std::string(SHAPES) + "@2:11"); + UNSCOPED_INFO("each module its own key"); + CHECK(refs.definitionFile + "@" + where(refs.definition) == std::string(SHAPES) + "@2:11"); const ALScriptHover local = service.hover(SCRIPT_A, 5, 7); // `util` itself - ensure("a local the script binds: the script's", local.hasDefinition && local.definitionFile.empty() && local.definitionLine == 1); + { + INFO("a local the script binds: the script's"); + CHECK(local.hasDefinition); + REQUIRE(local.definitionFile.empty()); + CHECK(local.definitionLine == 1); + } const ALScriptHover say = service.hover("ll.Say(0, \"x\")\n", 0, 4); - ensure("what the definitions declare: nowhere to go", say.found && !say.hasDefinition); + { + INFO("what the definitions declare: nowhere to go"); + REQUIRE(say.found); + CHECK_FALSE(say.hasDefinition); + } service.setDocument(""); } - template<> template<> - void alluaunavigation_object::test<2>() + TEST_CASE_METHOD(alluaunavigation_data, "alluaunavigation: a module's exported type is found through its local and declared there", "[alluaunavigation]") { - set_test_name("a type a module exports is declared there, found through the local the module was required as; its uses in the module too"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); open("a", true); const ALScriptReferences refs = service.references(SCRIPT_A, 4, 15); // `Point` in `util.Point` - ensure("found", refs.found && refs.name == "Point"); - ensure("a type", refs.kind == ALScriptSymbolKind::Type); - ensure_equals("declared in the module", refs.definitionFile + "@" + where(refs.definition), std::string(UTIL) + "@2:12"); - ensure_equals("each place: the script's, the module's", placesOf(refs), std::string("4:14 ") + UTIL + "@2:12 " + UTIL + "@7:21"); - ensure("renamable", refs.renamable); + REQUIRE(refs.found); + CHECK(refs.name == "Point"); + CHECK(refs.kind == ALScriptSymbolKind::Type); + UNSCOPED_INFO("declared in the module"); + CHECK(refs.definitionFile + "@" + where(refs.definition) == std::string(UTIL) + "@2:12"); + UNSCOPED_INFO("each place: the script's, the module's"); + CHECK(placesOf(refs) == std::string("4:14 ") + UTIL + "@2:12 " + UTIL + "@7:21"); + CHECK(refs.renamable); // The local the module was required as is named in a type's prefix // too, which a rename of it renames. const ALScriptReferences local = service.references(SCRIPT_A, 1, 7); - ensure_equals("the local, the prefix among its places", placesOf(local), std::string("1:6 4:9 4:22 5:6 6:6")); + UNSCOPED_INFO("the local, the prefix among its places"); + CHECK(placesOf(local) == std::string("1:6 4:9 4:22 5:6 6:6")); service.setDocument(""); } - template<> template<> - void alluaunavigation_object::test<3>() + TEST_CASE_METHOD(alluaunavigation_data, "alluaunavigation: a module's field is found everywhere it stands but not another table's", "[alluaunavigation]") { - set_test_name("a module's field is found wherever it stands, in the script and every module it requires; a field of the same name on another table is left alone"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); open("a", true); ALScriptReferences refs = service.references(SCRIPT_A, 5, 12); - ensure_equals("the script's, the module's own, and the other module's", placesOf(refs), - std::string("5:11 ") + UTIL + "@3:11 " + SHAPES + "@3:16"); + UNSCOPED_INFO("the script's, the module's own, and the other module's"); + CHECK(placesOf(refs) == std::string("5:11 ") + UTIL + "@3:11 " + SHAPES + "@3:16"); refs = service.references(SCRIPT_A, 6, 12); - ensure_equals("an assigned one the same", placesOf(refs), std::string("6:11 ") + UTIL + "@6:2 " + SHAPES + "@3:32"); + UNSCOPED_INFO("an assigned one the same"); + CHECK(placesOf(refs) == std::string("6:11 ") + UTIL + "@6:2 " + SHAPES + "@3:32"); // The script's own table's field of the same name: the script's alone. refs = service.references(SCRIPT_A, 5, 46); - ensure("found", refs.found && refs.name == "twice" && refs.definitionFile.empty()); - ensure_equals("declared in the script's constructor", where(refs.definition), std::string("3:16")); - ensure_equals("and used once; none of the module's", placesOf(refs), std::string("3:16 5:45")); + REQUIRE(refs.found); + CHECK(refs.name == "twice"); + REQUIRE(refs.definitionFile.empty()); + UNSCOPED_INFO("declared in the script's constructor"); + CHECK(where(refs.definition) == std::string("3:16")); + UNSCOPED_INFO("and used once; none of the module's"); + CHECK(placesOf(refs) == std::string("3:16 5:45")); // Asked at the declaration in the constructor: the same. - ensure_equals("from the declaration", placesOf(service.references(SCRIPT_A, 3, 17)), std::string("3:16 5:45")); + UNSCOPED_INFO("from the declaration"); + CHECK(placesOf(service.references(SCRIPT_A, 3, 17)) == std::string("3:16 5:45")); service.setDocument(""); } - template<> template<> - void alluaunavigation_object::test<4>() + TEST_CASE_METHOD(alluaunavigation_data, "alluaunavigation: scripts sharing a module find their places and the module's from either end", "[alluaunavigation]") { - set_test_name("two scripts requiring one module: each finds its own places and the module's; and asked at the module's declaration, as a lookup through another script asks, the script's places through the require"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); open("a", true); const ALScriptReferences a = service.references(SCRIPT_A, 5, 12); open("b", false); const ALScriptReferences b = service.references(SCRIPT_B, 1, 12); - ensure_equals("the other script's: its own and the module's", placesOf(b), std::string("1:11 ") + UTIL + "@3:11"); - ensure_equals("the same declaration", b.definitionFile + "@" + where(b.definition), a.definitionFile + "@" + where(a.definition)); + UNSCOPED_INFO("the other script's: its own and the module's"); + CHECK(placesOf(b) == std::string("1:11 ") + UTIL + "@3:11"); + UNSCOPED_INFO("the same declaration"); + CHECK(b.definitionFile + "@" + where(b.definition) == a.definitionFile + "@" + where(a.definition)); // What the lookup across scripts asks of each: the script read // apart with its modules, as the checker reads it, asked at the // module's declaration in the module's own lines. open("lookup", false); const ALScriptReferences from_module = service.references(SCRIPT_B, 3, 12, UTIL); // `twice` in `function M.twice` - ensure("found from the module", from_module.found && from_module.name == "twice" && from_module.definitionFile == UTIL); - ensure_equals("the script's use, through the require, and the module's declaration", placesOf(from_module), - std::string("1:11 ") + UTIL + "@3:11"); + { + INFO("found from the module"); + REQUIRE(from_module.found); + CHECK(from_module.name == "twice"); + CHECK(from_module.definitionFile == UTIL); + } + UNSCOPED_INFO("the script's use, through the require, and the module's declaration"); + CHECK(placesOf(from_module) == std::string("1:11 ") + UTIL + "@3:11"); // A module's local is its own alone. const ALScriptReferences local = service.references(SCRIPT_B, 1, 6, UTIL); // `M` in `local M = {}` - ensure("found", local.found && local.name == "M"); - ensure_equals("the module's places alone", placesOf(local), - std::string(UTIL) + "@1:6 " + UTIL + "@3:9 " + UTIL + "@6:0 " + UTIL + "@7:9 " + UTIL + "@10:7"); + REQUIRE(local.found); + CHECK(local.name == "M"); + UNSCOPED_INFO("the module's places alone"); + CHECK(placesOf(local) == std::string(UTIL) + "@1:6 " + UTIL + "@3:9 " + UTIL + "@6:0 " + UTIL + "@7:9 " + UTIL + "@10:7"); service.setDocument(""); } - template<> template<> - void alluaunavigation_object::test<5>() + TEST_CASE_METHOD(alluaunavigation_data, "alluaunavigation: hover on a type's name says it as declared and where it was declared", "[alluaunavigation]") { - set_test_name("a hover on a type's name says the type as its declaration reads, and where it was declared: a module's through its local, the script's own, a generic one, a built-in one"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); open("a", true); ALScriptHover hover = service.hover(SCRIPT_A, 4, 15); // `Point` in `util.Point` - ensure("found", hover.found); - ensure("the module's, by its name and fields: " + hover.label, - hover.label.rfind("type util.Point = ", 0) == 0 && hover.label.find("x: number") != std::string::npos); - ensure_equals("declared in the module", hover.definitionFile + "@" + llformat("%d:%d", hover.definitionLine, hover.definitionColumn), - std::string(UTIL) + "@2:12"); + REQUIRE(hover.found); + { + INFO("the module's, by its name and fields: " + hover.label); + CHECK(hover.label.rfind("type util.Point = ", 0) == 0); + REQUIRE(hover.label.find("x: number") != std::string::npos); + } + UNSCOPED_INFO("declared in the module"); + CHECK(hover.definitionFile + "@" + llformat("%d:%d", hover.definitionLine, hover.definitionColumn) == std::string(UTIL) + "@2:12"); const std::string own = "--!strict\n" // 0 "type Pair = { first: T, second: T }\n" // 1 @@ -257,45 +292,61 @@ namespace tut "local n: number = p.first\n"; // 3 open("own", false); hover = service.hover(own, 2, 10); // `Pair` - ensure("the script's own, generic: " + hover.label, hover.found && hover.label.rfind("type Pair = ", 0) == 0 && - hover.label.find("first: T") != std::string::npos); - ensure("declared in the script", hover.hasDefinition && hover.definitionFile.empty() && hover.definitionLine == 1); + { + INFO("the script's own, generic: " + hover.label); + REQUIRE(hover.found); + CHECK(hover.label.rfind("type Pair = ", 0) == 0); + REQUIRE(hover.label.find("first: T") != std::string::npos); + } + { + INFO("declared in the script"); + CHECK(hover.hasDefinition); + REQUIRE(hover.definitionFile.empty()); + CHECK(hover.definitionLine == 1); + } hover = service.hover(own, 3, 10); // `number` - ensure_equals("a built-in one, only itself", hover.label, std::string("type number")); + UNSCOPED_INFO("a built-in one, only itself"); + CHECK(hover.label == std::string("type number")); hover = service.hover(own, 3, 6); // `n`, the local, as ever - ensure("a local still a local: " + hover.label, hover.label.rfind("local n", 0) == 0); + UNSCOPED_INFO("a local still a local: " + hover.label); + CHECK(hover.label.rfind("local n", 0) == 0); service.setDocument(""); } - template<> template<> - void alluaunavigation_object::test<6>() + TEST_CASE_METHOD(alluaunavigation_data, "alluaunavigation: a field is found by a string key in brackets too with its quotes left out", "[alluaunavigation]") { - set_test_name("a field is found by a string in brackets too -- an index, a constructor's key -- each place its name alone, quotes aside, which is what a rename replaces"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string text = "--!strict\n" // 0 "local t = { size = 1, [\"name\"] = \"a\" }\n" // 1 "t[\"size\"] = t.size + 1\n" // 2 "print(t.name, t[\"name\"], t['size'])\n"; // 3 open("keys", false); ALScriptReferences refs = service.references(text, 2, 4); // `size` in `t["size"]` - ensure("found", refs.found && refs.name == "size"); - ensure_equals("each place: the record's key, the strings' insides, the name", placesOf(refs), std::string("1:12 2:3 2:14 3:28")); - ensure_equals("declared by the record", where(refs.definition), std::string("1:12")); - ensure("renamable", refs.hasDefinition && refs.renamable); + REQUIRE(refs.found); + CHECK(refs.name == "size"); + UNSCOPED_INFO("each place: the record's key, the strings' insides, the name"); + CHECK(placesOf(refs) == std::string("1:12 2:3 2:14 3:28")); + UNSCOPED_INFO("declared by the record"); + CHECK(where(refs.definition) == std::string("1:12")); + CHECK(refs.hasDefinition); + CHECK(refs.renamable); refs = service.references(text, 3, 9); // `name` in `t.name` - ensure_equals("a key in brackets declares it", placesOf(refs), std::string("1:24 3:8 3:17")); - ensure_equals("there, inside its quotes", where(refs.definition), std::string("1:24")); - ensure("renamable", refs.renamable); - ensure_equals("asked at that key: the same", placesOf(service.references(text, 1, 25)), std::string("1:24 3:8 3:17")); + UNSCOPED_INFO("a key in brackets declares it"); + CHECK(placesOf(refs) == std::string("1:24 3:8 3:17")); + UNSCOPED_INFO("there, inside its quotes"); + CHECK(where(refs.definition) == std::string("1:24")); + CHECK(refs.renamable); + UNSCOPED_INFO("asked at that key: the same"); + CHECK(placesOf(service.references(text, 1, 25)) == std::string("1:24 3:8 3:17")); service.setDocument(""); } - template<> template<> - void alluaunavigation_object::test<7>() + TEST_CASE_METHOD(alluaunavigation_data, "alluaunavigation: a method is its class's through __index; a function __index stops a rename", "[alluaunavigation]") { - set_test_name("an object's method is its class's, through its metatable's __index, whichever end it is asked at; one reached some way not followed leaves the field not to rename"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string account = "--!strict\n" // 0 "local Account = {}\n" // 1 "Account.__index = Account\n" // 2 @@ -309,12 +360,17 @@ namespace tut "print(a:deposit(5))\n"; // 10 open("account", false); ALScriptReferences refs = service.references(account, 6, 18); // `deposit` in `function Account:deposit` - ensure("found", refs.found && refs.name == "deposit"); - ensure_equals("the declaration and the object's call", placesOf(refs), std::string("6:17 10:8")); - ensure("renamable", refs.hasDefinition && refs.renamable); + REQUIRE(refs.found); + CHECK(refs.name == "deposit"); + UNSCOPED_INFO("the declaration and the object's call"); + CHECK(placesOf(refs) == std::string("6:17 10:8")); + CHECK(refs.hasDefinition); + CHECK(refs.renamable); refs = service.references(account, 10, 9); // `deposit` in `a:deposit(5)` - ensure_equals("from the call: the same", placesOf(refs), std::string("6:17 10:8")); - ensure_equals("declared in the class", where(refs.definition), std::string("6:17")); + UNSCOPED_INFO("from the call: the same"); + CHECK(placesOf(refs) == std::string("6:17 10:8")); + UNSCOPED_INFO("declared in the class"); + CHECK(where(refs.definition) == std::string("6:17")); const std::string proxy = "--!strict\n" // 0 "local Base = {}\n" // 1 @@ -325,8 +381,10 @@ namespace tut "print(Base.greet(), o.greet)\n"; // 6 open("proxy", false); refs = service.references(proxy, 2, 15); // `greet` in `function Base.greet` - ensure("found, and declared", refs.found && refs.hasDefinition); - ensure("an object's field of the name, its __index a function: not to rename", !refs.renamable); + REQUIRE(refs.found); + CHECK(refs.hasDefinition); + UNSCOPED_INFO("an object's field of the name, its __index a function: not to rename"); + CHECK_FALSE(refs.renamable); service.setDocument(""); } } diff --git a/indra/alscript/tests/alluauservice_test.cpp b/indra/alscript/tests/alluauservice_test.cpp index f922823c77..789220f451 100644 --- a/indra/alscript/tests/alluauservice_test.cpp +++ b/indra/alscript/tests/alluauservice_test.cpp @@ -32,7 +32,7 @@ #include "../lint/alselenefilters.h" #include "../core/alscriptlexicon.h" -#include "../test/lltut.h" +#include #include "llsdserialize.h" #include @@ -40,7 +40,7 @@ #include #include -namespace tut +namespace { struct alluauservice_data { @@ -102,63 +102,61 @@ namespace tut } }; - typedef test_group alluauservice_group; - typedef alluauservice_group::object alluauservice_object; - alluauservice_group alluauservice_instance("alluauservice"); - - template<> template<> - void alluauservice_object::test<1>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the grid's definitions load", "[alluauservice]") { - set_test_name("the grid's definitions load"); - ensure("definitions file read", !definitions.empty()); - ensure("definitions load: " + error, loaded); - ensure("service says so", service.hasDefinitions()); + UNSCOPED_INFO("definitions file read"); + REQUIRE_FALSE(definitions.empty()); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); + CHECK(service.hasDefinitions()); } - template<> template<> - void alluauservice_object::test<2>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a script using ll and the SL types checks clean", "[alluauservice]") { - set_test_name("a script using ll and the SL types checks clean"); - ensure("definitions load: " + error, loaded); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check( "local owner: uuid = ll.GetOwner()\n" "ll.Say(0, tostring(owner))\n" "local here: vector = vector.create(1, 2, 3)\n" "ll.SetPos(here + vector.one)\n"); - ensure_equals("no errors: " + said(problems), errors(problems), 0); + UNSCOPED_INFO("no errors: " + said(problems)); + CHECK(errors(problems) == 0); } - template<> template<> - void alluauservice_object::test<3>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a wrong argument type is a type error", "[alluauservice]") { - set_test_name("a wrong argument type is a type error"); - ensure("definitions load: " + error, loaded); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check("ll.Say(\"zero\", 0)\n"); - ensure("an error: " + said(problems), errors(problems) > 0); - ensure("from the type checker", problems.front().source == ALScriptProblem::Source::Types); - ensure_equals("on the first line", problems.front().line, 0); + UNSCOPED_INFO("an error: " + said(problems)); + REQUIRE(errors(problems) > 0); + CHECK(problems.front().source == ALScriptProblem::Source::Types); + CHECK(problems.front().line == 0); } - template<> template<> - void alluauservice_object::test<4>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: an unknown global is reported by name", "[alluauservice]") { - set_test_name("an unknown global is reported by name"); - ensure("definitions load: " + error, loaded); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check("frobnicate(1)\n"); - ensure("an error: " + said(problems), errors(problems) > 0); - ensure("naming it: " + said(problems), mentions(problems, "frobnicate")); + UNSCOPED_INFO("an error: " + said(problems)); + CHECK(errors(problems) > 0); + UNSCOPED_INFO("naming it: " + said(problems)); + CHECK(mentions(problems, "frobnicate")); } - template<> template<> - void alluauservice_object::test<5>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a deprecated function is a lint; lints and unknown globals come keyed", "[alluauservice]") { - set_test_name("a deprecated function is a lint"); - ensure("definitions load: " + error, loaded); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); // ll.Cloud is one of the few functions the ll table itself marks // deprecated; the legacy names live in llcompat. ALScriptProblems problems = service.check("local density = ll.Cloud(vector.zero)\n"); - ensure("something said: " + said(problems), !problems.empty()); - ensure("deprecated: " + said(problems), mentions(problems, "deprecated")); + UNSCOPED_INFO("something said: " + said(problems)); + REQUIRE_FALSE(problems.empty()); + UNSCOPED_INFO("deprecated: " + said(problems)); + CHECK(mentions(problems, "deprecated")); // A lint the map knows is taken apart by its name, an unknown // global by its shape, for another language to say. problems = service.check("local unused = 1\nnope()\n"); @@ -176,47 +174,46 @@ namespace tut keyed_error = true; } } - ensure("the unused local's lint, keyed with its name: " + said(problems), keyed_lint); - ensure("the unknown global, keyed with its name: " + said(problems), keyed_error); + UNSCOPED_INFO("the unused local's lint, keyed with its name: " + said(problems)); + CHECK(keyed_lint); + UNSCOPED_INFO("the unknown global, keyed with its name: " + said(problems)); + CHECK(keyed_error); } - template<> template<> - void alluauservice_object::test<6>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a syntax error is the parser's", "[alluauservice]") { - set_test_name("a syntax error is the parser's"); - ensure("definitions load: " + error, loaded); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); ALScriptProblems problems = service.check("local x = \n"); - ensure("an error: " + said(problems), errors(problems) > 0); - ensure("from the parser", problems.front().source == ALScriptProblem::Source::Parser); + UNSCOPED_INFO("an error: " + said(problems)); + REQUIRE(errors(problems) > 0); + CHECK(problems.front().source == ALScriptProblem::Source::Parser); } - template<> template<> - void alluauservice_object::test<7>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: without the definitions ll is unknown", "[alluauservice]") { - set_test_name("without the definitions, ll is unknown"); ALLuauService bare; - ensure("nothing loaded", !bare.hasDefinitions()); + CHECK_FALSE(bare.hasDefinitions()); ALScriptProblems problems = bare.check("ll.Say(0, \"x\")\n"); - ensure("an error: " + said(problems), errors(problems) > 0); - ensure("naming ll: " + said(problems), mentions(problems, "ll")); + UNSCOPED_INFO("an error: " + said(problems)); + CHECK(errors(problems) > 0); + UNSCOPED_INFO("naming ll: " + said(problems)); + CHECK(mentions(problems, "ll")); } - template<> template<> - void alluauservice_object::test<8>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: bad definitions are refused with a reason", "[alluauservice]") { - set_test_name("bad definitions are refused with a reason"); ALLuauService bare; std::string why; - ensure("refused", !bare.loadDefinitions("declare ll: {\n", why)); - ensure("with a reason", !why.empty()); - ensure("and nothing loaded", !bare.hasDefinitions()); + REQUIRE_FALSE(bare.loadDefinitions("declare ll: {\n", why)); + REQUIRE_FALSE(why.empty()); + CHECK_FALSE(bare.hasDefinitions()); } - template<> template<> - void alluauservice_object::test<9>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: completion offers the fields of ll and the locals in scope", "[alluauservice]") { - set_test_name("completion offers the fields of ll and the locals in scope"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local count = 1\nll.Sa\n"; // At the end of `ll.Sa`. std::vector found = service.complete(script, 1, 5); @@ -226,13 +223,15 @@ namespace tut if (c.text == "Say") { say = true; - ensure("a function", c.kind == ALScriptSymbolKind::Function); - ensure("with a signature: " + c.detail, c.detail.find("(") != std::string::npos); + REQUIRE(c.kind == ALScriptSymbolKind::Function); + UNSCOPED_INFO("with a signature: " + c.detail); + REQUIRE(c.detail.find("(") != std::string::npos); } } std::string names = llformat("%d entries:", (int)found.size()); for (size_t i = 0; i < found.size() && i < 12; ++i) names += " " + found[i].text; - ensure("ll.Say is offered; " + names, say); + UNSCOPED_INFO("ll.Say is offered; " + names); + CHECK(say); // At the start of an empty statement: bindings and keywords. found = service.complete(script, 2, 0); bool local = false, keyword = false; @@ -241,91 +240,78 @@ namespace tut local = local || (c.text == "count" && c.kind == ALScriptSymbolKind::Variable && c.detail == "number"); keyword = keyword || (c.text == "local" && c.kind == ALScriptSymbolKind::Keyword); } - ensure("the local, typed", local); - ensure("a keyword", keyword); + UNSCOPED_INFO("the local, typed"); + CHECK(local); + CHECK(keyword); } - template<> template<> - void alluauservice_object::test<10>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: hover says what a name is with its docs and where a local was bound", "[alluauservice]") { - set_test_name("hover says what a name is, with its documentation, and where a local was bound"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); llifstream in(std::string(AL_LSL_DEFINITIONS_DIR) + "/secondlife.docs.json", std::ios::binary); std::stringstream json; json << in.rdbuf(); std::string docs_error; - ensure("docs loaded: " + docs_error, service.loadDocs(json.str(), docs_error)); + CHECK(service.loadDocs(json.str(), docs_error)); const std::string script = "local count = 1\nll.Say(0, tostring(count))\n"; ALScriptHover hover = service.hover(script, 1, 4); // on `Say` - ensure("found", hover.found); - ensure("the name and its type: " + hover.label, hover.label.find("Say") != std::string::npos && hover.label.find("(") != std::string::npos); - ensure("documented: " + hover.documentation, !hover.documentation.empty()); - ensure("not the script's own", !hover.hasDefinition); + REQUIRE(hover.found); + { + INFO("the name and its type: " + hover.label); + REQUIRE(hover.label.find("Say") != std::string::npos); + REQUIRE(hover.label.find("(") != std::string::npos); + } + REQUIRE_FALSE(hover.documentation.empty()); + UNSCOPED_INFO("not the script's own"); + CHECK_FALSE(hover.hasDefinition); hover = service.hover(script, 1, 20); // on `count` in the call - ensure("found the local", hover.found); - ensure("a number: " + hover.label, hover.label.find("number") != std::string::npos); - ensure("bound in the script", hover.hasDefinition && hover.definitionLine == 0); - ensure("nothing at nothing", !service.hover(script, 2, 0).found); + UNSCOPED_INFO("found the local"); + REQUIRE(hover.found); + UNSCOPED_INFO("a number: " + hover.label); + REQUIRE(hover.label.find("number") != std::string::npos); + { + INFO("bound in the script"); + CHECK(hover.hasDefinition); + CHECK(hover.definitionLine == 0); + } + UNSCOPED_INFO("nothing at nothing"); + REQUIRE_FALSE(service.hover(script, 2, 0).found); hover = service.hover(script, 1, 0); // on `ll` itself - ensure("the table is found", hover.found); - ensure(llformat("and said in a glance, not %d characters", (int)hover.label.size()), hover.label.size() < 1200); - ensure("with how many more there are: " + hover.label, hover.label.find("more") != std::string::npos); - ensure("and the whole of it apart, a field to a line", hover.typeDetail.size() > hover.label.size() && hover.typeDetail.find('\n') != std::string::npos); - } - - template<> template<> - void alluauservice_object::test<17>() - { - set_test_name("hover says what kind of name it is, a function by its signature, and what is wanted where a type is wrong"); - ensure("definitions loaded: " + error, loaded); - const std::string script = - "local function half(n: number): number\n" - " return n / 2\n" - "end\n" - "const limit = 3\n" - "local p = { a = 1 }\n" - "ll.Say(0, half(\"x\"))\n" - "print(limit, p.a)\n"; - ALScriptHover hover = service.hover(script, 0, 16); // half, declared - ensure("a function by its signature: " + hover.label, hover.label.rfind("function half(n: number): number", 0) == 0); - hover = service.hover(script, 0, 20); // n, the parameter - ensure("a parameter: " + hover.label, hover.label == "(parameter) n: number"); - hover = service.hover(script, 1, 11); // n, used - ensure("still a parameter where used: " + hover.label, hover.label == "(parameter) n: number"); - hover = service.hover(script, 3, 7); // limit - ensure("a const: " + hover.label, hover.label == "const limit: number"); - hover = service.hover(script, 4, 6); // p - ensure("a local: " + hover.label, hover.label.rfind("local p: ", 0) == 0); - hover = service.hover(script, 6, 15); // p.a - ensure("a field: " + hover.label, hover.label == "(field) p.a: number"); - hover = service.hover(script, 5, 16); // "x" where a number is wanted - ensure("what is wanted there: " + hover.expected, hover.expected == "number"); - hover = service.hover(script, 6, 0); // print - // The new solver gives print its generic pack: print. - ensure("a builtin by its signature: " + hover.label, hover.label.rfind(newSolver ? "function print<" : "function print(", 0) == 0); + UNSCOPED_INFO("the table is found"); + REQUIRE(hover.found); + UNSCOPED_INFO("and said in a glance"); + REQUIRE(hover.label.size() < 1200); + UNSCOPED_INFO("with how many more there are: " + hover.label); + REQUIRE(hover.label.find("more") != std::string::npos); + { + INFO("and the whole of it apart, a field to a line"); + REQUIRE(hover.typeDetail.size() > hover.label.size()); + REQUIRE(hover.typeDetail.find('\n') != std::string::npos); + } } - template<> template<> - void alluauservice_object::test<11>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a signature names the call the position is in and which parameter it is at", "[alluauservice]") { - set_test_name("a signature names the call the position is in and which parameter it is at"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "ll.Say(0, \"hi\")\n"; ALScriptSignature sig = service.signature(script, 0, 7); // in the first argument - ensure("found", sig.found); - ensure("the call: " + sig.label, sig.label.find("Say") != std::string::npos); - ensure_equals("two parameters", sig.parameters.size(), size_t(2)); - ensure_equals("at the first", sig.active, 0); + REQUIRE(sig.found); + UNSCOPED_INFO("the call: " + sig.label); + REQUIRE(sig.label.find("Say") != std::string::npos); + REQUIRE(sig.parameters.size() == size_t(2)); + CHECK(sig.active == 0); sig = service.signature(script, 0, 11); // in the second - ensure_equals("at the second", sig.active, 1); - ensure("none outside a call", !service.signature(script, 0, 0).found); + CHECK(sig.active == 1); + UNSCOPED_INFO("none outside a call"); + REQUIRE_FALSE(service.signature(script, 0, 0).found); } - template<> template<> - void alluauservice_object::test<12>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: references find a name's binding and uses and whether it can be renamed", "[alluauservice]") { - set_test_name("references find where a name is bound and every place it stands, and know what is the script's to rename"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local count = 1\n" "ll.Say(0, tostring(count))\n" @@ -334,38 +320,72 @@ namespace tut "function M.f(n: number) return n end\n" "M.f(count)\n"; ALScriptReferences refs = service.references(script, 1, 20); // on `count` in the call - ensure("the local is found", refs.found); - ensure_equals("by name", refs.name, std::string("count")); - ensure(llformat("a variable, not kind %d", (int)refs.kind), refs.kind == ALScriptSymbolKind::Variable); - ensure(llformat("bound on the first line, not %d:%d-%d", refs.definition.line, refs.definition.column, refs.definition.endColumn), - refs.hasDefinition && refs.definition.line == 0 && refs.definition.column == 6 && refs.definition.endColumn == 11); - ensure_equals("five places", refs.references.size(), size_t(5)); - ensure("the first is the binding", refs.references[0] == refs.definition); - ensure("the last is the argument", refs.references[4].line == 5 && refs.references[4].column == 4); - ensure("the script's to rename", refs.renamable); + UNSCOPED_INFO("the local is found"); + REQUIRE(refs.found); + CHECK(refs.name == std::string("count")); + CHECK(refs.kind == ALScriptSymbolKind::Variable); + { + INFO("bound on the first line"); + CHECK(refs.hasDefinition); + CHECK(refs.definition.line == 0); + CHECK(refs.definition.column == 6); + CHECK(refs.definition.endColumn == 11); + } + REQUIRE(refs.references.size() == size_t(5)); + UNSCOPED_INFO("the first is the binding"); + CHECK(refs.references[0] == refs.definition); + { + INFO("the last is the argument"); + CHECK(refs.references[4].line == 5); + CHECK(refs.references[4].column == 4); + } + UNSCOPED_INFO("the script's to rename"); + CHECK(refs.renamable); refs = service.references(script, 1, 4); // on `Say` - ensure("a field of ll is found", refs.found && refs.name == "Say"); - ensure("a function", refs.kind == ALScriptSymbolKind::Function); - ensure("not bound in the script", !refs.hasDefinition && !refs.renamable); - ensure_equals("used once", refs.references.size(), size_t(1)); + { + INFO("a field of ll is found"); + REQUIRE(refs.found); + CHECK(refs.name == "Say"); + } + CHECK(refs.kind == ALScriptSymbolKind::Function); + { + INFO("not bound in the script"); + CHECK_FALSE(refs.hasDefinition); + CHECK_FALSE(refs.renamable); + } + REQUIRE(refs.references.size() == size_t(1)); refs = service.references(script, 5, 2); // on `f` in `M.f(count)` - ensure("the script's own field is found", refs.found && refs.name == "f"); - ensure("bound by the function statement", refs.hasDefinition && refs.definition.line == 4 && refs.definition.column == 11); - ensure_equals("declared and called", refs.references.size(), size_t(2)); - ensure("and renamable", refs.renamable); + { + INFO("the script's own field is found"); + REQUIRE(refs.found); + CHECK(refs.name == "f"); + } + { + INFO("bound by the function statement"); + CHECK(refs.hasDefinition); + CHECK(refs.definition.line == 4); + CHECK(refs.definition.column == 11); + } + UNSCOPED_INFO("declared and called"); + REQUIRE(refs.references.size() == size_t(2)); + CHECK(refs.renamable); refs = service.references(script, 0, 6); // on the binding itself - ensure("asked at the binding, the same answer", refs.found && refs.references.size() == 5); - ensure("nothing at nothing", !service.references(script, 3, 0).found); + { + INFO("asked at the binding, the same answer"); + REQUIRE(refs.found); + REQUIRE(refs.references.size() == 5); + } + UNSCOPED_INFO("nothing at nothing"); + REQUIRE_FALSE(service.references(script, 3, 0).found); } - template<> template<> - void alluauservice_object::test<13>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the outline lists what the top binds and functions with inner ones deeper", "[alluauservice]") { - set_test_name("the outline lists what the top binds and every function, each function's own one deeper"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local count = 1\n" "local function half(n: number)\n" @@ -378,33 +398,66 @@ namespace tut std::vector outline = service.outline(script); std::string names; for (const ALScriptOutlineEntry& e : outline) names += " " + e.name + llformat("@%d", e.depth); - ensure_equals("five entries:" + names, outline.size(), size_t(5)); - ensure("count first, a number", outline[0].name == "count" && outline[0].kind == ALScriptSymbolKind::Variable && outline[0].detail == "number" && outline[0].depth == 0); - ensure("half, a function spanning its lines", outline[1].name == "half" && outline[1].kind == ALScriptSymbolKind::Function && outline[1].span.line == 1 && outline[1].span.endLine == 5); - ensure("its name where it is", outline[1].nameSpan.line == 1 && outline[1].nameSpan.column == 15); - ensure("quarter inside it, one deeper", outline[2].name == "quarter" && outline[2].depth == 1); - ensure("the event handler, as an event", outline[3].name == "LLEvents.touch_start" && outline[3].kind == ALScriptSymbolKind::Event); - ensure("the global", outline[4].name == "handlers" && outline[4].kind == ALScriptSymbolKind::Variable); + UNSCOPED_INFO("five entries:" + names); + REQUIRE(outline.size() == size_t(5)); + { + INFO("count first, a number"); + CHECK(outline[0].name == "count"); + CHECK(outline[0].kind == ALScriptSymbolKind::Variable); + CHECK(outline[0].detail == "number"); + CHECK(outline[0].depth == 0); + } + { + INFO("half, a function spanning its lines"); + CHECK(outline[1].name == "half"); + CHECK(outline[1].kind == ALScriptSymbolKind::Function); + CHECK(outline[1].span.line == 1); + CHECK(outline[1].span.endLine == 5); + } + { + INFO("its name where it is"); + CHECK(outline[1].nameSpan.line == 1); + CHECK(outline[1].nameSpan.column == 15); + } + { + INFO("quarter inside it, one deeper"); + CHECK(outline[2].name == "quarter"); + CHECK(outline[2].depth == 1); + } + { + INFO("the event handler, as an event"); + CHECK(outline[3].name == "LLEvents.touch_start"); + CHECK(outline[3].kind == ALScriptSymbolKind::Event); + } + { + INFO("the global"); + CHECK(outline[4].name == "handlers"); + CHECK(outline[4].kind == ALScriptSymbolKind::Variable); + } } - template<> template<> - void alluauservice_object::test<14>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a missing key is named as written with the nearest key there is", "[alluauservice]") { - set_test_name("a key that is not there is named by what was written, with the nearest key there is"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); ALScriptProblems problems = service.check("ll.ay(0, \"hi\")\n"); - ensure("one problem", !problems.empty()); + REQUIRE_FALSE(problems.empty()); const std::string& message = problems.front().message; - ensure("names ll, not its fields: " + message, message.find("not found in ll") != std::string::npos && message.find("Abs") == std::string::npos); - ensure("and suggests Say: " + message, message.find("'Say'") != std::string::npos); - ensure(llformat("in a glance, not %d characters", (int)message.size()), message.size() < 100); + { + INFO("names ll, not its fields: " + message); + REQUIRE(message.find("not found in ll") != std::string::npos); + REQUIRE(message.find("Abs") == std::string::npos); + } + UNSCOPED_INFO("and suggests Say: " + message); + REQUIRE(message.find("'Say'") != std::string::npos); + UNSCOPED_INFO("in a glance"); + REQUIRE(message.size() < 100); } - template<> template<> - void alluauservice_object::test<15>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: every name is told by what it is: its kind and modifiers in order", "[alluauservice]") { - set_test_name("every name is told by what it is: parameter, local, global, field, function, type, builtin, deprecated"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local count = 1\n" "local function half(n: number): number\n" @@ -425,40 +478,96 @@ namespace tut std::string listed; for (const ALScriptSemanticToken& t : tokens) listed += llformat(" %d:%d/%d+%d", t.span.line, t.span.column, (int)t.kind, (int)t.modifiers); const ALScriptSemanticToken* count_decl = at(0, 6); - ensure("count declared, a variable:" + listed, count_decl && count_decl->kind == ALScriptSymbolKind::Variable && (count_decl->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("count declared, a variable:" + listed); + REQUIRE(count_decl); + REQUIRE(count_decl->kind == ALScriptSymbolKind::Variable); + CHECK(count_decl->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* half_decl = at(1, 15); - ensure("half declared, a function:" + listed, half_decl && half_decl->kind == ALScriptSymbolKind::Function && (half_decl->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("half declared, a function:" + listed); + REQUIRE(half_decl); + REQUIRE(half_decl->kind == ALScriptSymbolKind::Function); + CHECK(half_decl->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* n_param = at(1, 20); - ensure("n, a parameter:" + listed, n_param && n_param->kind == ALScriptSymbolKind::Parameter); + { + INFO("n, a parameter:" + listed); + REQUIRE(n_param); + CHECK(n_param->kind == ALScriptSymbolKind::Parameter); + } const ALScriptSemanticToken* n_type = at(1, 23); - ensure("number, a type:" + listed, n_type && n_type->kind == ALScriptSymbolKind::Type); + { + INFO("number, a type:" + listed); + REQUIRE(n_type); + CHECK(n_type->kind == ALScriptSymbolKind::Type); + } const ALScriptSemanticToken* n_use = at(2, 11); - ensure("n used, still a parameter:" + listed, n_use && n_use->kind == ALScriptSymbolKind::Parameter && !(n_use->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("n used, still a parameter:" + listed); + REQUIRE(n_use); + REQUIRE(n_use->kind == ALScriptSymbolKind::Parameter); + CHECK_FALSE(n_use->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* pair = at(4, 5); - ensure("Pair declared, a type:" + listed, pair && pair->kind == ALScriptSymbolKind::Type && (pair->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("Pair declared, a type:" + listed); + REQUIRE(pair); + REQUIRE(pair->kind == ALScriptSymbolKind::Type); + CHECK(pair->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* a_key = at(5, 18); - ensure("a in the table, a field declared:" + listed, a_key && a_key->kind == ALScriptSymbolKind::Field && (a_key->modifiers & ALScriptSemanticToken::Declaration)); + { + INFO("a in the table, a field declared:" + listed); + REQUIRE(a_key); + REQUIRE(a_key->kind == ALScriptSymbolKind::Field); + CHECK(a_key->modifiers & ALScriptSemanticToken::Declaration); + } const ALScriptSemanticToken* ll = at(6, 0); - ensure("ll, a builtin global:" + listed, ll && (ll->modifiers & ALScriptSemanticToken::Builtin) && (ll->modifiers & ALScriptSemanticToken::Global)); + { + INFO("ll, a builtin global:" + listed); + REQUIRE(ll); + REQUIRE(ll->modifiers & ALScriptSemanticToken::Builtin); + CHECK(ll->modifiers & ALScriptSemanticToken::Global); + } const ALScriptSemanticToken* say = at(6, 3); - ensure("Say, a builtin function:" + listed, say && say->kind == ALScriptSymbolKind::Function && (say->modifiers & ALScriptSemanticToken::Builtin)); + { + INFO("Say, a builtin function:" + listed); + REQUIRE(say); + REQUIRE(say->kind == ALScriptSymbolKind::Function); + CHECK(say->modifiers & ALScriptSemanticToken::Builtin); + } const ALScriptSemanticToken* p_a = at(6, 21); - ensure("p.a, a field:" + listed, p_a && p_a->kind == ALScriptSymbolKind::Field); + { + INFO("p.a, a field:" + listed); + REQUIRE(p_a); + CHECK(p_a->kind == ALScriptSymbolKind::Field); + } const ALScriptSemanticToken* half_call = at(6, 25); - ensure("half called, a function:" + listed, half_call && half_call->kind == ALScriptSymbolKind::Function); + { + INFO("half called, a function:" + listed); + REQUIRE(half_call); + CHECK(half_call->kind == ALScriptSymbolKind::Function); + } const ALScriptSemanticToken* handlers = at(7, 0); - ensure("handlers, a global the script binds:" + listed, handlers && (handlers->modifiers & ALScriptSemanticToken::Global) && !(handlers->modifiers & ALScriptSemanticToken::Builtin)); + { + INFO("handlers, a global the script binds:" + listed); + REQUIRE(handlers); + REQUIRE(handlers->modifiers & ALScriptSemanticToken::Global); + CHECK_FALSE(handlers->modifiers & ALScriptSemanticToken::Builtin); + } for (size_t i = 1; i < tokens.size(); ++i) { - ensure("in order, each once", tokens[i - 1].span < tokens[i].span); + UNSCOPED_INFO("in order, each once"); + REQUIRE(tokens[i - 1].span < tokens[i].span); } } - template<> template<> - void alluauservice_object::test<16>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: inlay hints name each argument's parameter and each untyped local's type", "[alluauservice]") { - set_test_name("inlay hints name each argument's parameter and the type a local was given without saying"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local function greet(name: string, times: number) end\n" "local times = 3\n" @@ -475,49 +584,105 @@ namespace tut } return false; }; - ensure("the local's type after its name:" + listed, has(1, 11, ": number")); + UNSCOPED_INFO("the local's type after its name:" + listed); + CHECK(has(1, 11, ": number")); for (const ALScriptInlayHint& h : hints) { - ensure("a type may be written in, a parameter's name never: " + h.text, h.writable == (h.kind == ALScriptInlayHint::Kind::Type)); + UNSCOPED_INFO("a type may be written in, a parameter's name never: " + h.text); + REQUIRE(h.writable == (h.kind == ALScriptInlayHint::Kind::Type)); } // A type cut short says less than the text would have to. const std::vector wide = service.inlayHints("local t = { a = 1, b = 2, c = 3, d = 4, e = 5 }\n", false, true); - ensure("not a table cut short", wide.empty() || !wide.front().writable || wide.front().text.find("...") == std::string::npos); - ensure("not one that says its type:" + listed, !has(2, 11, ": number")); - ensure("name: before the string:" + listed, has(3, 6, "name:")); - ensure("nothing before an argument that is the name:" + listed, !has(3, 12, "times:")); - ensure("the channel's name before the 0:" + listed, has(4, 7, "channel:")); - ensure("and the message's:" + listed, has(4, 10, "msg:")); + UNSCOPED_INFO("not a table cut short"); + REQUIRE((wide.empty() || !wide.front().writable || wide.front().text.find("...") == std::string::npos)); + UNSCOPED_INFO("not one that says its type:" + listed); + CHECK_FALSE(has(2, 11, ": number")); + UNSCOPED_INFO("name: before the string:" + listed); + CHECK(has(3, 6, "name:")); + UNSCOPED_INFO("nothing before an argument that is the name:" + listed); + CHECK_FALSE(has(3, 12, "times:")); + UNSCOPED_INFO("the channel's name before the 0:" + listed); + CHECK(has(4, 7, "channel:")); + UNSCOPED_INFO("and the message's:" + listed); + CHECK(has(4, 10, "msg:")); std::vector only_types = service.inlayHints(script, false, true); - ensure("types alone, when asked", only_types.size() == 1 && only_types.front().kind == ALScriptInlayHint::Kind::Type); - ensure("nothing when neither is asked", service.inlayHints(script, false, false).empty()); + { + INFO("types alone, when asked"); + REQUIRE(only_types.size() == 1); + CHECK(only_types.front().kind == ALScriptInlayHint::Kind::Type); + } + UNSCOPED_INFO("nothing when neither is asked"); + REQUIRE(service.inlayHints(script, false, false).empty()); } - template<> template<> - void alluauservice_object::test<18>() + + TEST_CASE_METHOD(alluauservice_data, "alluauservice: hover tells each kind of name and the type wanted where a type is wrong", "[alluauservice]") { - set_test_name("the configuration names the globals a script may use and turns lints off, until it is replaced"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); + const std::string script = + "local function half(n: number): number\n" + " return n / 2\n" + "end\n" + "const limit = 3\n" + "local p = { a = 1 }\n" + "ll.Say(0, half(\"x\"))\n" + "print(limit, p.a)\n"; + ALScriptHover hover = service.hover(script, 0, 16); // half, declared + UNSCOPED_INFO("a function by its signature: " + hover.label); + CHECK(hover.label.rfind("function half(n: number): number", 0) == 0); + hover = service.hover(script, 0, 20); // n, the parameter + UNSCOPED_INFO("a parameter: " + hover.label); + CHECK(hover.label == "(parameter) n: number"); + hover = service.hover(script, 1, 11); // n, used + UNSCOPED_INFO("still a parameter where used: " + hover.label); + CHECK(hover.label == "(parameter) n: number"); + hover = service.hover(script, 3, 7); // limit + UNSCOPED_INFO("a const: " + hover.label); + CHECK(hover.label == "const limit: number"); + hover = service.hover(script, 4, 6); // p + UNSCOPED_INFO("a local: " + hover.label); + CHECK(hover.label.rfind("local p: ", 0) == 0); + hover = service.hover(script, 6, 15); // p.a + UNSCOPED_INFO("a field: " + hover.label); + CHECK(hover.label == "(field) p.a: number"); + hover = service.hover(script, 5, 16); // "x" where a number is wanted + UNSCOPED_INFO("what is wanted there: " + hover.expected); + CHECK(hover.expected == "number"); + hover = service.hover(script, 6, 0); // print + // The new solver gives print its generic pack: print. + UNSCOPED_INFO("a builtin by its signature: " + hover.label); + CHECK(hover.label.rfind(newSolver ? "function print<" : "function print(", 0) == 0); + } + + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the configuration adds globals and turns lints off until it is replaced", "[alluauservice]") + { + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local unused = 1\nAlchemy.log(\"hi\")\n"; ALScriptProblems plain = service.check(script); - ensure("Alchemy unknown without a configuration: " + said(plain), mentions(plain, "Alchemy")); - ensure("the unused local warned of: " + said(plain), mentions(plain, "unused")); + UNSCOPED_INFO("Alchemy unknown without a configuration: " + said(plain)); + CHECK(mentions(plain, "Alchemy")); + UNSCOPED_INFO("the unused local warned of: " + said(plain)); + CHECK(mentions(plain, "unused")); ALLuauConfig config; std::string bad; - ensure("parses: " + bad, ALLuauConfig::parse("{ \"globals\": [\"Alchemy\"], \"lint\": { \"LocalUnused\": false } }", config, bad)); + REQUIRE(ALLuauConfig::parse("{ \"globals\": [\"Alchemy\"], \"lint\": { \"LocalUnused\": false } }", config, bad)); service.setConfig(config); ALScriptProblems configured = service.check(script); - ensure("Alchemy known: " + said(configured), !mentions(configured, "Alchemy")); - ensure("the unused local let be: " + said(configured), !mentions(configured, "unused")); + UNSCOPED_INFO("Alchemy known: " + said(configured)); + CHECK_FALSE(mentions(configured, "Alchemy")); + UNSCOPED_INFO("the unused local let be: " + said(configured)); + CHECK_FALSE(mentions(configured, "unused")); service.setConfig(ALLuauConfig()); ALScriptProblems again = service.check(script); - ensure("unknown again: " + said(again), mentions(again, "Alchemy")); + UNSCOPED_INFO("unknown again: " + said(again)); + CHECK(mentions(again, "Alchemy")); } - template<> template<> - void alluauservice_object::test<19>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the engine's commonest messages come back keyed", "[alluauservice]") { - set_test_name("the engine's commonest messages come back keyed, which an upgrade that rewords them would end"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); // Each script says one thing the map has a row for; a Luau whose // wording moved on gives the message with no key, and this is // where that shows -- run check_script_strings.py then. @@ -545,14 +710,15 @@ namespace tut { keyed |= p.key.compare(0, strlen(c.key), c.key) == 0; } - ensure(std::string("keyed ") + c.key + " for " + c.script + ": " + said(problems), keyed); + UNSCOPED_INFO(std::string("keyed ") + c.key + " for " + c.script + ": " + said(problems)); + REQUIRE(keyed); } } - template<> template<> - void alluauservice_object::test<20>() + + TEST_CASE_METHOD(alluauservice_data, "alluauservice: questions reuse a text's check and hints are the same whatever came first", "[alluauservice]") { - set_test_name("a question asked again of the same text is answered from the check made, and what goes beside the text is the same whatever came first"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local function f(a) return a + 1 end\nlocal y = f(2)\nlocal z = y * 2\nprint(z)\n"; ALLuauConfig config; service.setConfig(config); @@ -567,17 +733,20 @@ namespace tut service.references(script, 1, 6); service.outline(script); service.semanticTokens(script); - ensure_equals("the queries share the one check", service.typeChecks(), checked); + UNSCOPED_INFO("the queries share the one check"); + CHECK(service.typeChecks() == checked); service.setConfig(config); service.hover(script, 1, 6); - ensure_equals("the same configuration again changes nothing", service.typeChecks(), checked); + UNSCOPED_INFO("the same configuration again changes nothing"); + CHECK(service.typeChecks() == checked); // A text asked about before a check has what a hover does. const std::vector after_hover = service.inlayHints(script, true, true); - ensure_equals("as many hints", after_hover.size(), after_check.size()); + REQUIRE(after_hover.size() == after_check.size()); for (size_t i = 0; i < after_hover.size(); ++i) { - ensure("the same hint: " + after_hover[i].text + " / " + after_check[i].text, - after_hover[i].text == after_check[i].text && after_hover[i].line == after_check[i].line); + CAPTURE(i); + REQUIRE(after_hover[i].text == after_check[i].text); + REQUIRE(after_hover[i].line == after_check[i].line); } // Another configuration, or another text, is checked again: one // unlike either solver's default. @@ -585,45 +754,54 @@ namespace tut strict.mode = "nocheck"; service.setConfig(strict); service.hover(script, 1, 6); - ensure("another configuration, checked again", service.typeChecks() > checked); + UNSCOPED_INFO("another configuration, checked again"); + CHECK(service.typeChecks() > checked); const size_t now = service.typeChecks(); service.hover(script + "print(y)\n", 1, 6); - ensure("another text, checked again", service.typeChecks() > now); + UNSCOPED_INFO("another text, checked again"); + CHECK(service.typeChecks() > now); service.setConfig(config); } - template<> template<> - void alluauservice_object::test<21>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a dotted method call takes its object first and the signature counts it", "[alluauservice]") { - set_test_name("a method called with a dot is given its object as its first argument, and the signature counts it"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local T = {}\nfunction T:m(a: number, b: string) end\nT.m(T, 1, \"x\")\nT:m(1, \"x\")\n"; ALScriptSignature dotted = service.signature(script, 2, 12); // in "x" - ensure("found", dotted.found); - ensure_equals("self, a and b: " + dotted.label, dotted.parameters.size(), size_t(3)); - ensure_equals("at the third", dotted.active, 2); - ensure("which is b: " + dotted.parameters[2], dotted.parameters[2].find("b") == 0); + REQUIRE(dotted.found); + UNSCOPED_INFO("self, a and b: " + dotted.label); + REQUIRE(dotted.parameters.size() == size_t(3)); + CHECK(dotted.active == 2); + UNSCOPED_INFO("which is b: " + dotted.parameters[2]); + CHECK(dotted.parameters[2].find("b") == 0); ALScriptSignature colon = service.signature(script, 3, 9); // in "x" - ensure_equals("with a colon, a and b", colon.parameters.size(), size_t(2)); - ensure("at b: " + colon.parameters[static_cast(std::max(0, colon.active))], colon.active == 1 && colon.parameters[1].find("b") == 0); + UNSCOPED_INFO("with a colon, a and b"); + REQUIRE(colon.parameters.size() == size_t(2)); + { + INFO("at b: " + colon.parameters[static_cast(std::max(0, colon.active))]); + CHECK(colon.active == 1); + CHECK(colon.parameters[1].find("b") == 0); + } } - template<> template<> - void alluauservice_object::test<22>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: both solvers use the definitions; the new nonstrict says only sure failures", "[alluauservice]") { - set_test_name("either solver checks with the definitions, the new one in nonstrict mode saying only what is sure to fail as the script runs"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); for (const bool use : { true, false, true }) { std::string why; - ensure("switched: " + why, service.setNewSolver(use, why)); - ensure_equals("the one asked for", service.newSolver(), use); - ensure("the definitions with it", service.hasDefinitions()); + REQUIRE(service.setNewSolver(use, why)); + REQUIRE(service.newSolver() == use); + UNSCOPED_INFO("the definitions with it"); + REQUIRE(service.hasDefinitions()); ALLuauConfig config; config.mode = "strict"; service.setConfig(config); ALScriptProblems problems = service.check("ll.Say(\"zero\", 0)\n"); - ensure(std::string(use ? "new" : "old") + ", strict, a wrong argument: " + said(problems), errors(problems) > 0); + UNSCOPED_INFO(std::string(use ? "new" : "old") + ", strict, a wrong argument: " + said(problems)); + REQUIRE(errors(problems) > 0); // A method called with a dot is a missing self, whichever says it. problems = service.check("local T = {}\nfunction T:m(a: number) end\nT.m(1)\n"); bool missing_self = false; @@ -631,25 +809,30 @@ namespace tut { missing_self |= problem.key == "LuauRequiresSelf" && problem.line == 2 && problem.column == 0; } - ensure(std::string(use ? "new" : "old") + ": a missing self, at the call: " + said(problems), missing_self); + UNSCOPED_INFO(std::string(use ? "new" : "old") + ": a missing self, at the call: " + said(problems)); + REQUIRE(missing_self); } // Nonstrict, as the grid compiles: the new solver says nothing of // ll.Say's argument, and what it does say comes keyed. ALLuauConfig config; config.mode = "nonstrict"; service.setConfig(config); - ensure("nothing of ll.Say: " + said(service.check("ll.Say(\"zero\", 0)\n")), errors(service.check("ll.Say(\"zero\", 0)\n")) == 0); + UNSCOPED_INFO("nothing of ll.Say: " + said(service.check("ll.Say(\"zero\", 0)\n"))); + CHECK(errors(service.check("ll.Say(\"zero\", 0)\n")) == 0); const ALScriptProblems problems = service.check("print(string.len(5))\n"); - ensure("a checked function given the wrong type: " + said(problems), !problems.empty() && problems.front().key == "LuauCheckedCall"); + { + INFO("a checked function given the wrong type: " + said(problems)); + REQUIRE_FALSE(problems.empty()); + CHECK(problems.front().key == "LuauCheckedCall"); + } std::string why; service.setNewSolver(false, why); } - template<> template<> - void alluauservice_object::test<23>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a check past its time limit says so; a stopped one answers nothing", "[alluauservice]") { - set_test_name("a check past its time limit stops, says so, and answers what it found by then; a stopped one answers nothing"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); std::string script; for (int i = 0; i < 50; ++i) { @@ -663,42 +846,66 @@ namespace tut // Far less than any check takes. service.setTimeLimit(1e-9); ALScriptProblems problems = service.check(script); - ensure(which + "said to have stopped: " + said(problems), !problems.empty() && problems.front().key == "LuauCheckTimedOut" && - problems.front().severity == ALScriptProblem::Severity::Warning); + { + INFO(which + "said to have stopped: " + said(problems)); + REQUIRE_FALSE(problems.empty()); + REQUIRE(problems.front().key == "LuauCheckTimedOut"); + REQUIRE(problems.front().severity == ALScriptProblem::Severity::Warning); + } service.setTimeLimit(0.0); problems = service.check(script); - ensure(which + "unlimited, it runs its course: " + said(problems), problems.empty()); + UNSCOPED_INFO(which + "unlimited, it runs its course: " + said(problems)); + REQUIRE(problems.empty()); // Stopped before it began: nothing, and said to be stopped. ALLuauService::Stop stop = ALLuauService::newStop(); ALLuauService::cancel(stop); service.setStop(stop); problems = service.check("local n: number = \"s\"\n"); - ensure(which + "stopped, nothing: " + said(problems), problems.empty() && service.stopped()); + { + INFO(which + "stopped, nothing: " + said(problems)); + REQUIRE(problems.empty()); + REQUIRE(service.stopped()); + } // And a question after it is answered from a check of its own, // not the part of one the stop left. service.setStop(nullptr); const ALScriptHover hover = service.hover("local n: number = \"s\"\n", 0, 6); - ensure(which + "not stopped now", !service.stopped()); - ensure(which + "answered: " + hover.label, hover.label.find("n: number") != std::string::npos); + UNSCOPED_INFO(which + "not stopped now"); + REQUIRE_FALSE(service.stopped()); + UNSCOPED_INFO(which + "answered: " + hover.label); + REQUIRE(hover.label.find("n: number") != std::string::npos); problems = service.check("local n: number = \"s\"\n"); - ensure(which + "and checked in full: " + said(problems), errors(problems) > 0 || use); + UNSCOPED_INFO(which + "and checked in full: " + said(problems)); + REQUIRE((errors(problems) > 0 || use)); } std::string why; service.setNewSolver(newSolver, why); } - template<> template<> - void alluauservice_object::test<24>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: selene's comments allow or deny Luau's lints for the file or one statement", "[alluauservice]") { - set_test_name("selene's comments: its lints named as Luau's, allowed or denied for the whole file before any code, or for the statement beside"); - ensure("definitions load: " + error, loaded); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); const auto read = ALSeleneFilters::read("-- # selene: allow(unused_variable, multiple_statements))"); - ensure("read leniently, for the whole file", read && read->file && read->action == ALSeleneFilters::Action::Allow && read->lints.size() == 2); - ensure("a Luau name stands for itself, selene's for Luau's", - ALSeleneFilters::luauLints("LocalUnused") == ALLuauConfig::lintBit("LocalUnused") && - (ALSeleneFilters::luauLints("unused_variable") & ALLuauConfig::lintBit("FunctionUnused")) != 0); - ensure("a check Luau does not make stands for none", ALSeleneFilters::luauLints("almost_swapped") == 0); - ensure("not a directive", !ALSeleneFilters::read("-- seleneous: allow(x)") && !ALSeleneFilters::read("-- selene: permit(x)")); + { + INFO("read leniently, for the whole file"); + REQUIRE(read); + REQUIRE(read->file); + REQUIRE(read->action == ALSeleneFilters::Action::Allow); + REQUIRE(read->lints.size() == 2); + } + { + INFO("a Luau name stands for itself, selene's for Luau's"); + CHECK(ALSeleneFilters::luauLints("LocalUnused") == ALLuauConfig::lintBit("LocalUnused")); + CHECK((ALSeleneFilters::luauLints("unused_variable") & ALLuauConfig::lintBit("FunctionUnused")) != 0); + } + UNSCOPED_INFO("a check Luau does not make stands for none"); + CHECK(ALSeleneFilters::luauLints("almost_swapped") == 0); + { + INFO("not a directive"); + REQUIRE_FALSE(ALSeleneFilters::read("-- seleneous: allow(x)")); + REQUIRE_FALSE(ALSeleneFilters::read("-- selene: permit(x)")); + } const auto unused = [&](const std::string& script) { size_t count = 0; @@ -708,41 +915,48 @@ namespace tut } return count; }; - ensure_equals("without, both said", unused("local a = 1\nlocal b = 2\n"), size_t(2)); - ensure_equals("the whole file", unused("-- @file header\n-- # selene: allow(unused_variable)\nlocal a = 1\nlocal b = 2\n"), size_t(0)); - ensure_equals("after code, nothing: selene refuses a whole file's there", unused("local a = 1\n--# selene: allow(unused_variable)\nlocal b = 2\n"), size_t(2)); - ensure_equals("the statement beside, not the next", unused("-- selene: allow(unused_variable)\nlocal a = 1\nlocal b = 2\n"), size_t(1)); - ensure_equals("a whole function beside it", - unused("-- selene: allow(unused_variable)\nlocal function f()\n local a = 1\nend\nf()\nlocal b = 2\n"), size_t(1)); - ensure_equals("at a line's end, its statement", unused("local a = 1 -- selene: allow(unused_variable)\nlocal b = 2\n"), size_t(1)); + UNSCOPED_INFO("without, both said"); + CHECK(unused("local a = 1\nlocal b = 2\n") == size_t(2)); + UNSCOPED_INFO("the whole file"); + CHECK(unused("-- @file header\n-- # selene: allow(unused_variable)\nlocal a = 1\nlocal b = 2\n") == size_t(0)); + UNSCOPED_INFO("after code, nothing: selene refuses a whole file's there"); + CHECK(unused("local a = 1\n--# selene: allow(unused_variable)\nlocal b = 2\n") == size_t(2)); + UNSCOPED_INFO("the statement beside, not the next"); + CHECK(unused("-- selene: allow(unused_variable)\nlocal a = 1\nlocal b = 2\n") == size_t(1)); + UNSCOPED_INFO("a whole function beside it"); + CHECK(unused("-- selene: allow(unused_variable)\nlocal function f()\n local a = 1\nend\nf()\nlocal b = 2\n") == size_t(1)); + UNSCOPED_INFO("at a line's end, its statement"); + CHECK(unused("local a = 1 -- selene: allow(unused_variable)\nlocal b = 2\n") == size_t(1)); bool denied = false; for (const ALScriptProblem& p : service.check("-- selene: deny(unused_variable)\nlocal a = 1\n")) { denied |= p.key.compare(0, 19, "LuauLintLocalUnused") == 0 && p.severity == ALScriptProblem::Severity::Error; } - ensure("denied: an error", denied); + UNSCOPED_INFO("denied: an error"); + CHECK(denied); } - template<> template<> - void alluauservice_object::test<25>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: an overloaded function's signature shows every form and the one that fits", "[alluauservice]") { - set_test_name("an overloaded function's signature: every form, the one the arguments fit first shown"); - ensure("definitions load: " + error, loaded); + UNSCOPED_INFO("definitions load: " + error); + CHECK(loaded); const std::string script = "local f: ((n: number) -> ()) & ((a: string, b: string) -> ()) = nil :: any\nf(\"x\", \"y\")\n"; const ALScriptSignature sig = service.signature(script, 1, 7); - ensure("found", sig.found); - ensure_equals("both forms", sig.overloads.size(), size_t(2)); - ensure_equals("the second fits two arguments", sig.overload, 1); - ensure("shown", sig.label == sig.overloads[1].label && sig.parameters.size() == 2); - ensure_equals("at the second argument", sig.active, 1); - ensure("one form: none listed", service.signature("local function g(a: number) end\ng(1)\n", 1, 2).overloads.empty()); + REQUIRE(sig.found); + REQUIRE(sig.overloads.size() == size_t(2)); + UNSCOPED_INFO("the second fits two arguments"); + CHECK(sig.overload == 1); + CHECK(sig.label == sig.overloads[1].label); + REQUIRE(sig.parameters.size() == 2); + CHECK(sig.active == 1); + UNSCOPED_INFO("one form: none listed"); + REQUIRE(service.signature("local function g(a: number) end\ng(1)\n", 1, 2).overloads.empty()); } - template<> template<> - void alluauservice_object::test<26>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: type checked once for problems and once for questions; once if strict or new", "[alluauservice]") { - set_test_name("a text is type checked once for its check and once for the questions, in whatever order they come; once for both where it is strict or the solver is the new one"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); ALLuauConfig config; service.setConfig(config); const bool one_slot = service.newSolver(); @@ -759,9 +973,12 @@ namespace tut service.complete(loose, 2, 2); service.signature(loose, 1, 12); service.references(loose, 1, 6); - ensure_equals("nonstrict: the check, and the questions' own", service.typeChecks() - start, size_t(one_slot ? 1 : 2)); - ensure_equals("the check again, from what it found", said(service.check(loose)), first); - ensure_equals("and not checked again", service.typeChecks() - start, size_t(one_slot ? 1 : 2)); + UNSCOPED_INFO("nonstrict: the check, and the questions' own"); + CHECK(service.typeChecks() - start == size_t(one_slot ? 1 : 2)); + UNSCOPED_INFO("the check again, from what it found"); + CHECK(said(service.check(loose)) == first); + UNSCOPED_INFO("and not checked again"); + CHECK(service.typeChecks() - start == size_t(one_slot ? 1 : 2)); // A script strict by its own comment: its check serves the questions. const std::string strict = "--!strict\n" + loose; @@ -770,51 +987,71 @@ namespace tut service.check(strict); service.outline(strict); service.inlayHints(strict, true, true); - ensure_equals("strict: one check, the question first", service.typeChecks() - before, size_t(1)); + UNSCOPED_INFO("strict: one check, the question first"); + CHECK(service.typeChecks() - before == size_t(1)); service.complete(strict, 3, 2); - ensure_equals("a completion reads autocomplete's own", service.typeChecks() - before, size_t(one_slot ? 1 : 2)); + UNSCOPED_INFO("a completion reads autocomplete's own"); + CHECK(service.typeChecks() - before == size_t(one_slot ? 1 : 2)); const std::string strict_first = said(service.check(strict)); service.hover(strict, 2, 6); - ensure_equals("and nothing checked again", service.typeChecks() - before, size_t(one_slot ? 1 : 2)); + UNSCOPED_INFO("and nothing checked again"); + CHECK(service.typeChecks() - before == size_t(one_slot ? 1 : 2)); // The hints the questions read are the strict ones either way. // The types a question reads are the strict ones, whatever the // script's mode: a nonstrict check's module knows none of them. const std::string typed = "local s = string.rep(\"a\", 2)\nlocal n = #s\nprint(n)\n"; const auto loose_hints = service.inlayHints(typed, false, true); const auto strict_hints = service.inlayHints("--!strict\n" + typed, false, true); - ensure_equals("hints read nonstrict", loose_hints.size(), size_t(2)); - ensure_equals("and strict", strict_hints.size(), size_t(2)); - ensure("the same", loose_hints[0].text == strict_hints[0].text && loose_hints[1].text == strict_hints[1].text); - ensure("the problems of each told apart", said(service.check(loose)) == first && said(service.check(strict)) == strict_first); + UNSCOPED_INFO("hints read nonstrict"); + REQUIRE(loose_hints.size() == size_t(2)); + UNSCOPED_INFO("and strict"); + REQUIRE(strict_hints.size() == size_t(2)); + CHECK(loose_hints[0].text == strict_hints[0].text); + CHECK(loose_hints[1].text == strict_hints[1].text); + { + INFO("the problems of each told apart"); + CHECK(said(service.check(loose)) == first); + CHECK(said(service.check(strict)) == strict_first); + } } - template<> template<> - void alluauservice_object::test<27>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the same definitions and docs again load nothing and keep what was checked", "[alluauservice]") { - set_test_name("the same definitions and docs again are the ones in hand: nothing loaded over, and what was checked stands"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local n: number = 1\nprint(n)\n"; service.check(script); const size_t checked = service.typeChecks(); std::string why; - ensure("the same again: loaded", service.loadDefinitions(definitions, why) && why.empty()); + { + INFO("the same again: loaded"); + REQUIRE(service.loadDefinitions(definitions, why)); + REQUIRE(why.empty()); + } service.check(script); - ensure_equals("what was checked stands", service.typeChecks(), checked); + UNSCOPED_INFO("what was checked stands"); + CHECK(service.typeChecks() == checked); const std::string docs = "{\"@sl-slua/global/ll.Say\": {\"documentation\": \"Says it.\", \"learn_more_link\": \"x\"}}"; - ensure("docs loaded", service.loadDocs(docs, why) && service.hasDocs()); - ensure("and the same again", service.loadDocs(docs, why) && service.hasDocs()); + CHECK(service.loadDocs(docs, why)); + CHECK(service.hasDocs()); + { + INFO("and the same again"); + CHECK(service.loadDocs(docs, why)); + CHECK(service.hasDocs()); + } // Other definitions are loaded over, and the script checked again. - ensure("others loaded", service.loadDefinitions(definitions + "\ndeclare function extraThing(): number\n", why)); + UNSCOPED_INFO("others loaded"); + CHECK(service.loadDefinitions(definitions + "\ndeclare function extraThing(): number\n", why)); service.check(script); - ensure("checked again against them", service.typeChecks() > checked); + UNSCOPED_INFO("checked again against them"); + CHECK(service.typeChecks() > checked); service.loadDefinitions(definitions, why); } - template<> template<> - void alluauservice_object::test<28>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a local's type is hinted at any depth and offered as a refactor at its line", "[alluauservice]") { - set_test_name("a local's type is found however deep its scope, and the refactor at a line reads that line's hint as the whole text's has it"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local a = 1\n" "local function f()\n" " local b = \"x\"\n" @@ -830,7 +1067,8 @@ namespace tut { all += std::to_string(hint.line) + hint.text + ";"; } - ensure_equals("every local's, at every depth", all, std::string("0: number;2: string;4: number;")); + UNSCOPED_INFO("every local's, at every depth"); + CHECK(all == std::string("0: number;2: string;4: number;")); for (const auto& [line, column, type] : { std::tuple{ 0, 6, "number" }, std::tuple{ 2, 10, "string" }, std::tuple{ 4, 14, "number" } }) { bool offered = false; @@ -838,15 +1076,15 @@ namespace tut { offered |= fix.title == std::string("Declare it as '") + type + "'"; } - ensure("line " + std::to_string(line) + ": declared as " + type, offered); + CAPTURE(line, type); + REQUIRE(offered); } } - template<> template<> - void alluauservice_object::test<29>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: each script its own module and configuration; the oldest of many let go", "[alluauservice]") { - set_test_name("each script its own module: moving between a few finds each checked as it was left, each with its own configuration"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string a = "local a: number = 1\nprint(a)\n"; const std::string b = "local b: string = \"x\"\nprint(b)\n"; ALLuauConfig loose; @@ -868,7 +1106,8 @@ namespace tut service.setConfig(loose); service.check(b); } - ensure_equals("turn and turn about, each as it was left", service.typeChecks(), both); + UNSCOPED_INFO("turn and turn about, each as it was left"); + CHECK(service.typeChecks() == both); // Past a few, the one asked of longest ago is let go of. for (const char* other : { "c", "d", "e" }) { @@ -880,23 +1119,25 @@ namespace tut service.setDocument("b"); service.setConfig(loose); service.check(b); - ensure_equals("one of the last few kept", service.typeChecks(), now); + UNSCOPED_INFO("one of the last few kept"); + CHECK(service.typeChecks() == now); service.setDocument("a"); service.setConfig(strict); service.check(a); - ensure_equals("the one asked of longest ago let go of", service.typeChecks(), now + 1); + UNSCOPED_INFO("the one asked of longest ago let go of"); + CHECK(service.typeChecks() == now + 1); // The module of no name is apart from them all. service.setDocument(""); service.setConfig(loose); service.check(a); - ensure_equals("unnamed: its own", service.typeChecks(), now + 2); + UNSCOPED_INFO("unnamed: its own"); + CHECK(service.typeChecks() == now + 2); } - template<> template<> - void alluauservice_object::test<30>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a module put ahead of the script is passed over by the names and the hints", "[alluauservice]") { - set_test_name("lines nobody reads -- a module put ahead of the script -- are passed over by the names and the hints"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string text = "local __modules = {}\n" "__modules[\"m\"] = (function()\nlocal inner = 1\nreturn inner\nend)()\n" "local own = __modules[\"m\"]\nprint(own)\n"; @@ -906,29 +1147,32 @@ namespace tut { in_module |= token.span.line >= 1 && token.span.line <= 4; } - ensure("no names coloured in the module", !in_module); + UNSCOPED_INFO("no names coloured in the module"); + CHECK_FALSE(in_module); bool hinted_module = false, hinted_own = false; for (const ALScriptInlayHint& hint : service.inlayHints(text, false, true)) { hinted_module |= hint.line >= 1 && hint.line <= 4; hinted_own |= hint.line == 5; } - ensure("no hints in the module", !hinted_module); - ensure("the script's own hinted", hinted_own); + UNSCOPED_INFO("no hints in the module"); + CHECK_FALSE(hinted_module); + UNSCOPED_INFO("the script's own hinted"); + CHECK(hinted_own); service.setPassedOver({}); in_module = false; for (const ALScriptSemanticToken& token : service.semanticTokens(text)) { in_module |= token.span.line >= 1 && token.span.line <= 4; } - ensure("with nothing passed over, coloured", in_module); + UNSCOPED_INFO("with nothing passed over, coloured"); + CHECK(in_module); } - template<> template<> - void alluauservice_object::test<31>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: required modules are checked on their own and not again for a script edit", "[alluauservice]") { - set_test_name("a script's requires kept as calls reach its modules, each checked on its own and not again for an edit of the script alone"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); ALLuauConfig config; service.setDocument("uses"); service.setConfig(config); @@ -945,32 +1189,36 @@ namespace tut typed |= problem.file.empty() && problem.line == 2; module_said |= problem.file == "disk:/lib/util.luau" && problem.line == 5; } - ensure("the module's types reach the script: " + said(problems), typed); - ensure("the module's own problem is the module's, in its lines: " + said(problems), module_said); + UNSCOPED_INFO("the module's types reach the script: " + said(problems)); + CHECK(typed); + UNSCOPED_INFO("the module's own problem is the module's, in its lines: " + said(problems)); + CHECK(module_said); // The script typed in: only the script is checked again. const size_t modules_before = service.modulesChecked(); service.check(script + "print(s)\n"); - ensure_equals("the script alone", service.modulesChecked() - modules_before, size_t(1)); + UNSCOPED_INFO("the script alone"); + CHECK(service.modulesChecked() - modules_before == size_t(1)); // The module changed: it, and the script that requires it. modules.modules[0].text = "local M = {}\nfunction M.twice(n: number): string\n return tostring(n * 2)\nend\nreturn M\n"; service.setModules(modules); const size_t again = service.modulesChecked(); const ALScriptProblems now = service.check(script + "print(s)\n"); - ensure_equals("both", service.modulesChecked() - again, size_t(2)); + UNSCOPED_INFO("both"); + CHECK(service.modulesChecked() - again == size_t(2)); bool still = false; for (const ALScriptProblem& problem : now) { still |= problem.line == 2 && problem.file.empty() && problem.severity == ALScriptProblem::Severity::Error; } - ensure("and what it gives now fits: " + said(now), !still); + UNSCOPED_INFO("and what it gives now fits: " + said(now)); + CHECK_FALSE(still); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<32>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the outline lists handlers as their events and timers with what is inside", "[alluauservice]") { - set_test_name("the outline lists a handler put on an event as the event, a timer's as a timer, each with what is inside it"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "LLEvents:on(\"touch_start\", function(events)\n" " local who = events[1]\n" @@ -983,21 +1231,44 @@ namespace tut std::vector outline = service.outline(script); std::string names; for (const ALScriptOutlineEntry& e : outline) names += " " + e.name + llformat("@%d", e.depth); - ensure_equals("five entries:" + names, outline.size(), size_t(5)); - ensure("the touch as an event", outline[0].name == "touch_start" && outline[0].kind == ALScriptSymbolKind::Event && outline[0].depth == 0); - ensure("named where its event is", outline[0].nameSpan.line == 0 && outline[0].nameSpan.column == 12); - ensure("spanning the call", outline[0].span.line == 0 && outline[0].span.endLine == 3); - ensure("what it holds, one deeper", outline[1].name == "greet" && outline[1].depth == 1); - ensure("once, too", outline[2].name == "listen" && outline[2].kind == ALScriptSymbolKind::Event); - ensure_equals("a timer, how often", outline[3].name, std::string("timer every 2.5")); - ensure_equals("a timer whose delay the script works out", outline[4].name, std::string("timer once")); + UNSCOPED_INFO("five entries:" + names); + REQUIRE(outline.size() == size_t(5)); + { + INFO("the touch as an event"); + CHECK(outline[0].name == "touch_start"); + CHECK(outline[0].kind == ALScriptSymbolKind::Event); + CHECK(outline[0].depth == 0); + } + { + INFO("named where its event is"); + CHECK(outline[0].nameSpan.line == 0); + CHECK(outline[0].nameSpan.column == 12); + } + { + INFO("spanning the call"); + CHECK(outline[0].span.line == 0); + CHECK(outline[0].span.endLine == 3); + } + { + INFO("what it holds, one deeper"); + CHECK(outline[1].name == "greet"); + CHECK(outline[1].depth == 1); + } + { + INFO("once, too"); + CHECK(outline[2].name == "listen"); + CHECK(outline[2].kind == ALScriptSymbolKind::Event); + } + UNSCOPED_INFO("a timer, how often"); + CHECK(outline[3].name == std::string("timer every 2.5")); + UNSCOPED_INFO("a timer whose delay the script works out"); + CHECK(outline[4].name == std::string("timer once")); } - template<> template<> - void alluauservice_object::test<33>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: every SLua template Script Studio offers checks without an error", "[alluauservice]") { - set_test_name("every template Script Studio offers for a new SLua script checks without an error"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); // Nonstrict, as the grid compiles: the new solver checking strict // cannot yet push an overloaded function's parameter types into a // function given to it, so every LLEvents:on there is an error it @@ -1007,8 +1278,13 @@ namespace tut service.setConfig(grid); llifstream in(std::string(AL_SCRIPT_TEMPLATES_DIR) + "/slua.xml", std::ios::in | std::ios::binary); LLSD templates; - ensure("read", in.is_open() && LLSDSerialize::fromXML(templates, in) != LLSDParser::PARSE_FAILURE && templates.isArray()); - ensure("some", templates.size() > 0); + { + INFO("read"); + CHECK(in.is_open()); + CHECK(LLSDSerialize::fromXML(templates, in) != LLSDParser::PARSE_FAILURE); + REQUIRE(templates.isArray()); + } + REQUIRE(templates.size() > 0); for (LLSD::array_const_iterator it = templates.beginArray(); it != templates.endArray(); ++it) { const std::string name = (*it)["name"].asString(); @@ -1021,15 +1297,15 @@ namespace tut wrong += llformat(" [%d:%d] %s", problem.line, problem.column, problem.message.c_str()); } } - ensure(name + ":" + wrong, wrong.empty()); + CAPTURE(name, wrong); + REQUIRE(wrong.empty()); } } - template<> template<> - void alluauservice_object::test<34>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the studio's own lints answer to config and --!nolint and selene and NOLINT", "[alluauservice]") { - set_test_name("the studio's own lints beside Luau's: found, off or an error as configured, turned off by --!nolint and selene's comments, and a NOLINT answers to its name"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const auto found = [&](const std::string& text) { ALScriptProblems out; for (const ALScriptProblem& p : service.check(text)) @@ -1043,49 +1319,84 @@ namespace tut }; const std::string script = "local x = 1\nx = x + 1\nlocal t = { n = \"a\" }\nt.n = t.n .. \"b\"\nx = 1 + x\nprint(x, t.n)\n"; ALScriptProblems said = found(script); - ensure_equals("two: x and t.n", said.size(), size_t(2)); - ensure("the first, a note in its words: " + said[0].message, - said[0].key == "LuauLintSlCompoundAssign" && said[0].severity == ALScriptProblem::Severity::Note && said[0].line == 1 && - said[0].args == std::vector{ "x", "+" } && said[0].source == ALScriptProblem::Source::Lint); - ensure("a field, joined: " + said[1].message, said[1].line == 3 && said[1].args == std::vector{ "t.n", ".." }); - ensure("a NOLINT answers to its name", ALScriptFixes::suppressed(said[0], "x = x + 1 -- NOLINT(SlCompoundAssign)", "", true) && - !ALScriptFixes::suppressed(said[0], "x = x + 1 -- NOLINT(LocalUnused)", "", true)); + UNSCOPED_INFO("two: x and t.n"); + REQUIRE(said.size() == size_t(2)); + { + INFO("the first, a note in its words: " + said[0].message); + CHECK(said[0].key == "LuauLintSlCompoundAssign"); + CHECK(said[0].severity == ALScriptProblem::Severity::Note); + CHECK(said[0].line == 1); + CHECK(said[0].args == std::vector{ "x", "+" }); + CHECK(said[0].source == ALScriptProblem::Source::Lint); + } + { + INFO("a field, joined: " + said[1].message); + CHECK(said[1].line == 3); + CHECK(said[1].args == std::vector{ "t.n", ".." }); + } + { + INFO("a NOLINT answers to its name"); + CHECK(ALScriptFixes::suppressed(said[0], "x = x + 1 -- NOLINT(SlCompoundAssign)", "", true)); + CHECK_FALSE(ALScriptFixes::suppressed(said[0], "x = x + 1 -- NOLINT(LocalUnused)", "", true)); + } ALLuauConfig config; config.slLints = 0; service.setConfig(config); - ensure("off", found(script).empty()); + UNSCOPED_INFO("off"); + REQUIRE(found(script).empty()); config.slLints = ALScriptLintPass::defaults(); config.slFatalLints = ALScriptLintPass::bit("SlCompoundAssign"); service.setConfig(config); said = found(script); - ensure("an error", said.size() == 2 && said[0].severity == ALScriptProblem::Severity::Error); + { + INFO("an error"); + REQUIRE(said.size() == 2); + CHECK(said[0].severity == ALScriptProblem::Severity::Error); + } config.slFatalLints = 0; config.lintErrors = true; service.setConfig(config); said = found(script); - ensure("every lint an error", said.size() == 2 && said[0].severity == ALScriptProblem::Severity::Error); + { + INFO("every lint an error"); + REQUIRE(said.size() == 2); + CHECK(said[0].severity == ALScriptProblem::Severity::Error); + } service.setConfig(ALLuauConfig()); said = found("--!nolint SlCompoundAssign\n" + script); - ensure("--!nolint by its name, and no word that Luau does not know it", said.empty()); - ensure("--!nolint alone", found("--!nolint\n" + script).empty()); + UNSCOPED_INFO("--!nolint by its name, and no word that Luau does not know it"); + REQUIRE(said.empty()); + UNSCOPED_INFO("--!nolint alone"); + REQUIRE(found("--!nolint\n" + script).empty()); said = found("--!nolint SlNope\n" + script); - ensure("a name that is none is still said", said.size() == 3 && said[0].key.rfind("LuauLintDirective", 0) == 0); + { + INFO("a name that is none is still said"); + REQUIRE(said.size() == 3); + CHECK(said[0].key.rfind("LuauLintDirective", 0) == 0); + } said = found("local x = 1\nx = x + 1 -- selene: allow(SlCompoundAssign)\nprint(x)\n"); - ensure("selene's comment beside it", said.empty()); + UNSCOPED_INFO("selene's comment beside it"); + REQUIRE(said.empty()); said = found("--# selene: deny(SlCompoundAssign)\nlocal x = 1\nx = x + 1\nprint(x)\n"); - ensure("selene's for the whole file, an error", said.size() == 1 && said[0].severity == ALScriptProblem::Severity::Error); - ensure("selene's words for the studio's own", ALSeleneFilters::slLints("SlCompoundAssign") == ALScriptLintPass::bit("SlCompoundAssign") && - ALSeleneFilters::slLints("unused_variable") == 0); + { + INFO("selene's for the whole file, an error"); + REQUIRE(said.size() == 1); + CHECK(said[0].severity == ALScriptProblem::Severity::Error); + } + { + INFO("selene's words for the studio's own"); + CHECK(ALSeleneFilters::slLints("SlCompoundAssign") == ALScriptLintPass::bit("SlCompoundAssign")); + CHECK(ALSeleneFilters::slLints("unused_variable") == 0); + } } - template<> template<> - void alluauservice_object::test<35>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: SlNumberTruth: a number used as a condition; not a number? or a comparison", "[alluauservice]") { - set_test_name("SlNumberTruth: a number read as a condition, through and, or and brackets, and under not; not number?, nor a comparison"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const auto found = [&](const std::string& text) { std::vector out; for (const ALScriptProblem& p : service.check(text)) @@ -1105,10 +1416,11 @@ namespace tut "local m: number? = nil\n" "if m then print(m) end\n" "if n ~= 0 or not (n > 1) then print(b, t) end\n"); - ensure_equals("four: " + llformat("%zu", said.size()), said.size(), size_t(4)); - ensure("each: " + said[0] + "|" + said[1] + "|" + said[2] + "|" + said[3], - said[0] == "LuauLintSlNumberTruth n 1" && said[1] == "LuauLintSlNumberTruth n 2" && said[2] == "LuauLintSlNumberTruthNot n 3" && - said[3] == "LuauLintSlNumberTruth n 4"); + REQUIRE(said.size() == size_t(4)); + CHECK(said[0] == "LuauLintSlNumberTruth n 1"); + CHECK(said[1] == "LuauLintSlNumberTruth n 2"); + CHECK(said[2] == "LuauLintSlNumberTruthNot n 3"); + CHECK(said[3] == "LuauLintSlNumberTruth n 4"); // A local by what it is given, where the old solver's nonstrict // mode says any: a copy of a number is one; one given a string, or // joined to one, or a parameter, is not. @@ -1128,15 +1440,18 @@ namespace tut { all += one + "|"; } - ensure("a copy only: " + all, given.size() == (newSolver ? 3u : 1u) && given[0] == "LuauLintSlNumberTruth copy 7" && - (!newSolver || (given[1] == "LuauLintSlStringTruth s 7" && given[2] == "LuauLintSlNumberTruth j 7"))); + { + INFO("a copy only: " + all); + REQUIRE(given.size() == (newSolver ? 3u : 1u)); + CHECK(given[0] == "LuauLintSlNumberTruth copy 7"); + CHECK((!newSolver || (given[1] == "LuauLintSlStringTruth s 7" && given[2] == "LuauLintSlNumberTruth j 7"))); + } } - template<> template<> - void alluauservice_object::test<36>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: no mode means the solver's own default; nonstrict means nonstrict in either", "[alluauservice]") { - set_test_name("a configuration that says no mode is checked in the solver's own: the new one's strict, as strict as the old one's nonstrict; one that says nonstrict is nonstrict under either"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local function f(): number? return nil end\nlocal x = f() + 1\nprint(x)\n"; const auto nil_said = [&] { for (const ALScriptProblem& p : service.check(script)) @@ -1156,21 +1471,23 @@ namespace tut const bool asked_nonstrict = nil_said(); if (newSolver) { - ensure("strict where nothing says", by_default); - ensure("nonstrict where asked", !asked_nonstrict); + UNSCOPED_INFO("strict where nothing says"); + CHECK(by_default); + UNSCOPED_INFO("nonstrict where asked"); + CHECK_FALSE(asked_nonstrict); } else { - ensure_equals("nonstrict, whether asked or not", by_default, asked_nonstrict); + UNSCOPED_INFO("nonstrict, whether asked or not"); + CHECK(by_default == asked_nonstrict); } service.setConfig(ALLuauConfig()); } - template<> template<> - void alluauservice_object::test<37>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: SlNilSentinel: finds against -1 or 0 and llcompat's against nil only", "[alluauservice]") { - set_test_name("SlNilSentinel: a find against -1, or ordered against 0 or -1, on either side, a local given one, table's and string's; llcompat's against nil; not what can hold"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const auto found = [&](const std::string& text) { std::vector out; for (const ALScriptProblem& p : service.check(text)) @@ -1210,7 +1527,8 @@ namespace tut { listed += each + "\n"; } - ensure("each, an error:\n" + listed, said == expected); + UNSCOPED_INFO("each, an error:\n" + listed); + CHECK(said == expected); // What can hold: nil against SLua's, -1 and order against // llcompat's, an order that asks where, a local given a number // too, stepped, or nil before it is given llcompat's, a @@ -1226,14 +1544,14 @@ namespace tut "m = llcompat.SubStringIndex(\"a\", \"b\")\n" "local function f(p) return p == -1 end\n" "if j == -1 or n == -1 or m == nil or ll.GetInventoryType(\"x\") == -1 then print(f(3)) end\n"); - ensure("none: " + (none.empty() ? std::string() : none[0]), none.empty()); + UNSCOPED_INFO("none: " + (none.empty() ? std::string() : none[0])); + REQUIRE(none.empty()); } - template<> template<> - void alluauservice_object::test<38>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a shape is the untyped outline from a parse alone even of a broken text", "[alluauservice]") { - set_test_name("a shape is the outline from a parse alone: the same symbols and spans, no types; a text broken in places as far as it parses; the service as it was"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "local count = 1\n" "local function half(n: number)\n" @@ -1243,23 +1561,33 @@ namespace tut "function LLEvents.touch_start(n: number) end\n"; const std::vector outline = service.outline(script); const std::vector shape = ALLuauService::shape(script); - ensure_equals("as many", shape.size(), outline.size()); + REQUIRE(shape.size() == outline.size()); for (size_t i = 0; i < shape.size(); ++i) { - ensure("the same symbol, where it was", shape[i].name == outline[i].name && shape[i].kind == outline[i].kind && shape[i].span == outline[i].span && - shape[i].depth == outline[i].depth); - ensure("no type", shape[i].detail.empty()); + CAPTURE(i); + REQUIRE(shape[i].name == outline[i].name); + REQUIRE(shape[i].kind == outline[i].kind); + REQUIRE(shape[i].span == outline[i].span); + REQUIRE(shape[i].depth == outline[i].depth); + UNSCOPED_INFO("no type"); + REQUIRE(shape[i].detail.empty()); } - ensure("the outline still typed", service.outline(script)[0].detail == "number"); + UNSCOPED_INFO("the outline still typed"); + CHECK(service.outline(script)[0].detail == "number"); const std::vector broken = ALLuauService::shape("local function a()\n x = \nend\nlocal function b()\nend\n"); - ensure("broken in a: both still there", broken.size() == 2 && broken[0].name == "a" && broken[1].name == "b" && broken[1].span.line == 3); + { + INFO("broken in a: both still there"); + REQUIRE(broken.size() == 2); + CHECK(broken[0].name == "a"); + CHECK(broken[1].name == "b"); + CHECK(broken[1].span.line == 3); + } } - template<> template<> - void alluauservice_object::test<39>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: completion says what fits and where brackets go; methods only after a colon", "[alluauservice]") { - set_test_name("completion says what fits where it goes and where a call's brackets go, and offers a method only after a colon"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const auto entry = [](const std::vector& found, const std::string& text) -> const ALScriptCompletion* { for (const ALScriptCompletion& c : found) { @@ -1276,10 +1604,17 @@ namespace tut std::vector found = service.complete(typed, 2, 18); const ALScriptCompletion* count = entry(found, "count"); const ALScriptCompletion* name = entry(found, "name"); - ensure("both offered", count && name); - ensure("the number fits", count->fits); - ensure("the string does not", !name->fits); - ensure("said where an expression goes", count->context == ALScriptCompletion::Context::Expression); + { + INFO("both offered"); + REQUIRE(count); + REQUIRE(name); + } + UNSCOPED_INFO("the number fits"); + CHECK(count->fits); + UNSCOPED_INFO("the string does not"); + CHECK_FALSE(name->fits); + UNSCOPED_INFO("said where an expression goes"); + CHECK(count->context == ALScriptCompletion::Context::Expression); // Where a call's brackets go: between them for what takes // something, after them for what takes nothing, none for what is no @@ -1292,30 +1627,57 @@ namespace tut const ALScriptCompletion* say = entry(found, "print"); const ALScriptCompletion* now = entry(found, "now"); const ALScriptCompletion* lib = entry(found, "ll"); - ensure("print, between its brackets", say && say->brackets == ALScriptCompletion::Brackets::Inside); - ensure("now, after an empty pair", now && now->brackets == ALScriptCompletion::Brackets::After); - ensure("ll, no call to say anything of", lib && lib->brackets == ALScriptCompletion::Brackets::Guess); + { + INFO("print, between its brackets"); + REQUIRE(say); + CHECK(say->brackets == ALScriptCompletion::Brackets::Inside); + } + { + INFO("now, after an empty pair"); + REQUIRE(now); + CHECK(now->brackets == ALScriptCompletion::Brackets::After); + } + { + INFO("ll, no call to say anything of"); + REQUIRE(lib); + CHECK(lib->brackets == ALScriptCompletion::Brackets::Guess); + } found = service.complete(calls, 3, 30); const ALScriptCompletion* twice = entry(found, "twice"); - ensure("twice, wanted as it is", twice && twice->fits); - ensure("so no brackets", twice->brackets == ALScriptCompletion::Brackets::None); + { + INFO("twice, wanted as it is"); + REQUIRE(twice); + CHECK(twice->fits); + } + UNSCOPED_INFO("so no brackets"); + CHECK(twice->brackets == ALScriptCompletion::Brackets::None); // A method is called with a colon, and after a dot is no use. The // grid's own declare theirs as fields taking self, which are called // with a colon all the same. const std::string events = "local a = LLEvents.\nlocal b = LLEvents:\nlocal t = {}\nfunction t:greet(): number return 1 end\nlocal d = t:\n"; found = service.complete(events, 0, 19); - ensure("none of LLEvents' methods after a dot", !entry(found, "on") && !entry(found, "off") && !entry(found, "handlers")); + { + INFO("none of LLEvents' methods after a dot"); + REQUIRE_FALSE(entry(found, "on")); + REQUIRE_FALSE(entry(found, "off")); + REQUIRE_FALSE(entry(found, "handlers")); + } found = service.complete(events, 1, 19); - ensure("all of them after a colon", entry(found, "on") && entry(found, "off") && entry(found, "handlers")); - ensure("a script's own method after a colon", entry(service.complete(events, 4, 12), "greet") != nullptr); + { + INFO("all of them after a colon"); + REQUIRE(entry(found, "on")); + REQUIRE(entry(found, "off")); + REQUIRE(entry(found, "handlers")); + } + UNSCOPED_INFO("a script's own method after a colon"); + REQUIRE(entry(service.complete(events, 4, 12), "greet") != nullptr); } - template<> template<> - void alluauservice_object::test<40>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a function an argument wants is offered as a stub to fill in", "[alluauservice]") { - set_test_name("a function an argument wants is offered written out, to fill in: Luau's own stub, and an overloaded callee's from the overload chosen"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const auto stub = [](const std::vector& found) -> const ALScriptCompletion* { for (const ALScriptCompletion& c : found) { @@ -1330,41 +1692,48 @@ namespace tut const std::string each = "local function each(n: number, f: (i: number) -> ()) end\neach(3, f)\n"; const std::vector for_each = service.complete(each, 1, 8); const ALScriptCompletion* own = stub(for_each); - ensure("Luau's stub offered", own != nullptr); - ensure_equals("its head", own->text, std::string("function(i: number)")); - ensure_equals("its body to fill, then its end", own->snippet, std::string("function(i: number)\n $0\nend")); - ensure("of the type wanted", own->fits); + UNSCOPED_INFO("Luau's stub offered"); + REQUIRE(own != nullptr); + UNSCOPED_INFO("its head"); + CHECK(own->text == std::string("function(i: number)")); + UNSCOPED_INFO("its body to fill, then its end"); + CHECK(own->snippet == std::string("function(i: number)\n $0\nend")); + UNSCOPED_INFO("of the type wanted"); + CHECK(own->fits); const std::string events = "LLEvents:on(\"touch_start\", f)\n"; const std::vector for_event = service.complete(events, 0, 27); const ALScriptCompletion* handler = stub(for_event); - ensure("the event's handler offered", handler != nullptr); - ensure_equals("taking what the event gives", handler->text, std::string("function(detected: {DetectedEvent})")); - ensure("a body to fill: " + handler->snippet, handler->snippet.find("\n $0\nend") != std::string::npos); + UNSCOPED_INFO("the event's handler offered"); + REQUIRE(handler != nullptr); + UNSCOPED_INFO("taking what the event gives"); + CHECK(handler->text == std::string("function(detected: {DetectedEvent})")); + UNSCOPED_INFO("a body to fill: " + handler->snippet); + REQUIRE(handler->snippet.find("\n $0\nend") != std::string::npos); } - template<> template<> - void alluauservice_object::test<41>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: after --! only the hot comments the studio knows are offered", "[alluauservice]") { - set_test_name("after --! the comments that say how a script is checked are offered, those the studio offers alone"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::vector found = service.complete("--!st\nlocal x = 1\n", 0, 3); std::string said; for (const ALScriptCompletion& c : found) { said += " " + c.text; - ensure("a word of the language: " + c.text, c.kind == ALScriptSymbolKind::Keyword); - ensure("in a hot comment: " + c.text, c.context == ALScriptCompletion::Context::HotComment); - ensure("one the studio offers: " + c.text, ALScriptLexicon::isLuauHotComment(c.text)); + CAPTURE(c.text); + REQUIRE(c.kind == ALScriptSymbolKind::Keyword); + REQUIRE(c.context == ALScriptCompletion::Context::HotComment); + REQUIRE(ALScriptLexicon::isLuauHotComment(c.text)); } - ensure_equals("each of them:" + said, found.size(), std::size(ALScriptLexicon::LUAU_HOT_COMMENTS)); + UNSCOPED_INFO("each of them:" + said); + REQUIRE(found.size() == std::size(ALScriptLexicon::LUAU_HOT_COMMENTS)); } - template<> template<> - void alluauservice_object::test<42>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: fragments answer completion and signature help over the statement typed", "[alluauservice]") { - set_test_name("as a script is typed, a completion and signature help are answered over the statement typed, and nothing is checked whole"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const auto offered = [](const std::vector& found, const std::string& text) -> const ALScriptCompletion* { for (const ALScriptCompletion& c : found) { @@ -1376,11 +1745,11 @@ namespace tut return nullptr; }; std::string why; - ensure("docs loaded", service.loadDocs("{\"@sl-slua/global/ll.Say\": {\"documentation\": \"Says it.\", \"learn_more_link\": \"x\"}," - " \"@sl-slua/global/ll\": {\"documentation\": \"The library.\", \"learn_more_link\": \"x\"}," - " \"@sl-slua/global/vector\": {\"documentation\": \"Makes one.\", \"learn_more_link\": \"x\"}," - " \"@sl-slua/global/vector.magnitude\": {\"documentation\": \"Its length.\", \"learn_more_link\": \"x\"}}", - why)); + CHECK(service.loadDocs("{\"@sl-slua/global/ll.Say\": {\"documentation\": \"Says it.\", \"learn_more_link\": \"x\"}," + " \"@sl-slua/global/ll\": {\"documentation\": \"The library.\", \"learn_more_link\": \"x\"}," + " \"@sl-slua/global/vector\": {\"documentation\": \"Makes one.\", \"learn_more_link\": \"x\"}," + " \"@sl-slua/global/vector.magnitude\": {\"documentation\": \"Its length.\", \"learn_more_link\": \"x\"}}", + why)); const std::string base = "local count: number = 1\n" "local function twice(n: number): number return n * 2 end\n" "local t = { a = 1, b = \"x\" }\n"; @@ -1389,71 +1758,98 @@ namespace tut service.complete(base, 0, 0); size_t checks = service.typeChecks(); service.complete(base + "local n: number = co\n", 3, 18); - ensure("off: checked whole", service.typeChecks() > checks); - ensure_equals("a call of a field documented as the field, not what holds it", service.signature(base + "ll.Say(0, )\n", 3, 10).documentation, - std::string("Says it.")); + UNSCOPED_INFO("off: checked whole"); + CHECK(service.typeChecks() > checks); + UNSCOPED_INFO("a call of a field documented as the field, not what holds it"); + CHECK(service.signature(base + "ll.Say(0, )\n", 3, 10).documentation == std::string("Says it.")); // `vector` is a table that is also called, whose fields Luau gives // no symbol of their own. - ensure_equals("and of a callable table's field", service.signature(base + "local m = vector.magnitude()\n", 3, 27).documentation, - std::string("Its length.")); + UNSCOPED_INFO("and of a callable table's field"); + CHECK(service.signature(base + "local m = vector.magnitude()\n", 3, 27).documentation == std::string("Its length.")); service.setFragments(true); - ensure("on", service.fragments()); + CHECK(service.fragments()); service.check(base); service.complete(base, 0, 0); checks = service.typeChecks(); const size_t parts = service.fragmentsChecked(); std::vector found = service.complete(base + "local n: number = co\n", 3, 18); const ALScriptCompletion* count = offered(found, "count"); - ensure("a local above offered", count != nullptr); - ensure("and fitting where a number is wanted", count->fits); - ensure("a function in scope offered", offered(found, "twice") != nullptr); - ensure("a table's fields", offered(service.complete(base + "local y = t.\n", 3, 12), "b") != nullptr); + UNSCOPED_INFO("a local above offered"); + REQUIRE(count != nullptr); + UNSCOPED_INFO("and fitting where a number is wanted"); + CHECK(count->fits); + UNSCOPED_INFO("a function in scope offered"); + REQUIRE(offered(found, "twice") != nullptr); + UNSCOPED_INFO("a table's fields"); + REQUIRE(offered(service.complete(base + "local y = t.\n", 3, 12), "b") != nullptr); found = service.complete(base + "LLEvents:\n", 3, 9); - ensure("an object's methods", offered(found, "on") && offered(found, "off")); + { + INFO("an object's methods"); + REQUIRE(offered(found, "on")); + REQUIRE(offered(found, "off")); + } ALScriptSignature said = service.signature(base + "ll.Say(0, )\n", 3, 10); - ensure("a call's signature", said.found); - ensure_equals("its label", said.label, std::string("ll.Say(channel: number, msg: string): ()")); - ensure_equals("at its second argument", said.active, 1); - ensure_equals("documented as the field it calls", said.documentation, std::string("Says it.")); + UNSCOPED_INFO("a call's signature"); + REQUIRE(said.found); + CHECK(said.label == std::string("ll.Say(channel: number, msg: string): ()")); + CHECK(said.active == 1); + UNSCOPED_INFO("documented as the field it calls"); + CHECK(said.documentation == std::string("Says it.")); said = service.signature(base + "if twice() then end\n", 3, 9); - ensure("a call in a condition", said.found && said.label.find("twice(n: number)") == 0); - - ensure("nothing offered in a comment", service.complete(base + "-- tw\n", 3, 5).empty()); - ensure("no signature outside a call", !service.signature(base + "local q = 1\n", 3, 8).found); - ensure_equals("a callable table's field over the fragment too", service.signature(base + "local m = vector.magnitude()\n", 3, 27).documentation, - std::string("Its length.")); - ensure_equals("none of it checked whole", service.typeChecks(), checks); - ensure_equals("each answered over its statement", service.fragmentsChecked() - parts, size_t(6)); + { + INFO("a call in a condition"); + REQUIRE(said.found); + CHECK(said.label.find("twice(n: number)") == 0); + } + + UNSCOPED_INFO("nothing offered in a comment"); + REQUIRE(service.complete(base + "-- tw\n", 3, 5).empty()); + UNSCOPED_INFO("no signature outside a call"); + REQUIRE_FALSE(service.signature(base + "local q = 1\n", 3, 8).found); + UNSCOPED_INFO("a callable table's field over the fragment too"); + CHECK(service.signature(base + "local m = vector.magnitude()\n", 3, 27).documentation == std::string("Its length.")); + UNSCOPED_INFO("none of it checked whole"); + CHECK(service.typeChecks() == checks); + UNSCOPED_INFO("each answered over its statement"); + CHECK(service.fragmentsChecked() - parts == size_t(6)); service.setFragments(false); } - template<> template<> - void alluauservice_object::test<43>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the whole script answers where a fragment cannot and requires survive one", "[alluauservice]") { - set_test_name("where a fragment cannot answer the whole script does; one stopped answers nothing; and the script's requires survive one"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); service.setFragments(true); // Nothing checked yet to patch: the whole script. service.setDocument("unchecked"); size_t checks = service.typeChecks(); size_t parts = service.fragmentsChecked(); const std::string base = "local count: number = 1\n"; - ensure("answered", !service.complete(base + "local x = co\n", 1, 10).empty()); - ensure("checked whole", service.typeChecks() > checks && service.fragmentsChecked() == parts); + REQUIRE_FALSE(service.complete(base + "local x = co\n", 1, 10).empty()); + { + INFO("checked whole"); + CHECK(service.typeChecks() > checks); + CHECK(service.fragmentsChecked() == parts); + } // Stopped part way: nothing, and said so; the next, unstopped, // answers over its statement. ALLuauService::Stop stop = ALLuauService::newStop(); ALLuauService::cancel(stop); service.setStop(stop); - ensure("nothing", service.complete(base + "local y = co\n", 1, 10).empty()); - ensure("stopped", service.stopped()); + REQUIRE(service.complete(base + "local y = co\n", 1, 10).empty()); + CHECK(service.stopped()); service.setStop(nullptr); checks = service.typeChecks(); - ensure("answered again", !service.complete(base + "local y = co\n", 1, 10).empty() && !service.stopped()); - ensure_equals("over its statement", service.typeChecks(), checks); + { + INFO("answered again"); + REQUIRE_FALSE(service.complete(base + "local y = co\n", 1, 10).empty()); + CHECK_FALSE(service.stopped()); + } + UNSCOPED_INFO("over its statement"); + CHECK(service.typeChecks() == checks); // Something typed above since the last check, in another block // than the line typed: Luau sets beside the last check only the @@ -1468,14 +1864,19 @@ namespace tut parts = service.fragmentsChecked(); const std::string changed = "local function twice(n: string): string return n end\nlocal function main()\n local count = 1\n local x = tw\nend\n"; const std::vector found = service.complete(changed, 3, 14); - ensure("answered", offers(found, "twice")); - ensure("whole", service.typeChecks() > checks && service.fragmentsChecked() == parts); + CHECK(offers(found, "twice")); + { + INFO("whole"); + CHECK(service.typeChecks() > checks); + CHECK(service.fragmentsChecked() == parts); + } bool string_now = false; for (const ALScriptCompletion& c : found) { string_now |= c.text == "twice" && c.detail.find("string") != std::string::npos; } - ensure("as it is now", string_now); + UNSCOPED_INFO("as it is now"); + CHECK(string_now); // A module the script requires, its requires traced when the text // was parsed: a fragment between that parse and the next check @@ -1493,18 +1894,19 @@ namespace tut service.complete(script, 1, 0); const std::string typed = script + "local n = util.\n"; service.hover(typed, 1, 8); - ensure("the module's function, over the fragment", offers(service.complete(typed, 2, 15), "twice")); + UNSCOPED_INFO("the module's function, over the fragment"); + CHECK(offers(service.complete(typed, 2, 15), "twice")); service.setFragments(false); - ensure("and the whole check after it finds it too", offers(service.complete(typed, 2, 15), "twice")); + UNSCOPED_INFO("and the whole check after it finds it too"); + CHECK(offers(service.complete(typed, 2, 15), "twice")); service.setModules({}); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<44>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: warming after a check lets the next keystroke check nothing whole", "[alluauservice]") { - set_test_name("warming after a check makes what a fragment is checked against the text's, so the next keystroke checks nothing whole"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); service.setFragments(true); service.setDocument("warmed"); const std::string base = "local count: number = 1\n"; @@ -1514,29 +1916,32 @@ namespace tut service.check(base); size_t checks = service.typeChecks(); service.complete(typed, 1, 18); - ensure_equals("unwarmed", service.typeChecks() - checks, size_t(newSolver ? 0 : 1)); + UNSCOPED_INFO("unwarmed"); + CHECK(service.typeChecks() - checks == size_t(newSolver ? 0 : 1)); service.check(base); checks = service.typeChecks(); service.warm(base); - ensure_equals("the old solver's autocomplete module checked; the new solver's check is it", service.typeChecks() - checks, - size_t(newSolver ? 0 : 1)); + UNSCOPED_INFO("the old solver's autocomplete module checked; the new solver's check is it"); + CHECK(service.typeChecks() - checks == size_t(newSolver ? 0 : 1)); service.warm(base); - ensure_equals("and not again for the same text", service.typeChecks() - checks, size_t(newSolver ? 0 : 1)); + UNSCOPED_INFO("and not again for the same text"); + CHECK(service.typeChecks() - checks == size_t(newSolver ? 0 : 1)); checks = service.typeChecks(); const size_t parts = service.fragmentsChecked(); - ensure("answered", offers(service.complete(typed, 1, 18), "count")); - ensure_equals("over its statement", service.fragmentsChecked() - parts, size_t(1)); - ensure_equals("nothing whole", service.typeChecks(), checks); + CHECK(offers(service.complete(typed, 1, 18), "count")); + UNSCOPED_INFO("over its statement"); + CHECK(service.fragmentsChecked() - parts == size_t(1)); + UNSCOPED_INFO("nothing whole"); + CHECK(service.typeChecks() == checks); service.setFragments(false); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<45>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: several modules required are checked in parallel once each", "[alluauservice]") { - set_test_name("a script requiring several modules has them checked together, each on a thread of its own as what it requires is checked: the same problems, each module checked once, and not again for an edit of the script alone"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); service.setDocument("several"); service.setConfig(ALLuauConfig()); ALLuauService::Modules modules; @@ -1555,7 +1960,8 @@ namespace tut script += "local s: string = m1.f(1) + m2.f(2) + m3.f(3) + m4.f(4)\n"; const size_t before = service.modulesChecked(); const ALScriptProblems problems = service.check(script); - ensure_equals("each module and the script checked once", service.modulesChecked() - before, size_t(5)); + UNSCOPED_INFO("each module and the script checked once"); + CHECK(service.modulesChecked() - before == size_t(5)); S32 in_modules = 0; bool own = false; for (const ALScriptProblem& problem : problems) @@ -1563,24 +1969,27 @@ namespace tut in_modules += problem.file.rfind("disk:/lib/m", 0) == 0 && problem.line == 2 && problem.source == ALScriptProblem::Source::Types ? 1 : 0; own |= problem.file.empty() && problem.line == 5 && problem.severity == ALScriptProblem::Severity::Error; } - ensure_equals("each module's own type error, in its lines: " + said(problems), in_modules, 4); - ensure("and the script's: " + said(problems), own); + UNSCOPED_INFO("each module's own type error, in its lines: " + said(problems)); + CHECK(in_modules == 4); + UNSCOPED_INFO("and the script's: " + said(problems)); + CHECK(own); // The script typed in: only it, on its own. const size_t again = service.modulesChecked(); const ALScriptProblems edited = service.check(script + "print(s)\n"); - ensure_equals("the script alone", service.modulesChecked() - again, size_t(1)); - ensure("its own problem still: " + said(edited), std::any_of(edited.begin(), edited.end(), [](const ALScriptProblem& problem) { + UNSCOPED_INFO("the script alone"); + CHECK(service.modulesChecked() - again == size_t(1)); + UNSCOPED_INFO("its own problem still: " + said(edited)); + REQUIRE(std::any_of(edited.begin(), edited.end(), [](const ALScriptProblem& problem) { return problem.file.empty() && problem.line == 5; })); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<46>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a required module's lints are told in its lines without fixes and only once", "[alluauservice]") { - set_test_name("a module checked with the script has its lints told too, Luau's and the studio's own, in its lines and with no fix, while the script's keep theirs; not again for an edit of the script alone"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); service.setDocument("linted"); service.setConfig(ALLuauConfig()); ALLuauService::Modules modules; @@ -1608,23 +2017,33 @@ namespace tut return nullptr; }; const ALScriptProblem* unused = in(problems, "disk:/lib/util.luau", "LocalUnused", 0); - ensure("Luau's lint of the module, in its lines: " + said(problems), unused != nullptr); + UNSCOPED_INFO("Luau's lint of the module, in its lines: " + said(problems)); + REQUIRE(unused != nullptr); const ALScriptProblem* compound = in(problems, "disk:/lib/util.luau", "SlCompoundAssign", 2); - ensure("the studio's own of the module: " + said(problems), compound != nullptr); - ensure("neither with a fix, being another file's", unused->fixes.empty() && compound->fixes.empty()); + UNSCOPED_INFO("the studio's own of the module: " + said(problems)); + REQUIRE(compound != nullptr); + { + INFO("neither with a fix, being another file's"); + REQUIRE(unused->fixes.empty()); + REQUIRE(compound->fixes.empty()); + } const ALScriptProblem* own = in(problems, "", "SlCompoundAssign", 2); - ensure("the script's own, with its fix: " + said(problems), own && !own->fixes.empty()); + { + INFO("the script's own, with its fix: " + said(problems)); + REQUIRE(own); + REQUIRE_FALSE(own->fixes.empty()); + } const ALScriptProblems edited = service.check(script + "print(util)\n"); - ensure("an edit of the script alone: not the module's again", !in(edited, "disk:/lib/util.luau", "LocalUnused", 0)); + UNSCOPED_INFO("an edit of the script alone: not the module's again"); + REQUIRE_FALSE(in(edited, "disk:/lib/util.luau", "LocalUnused", 0)); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<47>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a required module's exports are kept from its type in declaration order", "[alluauservice]") { - set_test_name("a module checked with the script has what it exports kept as its type says, in the order its fields were declared, for what is offered from it"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); service.setDocument("exports"); service.setConfig(ALLuauConfig()); const std::string made = "--!strict\n" @@ -1641,14 +2060,17 @@ namespace tut service.setModules(modules); service.check("local made = require(\"made\")\nprint(made.alpha)\n"); const std::optional> kept = ALLuauExports::checkedOf("disk:/lib/made.luau", made); - ensure("kept as its type says", kept.has_value()); + UNSCOPED_INFO("kept as its type says"); + REQUIRE(kept.has_value()); std::string listed; for (const std::string& name : *kept) { listed += " " + name; } - ensure("each field, in order:" + listed, *kept == std::vector({ "alpha", "beta" })); - ensure("not for another text", !ALLuauExports::checkedOf("disk:/lib/made.luau", made + "\n")); + UNSCOPED_INFO("each field, in order:" + listed); + CHECK(*kept == std::vector({ "alpha", "beta" })); + UNSCOPED_INFO("not for another text"); + REQUIRE_FALSE(ALLuauExports::checkedOf("disk:/lib/made.luau", made + "\n")); // Its type no table, as a nonstrict check may leave one: nothing // kept, and its text alone says what it exports. @@ -1657,15 +2079,15 @@ namespace tut modules.reaches.push_back({ "", "loose", "disk:/lib/loose.luau" }); service.setModules(modules); service.check("local made = require(\"made\")\nlocal loose = require(\"loose\")\nprint(made.alpha, loose)\n"); - ensure("no table, nothing kept", !ALLuauExports::checkedOf("disk:/lib/loose.luau", loose)); + UNSCOPED_INFO("no table, nothing kept"); + REQUIRE_FALSE(ALLuauExports::checkedOf("disk:/lib/loose.luau", loose)); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<48>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a module returning other than one value is an error at the require of it", "[alluauservice]") { - set_test_name("a module that returns other than one value is said at the require that asks for it, as Luau's require would stop there: what the bundle need not check as it runs"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); for (const char* mode : { "", "nonstrict", "strict" }) { ALLuauConfig config; @@ -1697,21 +2119,24 @@ namespace tut in_mid |= problem.file == "disk:/lib/mid.luau" && problem.line == 0 && problem.key == "LuauModuleNotOneValue"; } const std::string where = std::string(mode) + ": " + said(problems); - ensure("two values: said at its require, " + where, two); - ensure("no value: said at its require, " + where, none); - ensure("one: nothing, " + where, !one); + UNSCOPED_INFO("two values: said at its require, " + where); + REQUIRE(two); + UNSCOPED_INFO("no value: said at its require, " + where); + REQUIRE(none); + UNSCOPED_INFO("one: nothing, " + where); + REQUIRE_FALSE(one); // Told as the module is checked, as all of a module's are: the // first time, its text unchanged after. - ensure("a module's require of it: said in that module, " + where, in_mid || *mode); + UNSCOPED_INFO("a module's require of it: said in that module, " + where); + REQUIRE((in_mid || *mode)); } service.setDocument(""); } - template<> template<> - void alluauservice_object::test<49>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a module a question checked first is told at the next check once", "[alluauservice]") { - set_test_name("a module a question checked before the script's check is told at that check all the same -- its lints, a require in it of what returns two values, what it exports -- and once"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); service.setDocument("asked first"); service.setConfig(ALLuauConfig()); const std::string util = "--!strict\n" // 0 @@ -1738,22 +2163,29 @@ namespace tut }); }; const ALScriptProblems problems = service.check(script); - ensure_equals("the check found the script checked", service.typeChecks(), checks); - ensure("the module's lint: " + said(problems), told(problems, "LocalUnused", 1)); - ensure("its require of what returns two values: " + said(problems), told(problems, "LuauModuleNotOneValue", 2)); + UNSCOPED_INFO("the check found the script checked"); + CHECK(service.typeChecks() == checks); + UNSCOPED_INFO("the module's lint: " + said(problems)); + CHECK(told(problems, "LocalUnused", 1)); + UNSCOPED_INFO("its require of what returns two values: " + said(problems)); + CHECK(told(problems, "LuauModuleNotOneValue", 2)); const std::optional> kept = ALLuauExports::checkedOf("disk:/lib/asked.luau", util); - ensure("what it exports, as its type says", kept && std::find(kept->begin(), kept->end(), "n") != kept->end()); + { + INFO("what it exports, as its type says"); + REQUIRE(kept); + REQUIRE(std::find(kept->begin(), kept->end(), "n") != kept->end()); + } // Told, and so not told again for the same text. const ALScriptProblems again = service.check(script); - ensure("once: " + said(again), !told(again, "LocalUnused", 1)); + UNSCOPED_INFO("once: " + said(again)); + CHECK_FALSE(told(again, "LocalUnused", 1)); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<50>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: hover on a type says it; only the script's own has a declaration to go to", "[alluauservice]") { - set_test_name("a hover on the name of a type the definitions declare says it, with nowhere in the script to go -- its place is a line of their file -- while the script's own goes where it declares it"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string script = "--!strict\n" // 0 "local id: uuid = ll.GetOwner()\n" // 1 "local r: rotation = ll.GetRot()\n" // 2 @@ -1761,20 +2193,35 @@ namespace tut "local p: Pair = { a = 1 }\n" // 4 "print(id, r, p)\n"; // 5 ALScriptHover hover = service.hover(script, 1, 11); // `uuid`, an extern type - ensure("uuid, said: " + hover.label, hover.found && hover.label.rfind("type uuid", 0) == 0); - ensure("nowhere to go", !hover.hasDefinition); + { + INFO("uuid, said: " + hover.label); + REQUIRE(hover.found); + CHECK(hover.label.rfind("type uuid", 0) == 0); + } + UNSCOPED_INFO("nowhere to go"); + CHECK_FALSE(hover.hasDefinition); hover = service.hover(script, 2, 10); // `rotation`, an alias - ensure("rotation, said: " + hover.label, hover.found && hover.label.rfind("type rotation", 0) == 0); - ensure("nowhere to go either", !hover.hasDefinition); + { + INFO("rotation, said: " + hover.label); + REQUIRE(hover.found); + CHECK(hover.label.rfind("type rotation", 0) == 0); + } + UNSCOPED_INFO("nowhere to go either"); + CHECK_FALSE(hover.hasDefinition); hover = service.hover(script, 4, 10); // `Pair` - ensure("the script's own, where it declares it", hover.found && hover.hasDefinition && hover.definitionFile.empty() && hover.definitionLine == 3); + { + INFO("the script's own, where it declares it"); + REQUIRE(hover.found); + CHECK(hover.hasDefinition); + REQUIRE(hover.definitionFile.empty()); + CHECK(hover.definitionLine == 3); + } } - template<> template<> - void alluauservice_object::test<51>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a module's exports leave out keyword-named fields by type as by text", "[alluauservice]") { - set_test_name("what a module checked exports, as its type says, leaves out a field no script could write after a dot, as what its text says does"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); service.setDocument("keywords"); service.setConfig(ALLuauConfig()); const std::string util = "--!strict\nreturn { [\"end\"] = 1, ok = 2, [\"not\"] = 3, export = 4 }\n"; @@ -1784,17 +2231,19 @@ namespace tut service.setModules(modules); service.check("local util = require(\"util\")\nprint(util.ok)\n"); const std::optional> kept = ALLuauExports::checkedOf("disk:/lib/keywords.luau", util); - ensure("kept as its type says", kept.has_value()); - ensure("the names, a word only a statement begins with among them, and no keyword", *kept == std::vector({ "ok", "export" })); - ensure("as its text alone says", ALLuauExports::of(util) == std::vector({ "ok", "export" })); + UNSCOPED_INFO("kept as its type says"); + REQUIRE(kept.has_value()); + UNSCOPED_INFO("the names, a word only a statement begins with among them, and no keyword"); + CHECK(*kept == std::vector({ "ok", "export" })); + UNSCOPED_INFO("as its text alone says"); + CHECK(ALLuauExports::of(util) == std::vector({ "ok", "export" })); service.setDocument(""); } - template<> template<> - void alluauservice_object::test<52>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: a module's lints follow the configuration of the script requiring it", "[alluauservice]") { - set_test_name("a module's lints are told as the configuration of the script that requires it says, a module having none of its own: one it turns off not told, one it makes an error an error, every one where all are, Luau's and the studio's own alike"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const std::string module = "local unusedThing = 1\n" // 0 "local function helper() end\n" // 1 "local n = 1\n" // 2 @@ -1828,19 +2277,36 @@ namespace tut some.lints &= ~ALLuauConfig::lintBit("LocalUnused"); some.fatalLints |= ALLuauConfig::lintBit("FunctionUnused"); const ALScriptProblems problems = checked("some", "disk:/lib/some.luau", some); - ensure("one it turns off, not told: " + said(problems), !told(problems, "LocalUnused")); + UNSCOPED_INFO("one it turns off, not told: " + said(problems)); + REQUIRE_FALSE(told(problems, "LocalUnused")); const ALScriptProblem* fatal = told(problems, "FunctionUnused"); - ensure("one it makes an error, an error: " + said(problems), fatal && fatal->severity == ALScriptProblem::Severity::Error); + { + INFO("one it makes an error, an error: " + said(problems)); + REQUIRE(fatal); + CHECK(fatal->severity == ALScriptProblem::Severity::Error); + } const ALScriptProblem* compound = told(problems, "SlCompoundAssign"); - ensure("the studio's own as it says, no error: " + said(problems), compound && compound->severity != ALScriptProblem::Severity::Error); + { + INFO("the studio's own as it says, no error: " + said(problems)); + REQUIRE(compound); + CHECK(compound->severity != ALScriptProblem::Severity::Error); + } ALLuauConfig all; all.lintErrors = true; const ALScriptProblems errors = checked("all", "disk:/lib/all.luau", all); const ALScriptProblem* unused = told(errors, "LocalUnused"); - ensure("every one an error, Luau's: " + said(errors), unused && unused->severity == ALScriptProblem::Severity::Error); + { + INFO("every one an error, Luau's: " + said(errors)); + REQUIRE(unused); + CHECK(unused->severity == ALScriptProblem::Severity::Error); + } compound = told(errors, "SlCompoundAssign"); - ensure("and the studio's own: " + said(errors), compound && compound->severity == ALScriptProblem::Severity::Error); + { + INFO("and the studio's own: " + said(errors)); + REQUIRE(compound); + CHECK(compound->severity == ALScriptProblem::Severity::Error); + } service.setDocument(""); } @@ -1871,11 +2337,41 @@ namespace tut return false; } - template<> template<> - void alluauservice_object::test<71>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: LLEvents:on shows the form its event name fits and the first with no name", "[alluauservice]") { - set_test_name("the constants the grid's VM sets and the definitions leave out are known, as numbers: none an unknown global, in either mode"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); + const std::string script = "LLEvents:on(\"listen\", function(channel, name, id, msg) end)\n" // 0 + "LLEvents:on(\"touch_start\", function(detected) end)\n" // 1 + "LLEvents:once(\"timer\", function() end)\n" // 2 + "LLEvents:on(\n"; // 3 + const auto shown = [&](S32 line, S32 column) { + const ALScriptSignature sig = service.signature(script, line, column); + return sig.found && sig.overloads.size() == 2 ? sig.overload : -1; + }; + const ALScriptSignature listen = service.signature(script, 0, 24); + REQUIRE(listen.found); + REQUIRE(listen.overloads.size() == 2); + { + INFO("listen, not a detected event: " + listen.label); + CHECK(listen.overload == 1); + REQUIRE(listen.parameters.size() == 2); + REQUIRE(listen.parameters[1].find("Detected") == std::string::npos); + } + UNSCOPED_INFO("touch_start, a detected event"); + CHECK(shown(1, 29) == 0); + UNSCOPED_INFO("once too"); + CHECK(shown(2, 25) == 1); + UNSCOPED_INFO("in the name, as it is written"); + CHECK(shown(0, 14) == 1); + UNSCOPED_INFO("no name yet"); + CHECK(shown(3, 12) == 0); + } + + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the grid's undocumented constants are known as numbers in either mode", "[alluauservice]") + { + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); std::string read = "print("; std::string summed = "--!strict\nlocal total: number = 0"; for (const char* name : UNDOCUMENTED_CONSTANTS) @@ -1888,91 +2384,82 @@ namespace tut ALScriptProblems problems = service.check(read); for (const char* name : UNDOCUMENTED_CONSTANTS) { - ensure(std::string("known: ") + name + "\n" + said(problems), !unknownGlobal(problems, name)); + UNSCOPED_INFO(std::string("known: ") + name + "\n" + said(problems)); + REQUIRE_FALSE(unknownGlobal(problems, name)); } - ensure_equals("no errors: " + said(problems), alluauservice_data::errors(problems), size_t(0)); + UNSCOPED_INFO("no errors: " + said(problems)); + CHECK(alluauservice_data::errors(problems) == size_t(0)); problems = service.check(summed); - ensure_equals("each a number, strictly: " + said(problems), alluauservice_data::errors(problems), size_t(0)); + UNSCOPED_INFO("each a number, strictly: " + said(problems)); + CHECK(alluauservice_data::errors(problems) == size_t(0)); } - template<> template<> - void alluauservice_object::test<72>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: TRUE and FALSE are still unknown globals", "[alluauservice]") { - set_test_name("TRUE and FALSE are still unknown globals: the VM sets every builtins.txt constant but those two, which SLua says as true and false"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); const ALScriptProblems problems = service.check("print(PRIM_MATERIAL_DENSITY, TRUE, FALSE)\n"); - ensure("TRUE unknown: " + said(problems), unknownGlobal(problems, "TRUE")); - ensure("FALSE unknown: " + said(problems), unknownGlobal(problems, "FALSE")); - ensure("the constant beside them known: " + said(problems), !unknownGlobal(problems, "PRIM_MATERIAL_DENSITY")); + UNSCOPED_INFO("TRUE unknown: " + said(problems)); + CHECK(unknownGlobal(problems, "TRUE")); + UNSCOPED_INFO("FALSE unknown: " + said(problems)); + CHECK(unknownGlobal(problems, "FALSE")); + UNSCOPED_INFO("the constant beside them known: " + said(problems)); + CHECK_FALSE(unknownGlobal(problems, "PRIM_MATERIAL_DENSITY")); } - template<> template<> - void alluauservice_object::test<73>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: completion leaves out the undocumented constants but not a script's own", "[alluauservice]") { - set_test_name("completion does not offer the undocumented constants, as the grid's own lists do not, but offers the documented ones beside them and a script's own of the same name"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); std::string script = "print(PRIM_MATERIAL_)\n"; std::vector found = service.complete(script, 0, 20); - ensure("a documented one offered", alluauservice_data::offers(found, "PRIM_MATERIAL_FLESH")); - ensure("an undocumented one not", !alluauservice_data::offers(found, "PRIM_MATERIAL_DENSITY")); + UNSCOPED_INFO("a documented one offered"); + CHECK(alluauservice_data::offers(found, "PRIM_MATERIAL_FLESH")); + UNSCOPED_INFO("an undocumented one not"); + CHECK_FALSE(alluauservice_data::offers(found, "PRIM_MATERIAL_DENSITY")); script = "print(SKY_)\n"; found = service.complete(script, 0, 10); - ensure("nor its kin", !alluauservice_data::offers(found, "SKY_MIE_CONFIG") && !alluauservice_data::offers(found, "SKY_RAYLEIGH_CONFIG")); + { + INFO("nor its kin"); + CHECK_FALSE(alluauservice_data::offers(found, "SKY_MIE_CONFIG")); + CHECK_FALSE(alluauservice_data::offers(found, "SKY_RAYLEIGH_CONFIG")); + } script = "local REZ_TORQUE = 1\nprint(REZ_)\n"; found = service.complete(script, 1, 10); - ensure("a script's own of the name offered", alluauservice_data::offers(found, "REZ_TORQUE")); + UNSCOPED_INFO("a script's own of the name offered"); + CHECK(alluauservice_data::offers(found, "REZ_TORQUE")); } - template<> template<> - void alluauservice_object::test<74>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: definitions declaring an undocumented constant win and the rest stay known", "[alluauservice]") { - set_test_name("where the definitions declare one of the undocumented constants themselves, theirs stands, and the rest are still known"); ALLuauService own; std::string why; own.setNewSolver(newSolver, why); - ensure("loads: " + why, own.loadDefinitions(definitions + "\ndeclare REZ_TORQUE: string\n", why)); + UNSCOPED_INFO("loads: " + why); + CHECK(own.loadDefinitions(definitions + "\ndeclare REZ_TORQUE: string\n", why)); const ALScriptProblems problems = own.check("--!strict\nlocal s: string = REZ_TORQUE\nlocal n: number = SKY_MIE_CONFIG\nprint(s, n)\n"); - ensure_equals("theirs, a string; ours, a number: " + said(problems), alluauservice_data::errors(problems), size_t(0)); + UNSCOPED_INFO("theirs, a string; ours, a number: " + said(problems)); + CHECK(alluauservice_data::errors(problems) == size_t(0)); } - template<> template<> - void alluauservice_object::test<75>() + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the undocumented constants survive a solver change and hover as numbers", "[alluauservice]") { - set_test_name("the undocumented constants come back with the definitions when the solver changes, and hover says a number"); - ensure("definitions loaded: " + error, loaded); + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); std::string why; - ensure("the other solver: " + why, service.setNewSolver(!newSolver, why)); + UNSCOPED_INFO("the other solver"); + REQUIRE(service.setNewSolver(!newSolver, why)); ALScriptProblems problems = service.check("print(REZ_TORQUE)\n"); - ensure("known there: " + said(problems), !unknownGlobal(problems, "REZ_TORQUE")); - ensure("and back: " + why, service.setNewSolver(newSolver, why)); + UNSCOPED_INFO("known there: " + said(problems)); + CHECK_FALSE(unknownGlobal(problems, "REZ_TORQUE")); + UNSCOPED_INFO("and back: " + why); + CHECK(service.setNewSolver(newSolver, why)); problems = service.check("print(REZ_TORQUE)\n"); - ensure("known again: " + said(problems), !unknownGlobal(problems, "REZ_TORQUE")); + UNSCOPED_INFO("known again: " + said(problems)); + CHECK_FALSE(unknownGlobal(problems, "REZ_TORQUE")); const ALScriptHover hover = service.hover("print(REZ_TORQUE)\n", 0, 8); - ensure("hover finds it", hover.found); - ensure("a number: " + hover.label, hover.label.find("number") != std::string::npos); - } - - template<> template<> - void alluauservice_object::test<53>() - { - set_test_name("LLEvents:on's signature is the form its event's name fits: a detected event's handler given the detected table, another's " - "its own arguments; the first form where no name is written yet"); - ensure("definitions loaded: " + error, loaded); - const std::string script = "LLEvents:on(\"listen\", function(channel, name, id, msg) end)\n" // 0 - "LLEvents:on(\"touch_start\", function(detected) end)\n" // 1 - "LLEvents:once(\"timer\", function() end)\n" // 2 - "LLEvents:on(\n"; // 3 - const auto shown = [&](S32 line, S32 column) { - const ALScriptSignature sig = service.signature(script, line, column); - return sig.found && sig.overloads.size() == 2 ? sig.overload : -1; - }; - const ALScriptSignature listen = service.signature(script, 0, 24); - ensure("found, both forms", listen.found && listen.overloads.size() == 2); - ensure("listen, not a detected event: " + listen.label, listen.overload == 1 && listen.parameters.size() == 2 && - listen.parameters[1].find("Detected") == std::string::npos); - ensure_equals("touch_start, a detected event", shown(1, 29), 0); - ensure_equals("once too", shown(2, 25), 1); - ensure_equals("in the name, as it is written", shown(0, 14), 1); - ensure_equals("no name yet", shown(3, 12), 0); + REQUIRE(hover.found); + UNSCOPED_INFO("a number: " + hover.label); + REQUIRE(hover.label.find("number") != std::string::npos); } } diff --git a/indra/alscript/tests/almasterburst_test.cpp b/indra/alscript/tests/almasterburst_test.cpp index c82b2e2d27..a28a2b9e25 100644 --- a/indra/alscript/tests/almasterburst_test.cpp +++ b/indra/alscript/tests/almasterburst_test.cpp @@ -26,7 +26,7 @@ #include "../masters/almasterburst.h" -#include "../test/lltut.h" +#include #include #include @@ -34,7 +34,7 @@ #include #include -namespace tut +namespace { struct almasterburst_data { @@ -52,39 +52,47 @@ namespace tut static F64 before(F64 time) { return std::nextafter(time, -std::numeric_limits::infinity()); } }; - typedef test_group almasterburst_group; - typedef almasterburst_group::object almasterburst_object; - almasterburst_group almasterburst_instance("almasterburst"); - - template<> template<> - void almasterburst_object::test<1>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: a single file goes once only after the quiet time with nothing changed", "[almasterburst]") { - set_test_name("a single file goes once nothing has changed for the quiet time, and once only"); ALMasterBurst burst; - ensure("nothing waits at first", burst.empty() && !burst.dueAt()); - ensure("and nothing goes", burst.release(100.0).empty()); + { + INFO("nothing waits at first"); + REQUIRE(burst.empty()); + REQUIRE_FALSE(burst.dueAt()); + } + UNSCOPED_INFO("and nothing goes"); + REQUIRE(burst.release(100.0).empty()); burst.changed("/s/door.lsl", 100.0); - ensure("it waits", burst.waiting("/s/door.lsl") && burst.size() == 1); - ensure_equals("due a second on", *burst.dueAt(), 101.0); - ensure("not before", burst.release(100.5).empty() && burst.release(almasterburst_data::before(101.0)).empty()); - ensure_equals("then", almasterburst_data::joined(burst.release(101.0)), std::string("/s/door.lsl")); - ensure("and is gone", burst.empty() && !burst.dueAt()); - ensure("not given twice", burst.release(200.0).empty()); + CHECK(burst.waiting("/s/door.lsl")); + REQUIRE(burst.size() == 1); + UNSCOPED_INFO("due a second on"); + CHECK(*burst.dueAt() == 101.0); + { + INFO("not before"); + REQUIRE(burst.release(100.5).empty()); + REQUIRE(burst.release(almasterburst_data::before(101.0)).empty()); + } + CHECK(almasterburst_data::joined(burst.release(101.0)) == std::string("/s/door.lsl")); + REQUIRE(burst.empty()); + REQUIRE_FALSE(burst.dueAt()); + UNSCOPED_INFO("not given twice"); + REQUIRE(burst.release(200.0).empty()); // A quiet time set shorter, and one less than nothing. burst.setQuiet(0.25); burst.changed("/s/door.lsl", 300.0); - ensure_equals("the quiet time set", *burst.dueAt(), 300.25); + UNSCOPED_INFO("the quiet time set"); + CHECK(*burst.dueAt() == 300.25); burst.setQuiet(-1.0); - ensure_equals("none at all is no wait", *burst.dueAt(), 300.0); - ensure_equals("and goes at once", almasterburst_data::joined(burst.release(300.0)), std::string("/s/door.lsl")); + UNSCOPED_INFO("none at all is no wait"); + CHECK(*burst.dueAt() == 300.0); + UNSCOPED_INFO("and goes at once"); + CHECK(almasterburst_data::joined(burst.release(300.0)) == std::string("/s/door.lsl")); } - template<> template<> - void almasterburst_object::test<2>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: a checkout of thirty files over 0.8 s is let go together once", "[almasterburst]") { - set_test_name("a checkout of thirty files over 0.8 s is let go together, once"); ALMasterBurst burst(1.0, 10.0); std::string expected; F64 last = 0.0; @@ -95,19 +103,22 @@ namespace tut burst.changed(path, last); expected += (expected.empty() ? "" : " ") + path; // Looked at as each is heard: the burst goes on. - ensure("none while it goes on", burst.release(last).empty()); + UNSCOPED_INFO("none while it goes on"); + REQUIRE(burst.release(last).empty()); } - ensure_equals("all thirty wait", burst.size(), size_t(30)); - ensure_equals("due a second after the last", *burst.dueAt(), last + 1.0); - ensure("not a moment before", burst.release(almasterburst_data::before(last + 1.0)).empty()); - ensure_equals("all of them, in the order heard", almasterburst_data::joined(burst.release(last + 1.0)), expected); - ensure("nothing left", burst.empty() && burst.release(last + 60.0).empty()); + REQUIRE(burst.size() == size_t(30)); + UNSCOPED_INFO("due a second after the last"); + CHECK(*burst.dueAt() == last + 1.0); + UNSCOPED_INFO("not a moment before"); + REQUIRE(burst.release(almasterburst_data::before(last + 1.0)).empty()); + UNSCOPED_INFO("all of them, in the order heard"); + CHECK(almasterburst_data::joined(burst.release(last + 1.0)) == expected); + REQUIRE(burst.empty()); + REQUIRE(burst.release(last + 60.0).empty()); } - template<> template<> - void almasterburst_object::test<3>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: a file rewritten every half-second still goes at the burst's longest", "[almasterburst]") { - set_test_name("a file rewritten every half-second still goes, once the burst has gone on its longest"); ALMasterBurst burst(1.0, 10.0); std::vector gone; for (int n = 0; n <= 50; ++n) @@ -116,73 +127,90 @@ namespace tut burst.changed("/s/build/out.lsl", now); if (n == 6) { - ensure_equals("due when quiet, while that is sooner", *burst.dueAt(), 4.0); + UNSCOPED_INFO("due when quiet, while that is sooner"); + REQUIRE(*burst.dueAt() == 4.0); } if (!burst.release(now).empty()) { gone.push_back(now); } } - ensure_equals("let go twice in 25 s", gone.size(), size_t(2)); - ensure_equals("first at ten seconds", gone[0], 10.0); + UNSCOPED_INFO("let go twice in 25 s"); + REQUIRE(gone.size() == size_t(2)); + UNSCOPED_INFO("first at ten seconds"); + CHECK(gone[0] == 10.0); // The next change began a burst of its own. - ensure_equals("then ten seconds after the change that followed", gone[1], 20.5); - ensure_equals("and once more due when that one is over", *burst.dueAt(), 25.0 + 1.0); + UNSCOPED_INFO("then ten seconds after the change that followed"); + CHECK(gone[1] == 20.5); + UNSCOPED_INFO("and once more due when that one is over"); + CHECK(*burst.dueAt() == 25.0 + 1.0); } - template<> template<> - void almasterburst_object::test<4>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: a file held past the quiet time stays and the others go first", "[almasterburst]") { - set_test_name("a file held past the quiet time stays, and the others go first"); ALMasterBurst burst(1.0, 10.0); burst.changed("/s/a.lsl", 0.0); // Found emptied: held a second and a half. burst.changed("/s/emptied.lsl", 0.25, 1.5); burst.changed("/s/b.lsl", 0.5); - ensure_equals("due when quiet", *burst.dueAt(), 1.5); - ensure_equals("the others go", almasterburst_data::joined(burst.release(1.5)), std::string("/s/a.lsl /s/b.lsl")); - ensure("the held one waits", burst.waiting("/s/emptied.lsl") && burst.size() == 1); - ensure_equals("due when its hold is over", *burst.dueAt(), 1.75); - ensure("not before", burst.release(almasterburst_data::before(1.75)).empty()); - ensure_equals("then it goes", almasterburst_data::joined(burst.release(1.75)), std::string("/s/emptied.lsl")); + UNSCOPED_INFO("due when quiet"); + CHECK(*burst.dueAt() == 1.5); + UNSCOPED_INFO("the others go"); + CHECK(almasterburst_data::joined(burst.release(1.5)) == std::string("/s/a.lsl /s/b.lsl")); + { + INFO("the held one waits"); + CHECK(burst.waiting("/s/emptied.lsl")); + REQUIRE(burst.size() == 1); + } + UNSCOPED_INFO("due when its hold is over"); + CHECK(*burst.dueAt() == 1.75); + UNSCOPED_INFO("not before"); + REQUIRE(burst.release(almasterburst_data::before(1.75)).empty()); + UNSCOPED_INFO("then it goes"); + CHECK(almasterburst_data::joined(burst.release(1.75)) == std::string("/s/emptied.lsl")); // Held, but the burst it is in goes on: it waits for both. burst.changed("/s/emptied.lsl", 10.0, 1.5); burst.changed("/s/c.lsl", 11.0); - ensure_equals("past its hold, the burst not over", *burst.dueAt(), 12.0); - ensure_equals("together then", almasterburst_data::joined(burst.release(12.0)), std::string("/s/emptied.lsl /s/c.lsl")); + UNSCOPED_INFO("past its hold, the burst not over"); + CHECK(*burst.dueAt() == 12.0); + UNSCOPED_INFO("together then"); + CHECK(almasterburst_data::joined(burst.release(12.0)) == std::string("/s/emptied.lsl /s/c.lsl")); } - template<> template<> - void almasterburst_object::test<5>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: a refilled file loses its hold and keeps its place; the last hold counts", "[almasterburst]") { - set_test_name("an emptied file filled again is held no longer, and keeps its place; the hold given last counts"); ALMasterBurst burst(1.0, 10.0); burst.changed("/s/a.lsl", 0.0, 1.5); burst.changed("/s/b.lsl", 0.125); burst.changed("/s/a.lsl", 0.25); - ensure_equals("due when quiet, the hold gone", *burst.dueAt(), 1.25); - ensure_equals("both, the refilled one in its first place", almasterburst_data::joined(burst.release(1.25)), + UNSCOPED_INFO("due when quiet, the hold gone"); + CHECK(*burst.dueAt() == 1.25); + UNSCOPED_INFO("both, the refilled one in its first place"); + CHECK(almasterburst_data::joined(burst.release(1.25)) == std::string("/s/a.lsl /s/b.lsl")); burst.changed("/s/a.lsl", 5.0); burst.changed("/s/a.lsl", 5.25, 1.5); - ensure_equals("emptied later, held from then", *burst.dueAt(), 6.75); - ensure("not when quiet", burst.release(6.25).empty()); - ensure_equals("when the later hold is over", almasterburst_data::joined(burst.release(6.75)), std::string("/s/a.lsl")); + UNSCOPED_INFO("emptied later, held from then"); + CHECK(*burst.dueAt() == 6.75); + UNSCOPED_INFO("not when quiet"); + REQUIRE(burst.release(6.25).empty()); + UNSCOPED_INFO("when the later hold is over"); + CHECK(almasterburst_data::joined(burst.release(6.75)) == std::string("/s/a.lsl")); } - template<> template<> - void almasterburst_object::test<6>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: a file forgotten is not let go and the burst is of what is left", "[almasterburst]") { - set_test_name("a file forgotten is not let go, and the burst is of what is left"); ALMasterBurst burst(1.0, 10.0); burst.changed("/s/a.lsl", 0.0); burst.changed("/s/b.lsl", 0.25); burst.forget("/s/a.lsl"); burst.forget("/s/never.lsl"); - ensure("forgotten", !burst.waiting("/s/a.lsl") && burst.size() == 1); - ensure_equals("the rest goes", almasterburst_data::joined(burst.release(1.25)), std::string("/s/b.lsl")); + CHECK_FALSE(burst.waiting("/s/a.lsl")); + REQUIRE(burst.size() == 1); + UNSCOPED_INFO("the rest goes"); + CHECK(almasterburst_data::joined(burst.release(1.25)) == std::string("/s/b.lsl")); // The first heard forgotten: the burst is as long as the rest's. burst.changed("/s/a.lsl", 100.0); @@ -194,41 +222,47 @@ namespace tut { burst.forget("/s/a.lsl"); } - ensure("not at the forgotten one's longest, nor before the rest's", burst.release(now).empty()); + UNSCOPED_INFO("not at the forgotten one's longest, nor before the rest's"); + REQUIRE(burst.release(now).empty()); } burst.changed("/s/busy.lsl", 115.0); - ensure_equals("due at the rest's longest", *burst.dueAt(), 115.0); - ensure_equals("which it goes at", almasterburst_data::joined(burst.release(115.0)), std::string("/s/busy.lsl")); + UNSCOPED_INFO("due at the rest's longest"); + CHECK(*burst.dueAt() == 115.0); + UNSCOPED_INFO("which it goes at"); + CHECK(almasterburst_data::joined(burst.release(115.0)) == std::string("/s/busy.lsl")); burst.changed("/s/a.lsl", 200.0); burst.changed("/s/b.lsl", 200.0, 1.5); burst.clear(); - ensure("cleared", burst.empty() && !burst.dueAt() && burst.release(300.0).empty()); + REQUIRE(burst.empty()); + REQUIRE_FALSE(burst.dueAt()); + REQUIRE(burst.release(300.0).empty()); } - template<> template<> - void almasterburst_object::test<7>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: Windows paths match in any case and separator and go as last spelled", "[almasterburst]") { - set_test_name("Windows's paths are one file in any case and with either separator; the spelling last given goes"); ALMasterBurst burst(1.0, 10.0); burst.changed("C:\\Scripts\\Door.lsl", 0.0); burst.changed("c:/scripts/door.LSL", 0.5); burst.changed("/s/Door.lsl", 0.5); burst.changed("/s/door.lsl", 0.5); - ensure_equals("one Windows file, and two others", burst.size(), size_t(3)); - ensure("found in any case", burst.waiting("C:/SCRIPTS/DOOR.LSL")); - ensure("but not another's", !burst.waiting("/S/door.lsl")); - ensure_equals("each once, as last spelled", almasterburst_data::joined(burst.release(1.5)), + UNSCOPED_INFO("one Windows file, and two others"); + REQUIRE(burst.size() == size_t(3)); + UNSCOPED_INFO("found in any case"); + CHECK(burst.waiting("C:/SCRIPTS/DOOR.LSL")); + UNSCOPED_INFO("but not another's"); + CHECK_FALSE(burst.waiting("/S/door.lsl")); + UNSCOPED_INFO("each once, as last spelled"); + CHECK(almasterburst_data::joined(burst.release(1.5)) == std::string("c:/scripts/door.LSL /s/Door.lsl /s/door.lsl")); burst.changed("C:\\Scripts\\Door.lsl", 2.0); burst.forget("c:\\SCRIPTS\\door.lsl"); - ensure("forgotten in any case", burst.empty()); + UNSCOPED_INFO("forgotten in any case"); + REQUIRE(burst.empty()); } - template<> template<> - void almasterburst_object::test<8>() + TEST_CASE_METHOD(almasterburst_data, "almasterburst: dueAt and release agree: nothing goes before it and something goes at it", "[almasterburst]") { - set_test_name("dueAt and release agree: nothing goes before it, and something goes at it"); ALMasterBurst burst(1.0, 4.0); // Changes to twenty files at times a step of 1/8 s apart, some of // them emptied, as a small generator picks them. @@ -251,15 +285,20 @@ namespace tut { break; } - ensure("nothing before it is due", burst.release(almasterburst_data::before(*due)).empty()); + UNSCOPED_INFO("nothing before it is due"); + REQUIRE(burst.release(almasterburst_data::before(*due)).empty()); const std::vector gone = burst.release(*due); - ensure("something when it is", !gone.empty()); + UNSCOPED_INFO("something when it is"); + REQUIRE_FALSE(gone.empty()); const std::set once(gone.begin(), gone.end()); - ensure_equals("each once", once.size(), gone.size()); + UNSCOPED_INFO("each once"); + REQUIRE(once.size() == gone.size()); for (const std::string& path : gone) { - ensure("only what was heard", heard.count(path) == 1); - ensure("and is waited on no longer", !burst.waiting(path)); + UNSCOPED_INFO("only what was heard"); + REQUIRE(heard.count(path) == 1); + UNSCOPED_INFO("and is waited on no longer"); + REQUIRE_FALSE(burst.waiting(path)); } given += gone.size(); } @@ -269,9 +308,12 @@ namespace tut } while (const std::optional due = burst.dueAt()) { - ensure("the last of them too", !burst.release(*due).empty()); + UNSCOPED_INFO("the last of them too"); + REQUIRE_FALSE(burst.release(*due).empty()); } - ensure("all let go in the end", burst.empty()); - ensure("many times over", given > 100); + UNSCOPED_INFO("all let go in the end"); + REQUIRE(burst.empty()); + UNSCOPED_INFO("many times over"); + CHECK(given > 100); } } diff --git a/indra/alscript/tests/almasterindex_test.cpp b/indra/alscript/tests/almasterindex_test.cpp index 6b826abe9d..99370ed00b 100644 --- a/indra/alscript/tests/almasterindex_test.cpp +++ b/indra/alscript/tests/almasterindex_test.cpp @@ -33,7 +33,7 @@ #include "llfile.h" #include "llsdserialize.h" -#include "../test/lltut.h" +#include #include #include @@ -43,7 +43,7 @@ #include #include -namespace tut +namespace { struct almasterindex_data { @@ -166,16 +166,10 @@ namespace tut } }; - typedef test_group almasterindex_group; - typedef almasterindex_group::object almasterindex_object; - almasterindex_group almasterindex_instance("almasterindex"); - - template<> template<> - void almasterindex_object::test<1>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: links go through the file and come back as they went", "[almasterindex]") { - set_test_name("links go through the file and come back as they went"); ALMasterIndex& made = open(); - ensure("none at first", made.empty()); + REQUIRE(made.empty()); ALMasterLink door = almasterindex_data::link(1, 2, "/s/door.lsl"); door.uses = { "disk:/s/util.lsl" }; door.hash = "abc"; @@ -186,51 +180,53 @@ namespace tut card.notecard = true; made.link({ door, card }); made.flush(); - ensure("written", !made.dirty()); + UNSCOPED_INFO("written"); + CHECK_FALSE(made.dirty()); ALMasterIndex& back = open(); - ensure_equals("both back", back.size(), size_t(2)); + REQUIRE(back.size() == size_t(2)); const std::optional read = back.linkOf(almasterindex_data::key(1), almasterindex_data::key(2)); - ensure("the door's", read.has_value()); - ensure_equals("its master", read->master, std::string("/s/door.lsl")); - ensure_equals("its hash", read->hash, std::string("abc")); - ensure_equals("its stamp", read->stamp, S64(1234)); - ensure_equals("its base", read->base, almasterindex_data::key(9)); - ensure_equals("its uses", read->uses.size(), size_t(1)); - ensure_equals("its name", read->itemName, std::string("Door")); - ensure("the card's, a notecard's", back.linkOf(LLUUID::null, almasterindex_data::key(3))->notecard); - ensure_equals("found by its master", back.mastering("/s/door.lsl").size(), size_t(1)); - ensure_equals("and by what it read", back.affectedBy("disk:/s/util.lsl").size(), size_t(1)); - ensure_equals("in its object", back.linksIn(almasterindex_data::key(1)).size(), size_t(1)); - ensure_equals("in the inventory", back.linksIn(LLUUID::null).size(), size_t(1)); - ensure("read, nothing changed", !back.dirty()); + REQUIRE(read.has_value()); + CHECK(read->master == std::string("/s/door.lsl")); + CHECK(read->hash == std::string("abc")); + CHECK(read->stamp == S64(1234)); + CHECK(read->base == almasterindex_data::key(9)); + REQUIRE(read->uses.size() == size_t(1)); + CHECK(read->itemName == std::string("Door")); + CHECK(back.linkOf(LLUUID::null, almasterindex_data::key(3))->notecard); + REQUIRE(back.mastering("/s/door.lsl").size() == size_t(1)); + REQUIRE(back.affectedBy("disk:/s/util.lsl").size() == size_t(1)); + REQUIRE(back.linksIn(almasterindex_data::key(1)).size() == size_t(1)); + UNSCOPED_INFO("in the inventory"); + REQUIRE(back.linksIn(LLUUID::null).size() == size_t(1)); + UNSCOPED_INFO("read, nothing changed"); + CHECK_FALSE(back.dirty()); } - template<> template<> - void almasterindex_object::test<2>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: an unreadable file holds no links and is kept until a link changes", "[almasterindex]") { - set_test_name("a file that will not read is no links, and is not written over until a link changes"); const std::string garbage = "this is not at all"; almasterindex_data::put(file, garbage); { ALMasterIndex& looked = open(); - ensure("no links", looked.empty()); - ensure("nothing to write", !looked.dirty()); + REQUIRE(looked.empty()); + UNSCOPED_INFO("nothing to write"); + CHECK_FALSE(looked.dirty()); pass(10.0); index.reset(); } - ensure_equals("left as it was", text(), garbage); + UNSCOPED_INFO("left as it was"); + CHECK(text() == garbage); ALMasterIndex& changed = open(); changed.link(almasterindex_data::link(1, 2, "/s/door.lsl")); index.reset(); - ensure_equals("a link written over it", onDisk().size(), size_t(1)); + UNSCOPED_INFO("a link written over it"); + REQUIRE(onDisk().size() == size_t(1)); } - template<> template<> - void almasterindex_object::test<3>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: links orphaned long ago are let go of on reading and that is written", "[almasterindex]") { - set_test_name("links orphaned long ago are let go of as the file is read, and that is written"); ALMasterLinks links; ALMasterLink old = almasterindex_data::link(1, 1, "/s/old.lsl"); old.state = ALMasterLink::State::Orphaned; @@ -246,60 +242,66 @@ namespace tut almasterindex_data::put(file, out.str()); ALMasterIndex& read = open(); - ensure_equals("the old one gone", read.size(), size_t(2)); - ensure("not the recent one", read.linkOf(almasterindex_data::key(1), almasterindex_data::key(2)).has_value()); - ensure("to be written", read.dirty()); - ensure_equals("nobody told: nobody listened yet", told, 0); + UNSCOPED_INFO("the old one gone"); + REQUIRE(read.size() == size_t(2)); + UNSCOPED_INFO("not the recent one"); + REQUIRE(read.linkOf(almasterindex_data::key(1), almasterindex_data::key(2)).has_value()); + UNSCOPED_INFO("to be written"); + CHECK(read.dirty()); + UNSCOPED_INFO("nobody told: nobody listened yet"); + CHECK(told == 0); index.reset(); - ensure_equals("written without it", onDisk().size(), size_t(2)); + UNSCOPED_INFO("written without it"); + REQUIRE(onDisk().size() == size_t(2)); } - template<> template<> - void almasterindex_object::test<4>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: links made together are told once and one alone once", "[almasterindex]") { - set_test_name("links made together are told once, and one alone once"); ALMasterIndex& made = open(); made.link({ almasterindex_data::link(1, 1, "/s/a.lsl"), almasterindex_data::link(1, 2, "/s/b.lsl"), almasterindex_data::link(2, 3, "/s/c.lsl") }); - ensure_equals("three", made.size(), size_t(3)); - ensure_equals("told once", told, 1); + REQUIRE(made.size() == size_t(3)); + CHECK(told == 1); made.link(almasterindex_data::link(2, 4, "/s/d.lsl")); - ensure_equals("and once more", told, 2); + CHECK(told == 2); made.link(std::vector()); - ensure_equals("none, not told", told, 2); + UNSCOPED_INFO("none, not told"); + CHECK(told == 2); made.link(almasterindex_data::link(1, 1, "/s/a2.lsl")); - ensure_equals("put over, not beside", made.size(), size_t(4)); - ensure_equals("the new master", made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1))->master, std::string("/s/a2.lsl")); + UNSCOPED_INFO("put over, not beside"); + REQUIRE(made.size() == size_t(4)); + CHECK(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1))->master == std::string("/s/a2.lsl")); } - template<> template<> - void almasterindex_object::test<5>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: a link let go of and links marked pending only where linked", "[almasterindex]") { - set_test_name("a link let go of, and links marked pending, those linked only"); ALMasterIndex& made = open(); made.link({ almasterindex_data::link(1, 1, "/s/a.lsl"), almasterindex_data::link(1, 2, "/s/b.lsl") }); told = 0; - ensure("let go of", made.unlink(almasterindex_data::key(1), almasterindex_data::key(1))); - ensure_equals("told", told, 1); - ensure("gone", !made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1))); - ensure("one not linked is not", !made.unlink(almasterindex_data::key(1), almasterindex_data::key(1))); - ensure_equals("and not told", told, 1); + CHECK(made.unlink(almasterindex_data::key(1), almasterindex_data::key(1))); + CHECK(told == 1); + REQUIRE_FALSE(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1))); + UNSCOPED_INFO("one not linked is not"); + CHECK_FALSE(made.unlink(almasterindex_data::key(1), almasterindex_data::key(1))); + UNSCOPED_INFO("and not told"); + CHECK(told == 1); const size_t marked = made.markPending({ { almasterindex_data::key(1), almasterindex_data::key(1) }, { almasterindex_data::key(1), almasterindex_data::key(2) } }); - ensure_equals("the one linked", marked, size_t(1)); - ensure("pending", made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->state == ALMasterLink::State::Pending); - ensure_equals("told", told, 2); - ensure_equals("none linked, none marked", made.markPending({ { almasterindex_data::key(5), almasterindex_data::key(5) } }), size_t(0)); - ensure_equals("and not told", told, 2); + UNSCOPED_INFO("the one linked"); + CHECK(marked == size_t(1)); + CHECK(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->state == ALMasterLink::State::Pending); + CHECK(told == 2); + UNSCOPED_INFO("none linked, none marked"); + CHECK(made.markPending({ { almasterindex_data::key(5), almasterindex_data::key(5) } }) == size_t(0)); + UNSCOPED_INFO("and not told"); + CHECK(told == 2); index.reset(); const ALMasterLinks disk = onDisk(); - ensure_equals("one on disk", disk.size(), size_t(1)); - ensure("pending there", disk.of(almasterindex_data::key(1), almasterindex_data::key(2))->state == ALMasterLink::State::Pending); + REQUIRE(disk.size() == size_t(1)); + CHECK(disk.of(almasterindex_data::key(1), almasterindex_data::key(2))->state == ALMasterLink::State::Pending); } - template<> template<> - void almasterindex_object::test<6>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: a save heard elsewhere marks the link differing unless the master sent it", "[almasterindex]") { - set_test_name("a save heard from elsewhere marks the link differing, unless it is what the master last sent"); ALMasterIndex& made = open(); const std::string ours = "default { state_entry() {} }"; ALMasterLink door = almasterindex_data::link(1, 2, "/s/door.lsl"); @@ -313,100 +315,115 @@ namespace tut made.flush(); told = 0; - ensure("what it is of already: nothing", made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), almasterindex_data::key(100), ours) == ALMasterIndex::Heard::Nothing); - ensure("no asset: nothing", made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), LLUUID::null, "x") == ALMasterIndex::Heard::Nothing); - ensure("not linked: nothing", made.heardSaved(almasterindex_data::key(7), almasterindex_data::key(7), almasterindex_data::key(101), ours) == ALMasterIndex::Heard::Nothing); - ensure("nothing changed", !made.dirty()); + UNSCOPED_INFO("what it is of already: nothing"); + CHECK(made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), almasterindex_data::key(100), ours) == ALMasterIndex::Heard::Nothing); + UNSCOPED_INFO("no asset: nothing"); + CHECK(made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), LLUUID::null, "x") == ALMasterIndex::Heard::Nothing); + UNSCOPED_INFO("not linked: nothing"); + CHECK(made.heardSaved(almasterindex_data::key(7), almasterindex_data::key(7), almasterindex_data::key(101), ours) == ALMasterIndex::Heard::Nothing); + UNSCOPED_INFO("nothing changed"); + CHECK_FALSE(made.dirty()); // A recompile: the same text, a new asset. - ensure("the same", made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), almasterindex_data::key(101), ours) == ALMasterIndex::Heard::Same); + CHECK(made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), almasterindex_data::key(101), ours) == ALMasterIndex::Heard::Same); const std::optional same = made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2)); - ensure_equals("the base moved", same->base, almasterindex_data::key(101)); - ensure("still active", same->state == ALMasterLink::State::Active); - ensure_equals("nobody told", told, 0); - ensure("to be written", made.dirty()); - ensure("a notecard's by its text", made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(3), almasterindex_data::key(201), "hello") == ALMasterIndex::Heard::Same); - - ensure("something else", made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), almasterindex_data::key(102), "default {}") == ALMasterIndex::Heard::Differing); + CHECK(same->base == almasterindex_data::key(101)); + CHECK(same->state == ALMasterLink::State::Active); + CHECK(told == 0); + UNSCOPED_INFO("to be written"); + CHECK(made.dirty()); + UNSCOPED_INFO("a notecard's by its text"); + CHECK(made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(3), almasterindex_data::key(201), "hello") == ALMasterIndex::Heard::Same); + + CHECK(made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(2), almasterindex_data::key(102), "default {}") == ALMasterIndex::Heard::Differing); const std::optional other = made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2)); - ensure("differing", other->state == ALMasterLink::State::Differing); - ensure_equals("its base as it was", other->base, almasterindex_data::key(101)); - ensure_equals("told", told, 1); + CHECK(other->state == ALMasterLink::State::Differing); + UNSCOPED_INFO("its base as it was"); + CHECK(other->base == almasterindex_data::key(101)); + CHECK(told == 1); // Nothing sent through it yet: no hash to be the same as. made.link(almasterindex_data::link(2, 4, "/s/new.lsl")); - ensure("never sent: differing", made.heardSaved(almasterindex_data::key(2), almasterindex_data::key(4), almasterindex_data::key(300), ours) == ALMasterIndex::Heard::Differing); + UNSCOPED_INFO("never sent: differing"); + CHECK(made.heardSaved(almasterindex_data::key(2), almasterindex_data::key(4), almasterindex_data::key(300), ours) == ALMasterIndex::Heard::Differing); } - template<> template<> - void almasterindex_object::test<7>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: a send's link is put back if still linked and moves to the item it saved as", "[almasterindex]") { - set_test_name("a send's link is put back where the item is linked still, and moves to a new item it was saved as"); ALMasterIndex& made = open(); made.link(almasterindex_data::link(1, 2, "/s/door.lsl")); told = 0; ALMasterLink sent = *made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2)); sent.hash = "h1"; sent.base = almasterindex_data::key(50); - ensure("put back", made.finished(almasterindex_data::key(1), almasterindex_data::key(2), sent)); - ensure_equals("with what the send left", made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->hash, std::string("h1")); - ensure_equals("told", told, 1); + CHECK(made.finished(almasterindex_data::key(1), almasterindex_data::key(2), sent)); + UNSCOPED_INFO("with what the send left"); + CHECK(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->hash == std::string("h1")); + CHECK(told == 1); // Saved as another item: the link is that one's. ALMasterLink rekeyed = sent; rekeyed.item = almasterindex_data::key(3); rekeyed.hash = "h2"; - ensure("put", made.finished(almasterindex_data::key(1), almasterindex_data::key(2), rekeyed)); - ensure("not the old item's", !made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))); - ensure_equals("the new one's", made.linkOf(almasterindex_data::key(1), almasterindex_data::key(3))->hash, std::string("h2")); - ensure_equals("one link", made.size(), size_t(1)); - ensure_equals("found by its master", made.mastering("/s/door.lsl").size(), size_t(1)); + CHECK(made.finished(almasterindex_data::key(1), almasterindex_data::key(2), rekeyed)); + UNSCOPED_INFO("not the old item's"); + REQUIRE_FALSE(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))); + CHECK(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(3))->hash == std::string("h2")); + REQUIRE(made.size() == size_t(1)); + REQUIRE(made.mastering("/s/door.lsl").size() == size_t(1)); // Let go of while it was on its way: stays let go of. made.unlink(almasterindex_data::key(1), almasterindex_data::key(3)); - ensure("not put", !made.finished(almasterindex_data::key(1), almasterindex_data::key(3), rekeyed)); - ensure("none", made.empty()); + CHECK_FALSE(made.finished(almasterindex_data::key(1), almasterindex_data::key(3), rekeyed)); + REQUIRE(made.empty()); } - template<> template<> - void almasterindex_object::test<8>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: what a probe found is kept only by a link of its master knowing nothing yet", "[almasterindex]") { - set_test_name("what a probe found is kept only by a link that knows nothing still, of the same master, and told a moment later"); ALMasterIndex& made = open(); made.link({ almasterindex_data::link(1, 1, "/s/a.lsl"), almasterindex_data::link(1, 2, "/s/b.lsl"), almasterindex_data::link(1, 3, "/s/c.lsl") }); - ensure("a new link knows nothing", ALMasterIndex::knowsNothing(*made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1)))); + UNSCOPED_INFO("a new link knows nothing"); + CHECK(ALMasterIndex::knowsNothing(*made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1)))); told = 0; - ensure("kept", made.adopted(almasterindex_data::key(1), almasterindex_data::key(1), "/s/a.lsl", { "disk:/s/util.lsl" }, false, "ha", 77)); + CHECK(made.adopted(almasterindex_data::key(1), almasterindex_data::key(1), "/s/a.lsl", { "disk:/s/util.lsl" }, false, "ha", 77)); const std::optional a = made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1)); - ensure_equals("what it read", a->uses.size(), size_t(1)); - ensure_equals("what went up, as the world holds it", a->hash, std::string("ha")); - ensure_equals("the stamp with it", a->stamp, S64(77)); - ensure("found by what it read", !made.affectedBy("disk:/s/util.lsl").empty()); + REQUIRE(a->uses.size() == size_t(1)); + UNSCOPED_INFO("what went up, as the world holds it"); + CHECK(a->hash == std::string("ha")); + CHECK(a->stamp == S64(77)); + REQUIRE_FALSE(made.affectedBy("disk:/s/util.lsl").empty()); // The world holding something else: what it read, and no hash. - ensure("kept too", made.adopted(almasterindex_data::key(1), almasterindex_data::key(2), "/s/b.lsl", {}, true, std::string(), 0)); - ensure("missed", made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->missed); - ensure("no hash", made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->hash.empty()); - ensure_equals("not told yet", told, 0); + CHECK(made.adopted(almasterindex_data::key(1), almasterindex_data::key(2), "/s/b.lsl", {}, true, std::string(), 0)); + CHECK(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->missed); + REQUIRE(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(2))->hash.empty()); + UNSCOPED_INFO("not told yet"); + CHECK(told == 0); pass(ALMasterIndex::TELL_SOON + 0.01); - ensure_equals("told once for the run", told, 1); + UNSCOPED_INFO("told once for the run"); + CHECK(told == 1); - ensure("knowing something now: not again", !made.adopted(almasterindex_data::key(1), almasterindex_data::key(1), "/s/a.lsl", {}, false, "other", 1)); - ensure_equals("as it was", made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1))->hash, std::string("ha")); + UNSCOPED_INFO("knowing something now: not again"); + CHECK_FALSE(made.adopted(almasterindex_data::key(1), almasterindex_data::key(1), "/s/a.lsl", {}, false, "other", 1)); + UNSCOPED_INFO("as it was"); + CHECK(made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1))->hash == std::string("ha")); made.link(almasterindex_data::link(1, 3, "/s/elsewhere.lsl")); - ensure("linked to another master: not", !made.adopted(almasterindex_data::key(1), almasterindex_data::key(3), "/s/c.lsl", { "disk:/s/x.lsl" }, false, "hc", 1)); - ensure("nothing learned: nothing kept", !made.adopted(almasterindex_data::key(1), almasterindex_data::key(3), "/s/elsewhere.lsl", {}, false, std::string(), 0)); - ensure("not linked: not", !made.adopted(almasterindex_data::key(9), almasterindex_data::key(9), "/s/a.lsl", { "disk:/s/x.lsl" }, false, "h", 1)); - ensure("still knowing nothing", ALMasterIndex::knowsNothing(*made.linkOf(almasterindex_data::key(1), almasterindex_data::key(3)))); + UNSCOPED_INFO("linked to another master: not"); + CHECK_FALSE(made.adopted(almasterindex_data::key(1), almasterindex_data::key(3), "/s/c.lsl", { "disk:/s/x.lsl" }, false, "hc", 1)); + UNSCOPED_INFO("nothing learned: nothing kept"); + CHECK_FALSE(made.adopted(almasterindex_data::key(1), almasterindex_data::key(3), "/s/elsewhere.lsl", {}, false, std::string(), 0)); + UNSCOPED_INFO("not linked: not"); + CHECK_FALSE(made.adopted(almasterindex_data::key(9), almasterindex_data::key(9), "/s/a.lsl", { "disk:/s/x.lsl" }, false, "h", 1)); + UNSCOPED_INFO("still knowing nothing"); + CHECK(ALMasterIndex::knowsNothing(*made.linkOf(almasterindex_data::key(1), almasterindex_data::key(3)))); const S32 before = told; pass(1.0); - ensure_equals("nothing more told", told, before); + UNSCOPED_INFO("nothing more told"); + CHECK(told == before); } - template<> template<> - void almasterindex_object::test<9>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: a send's bookkeeping is written after a quiet second or five after the first", "[almasterindex]") { - set_test_name("a send's bookkeeping is written once nothing has changed for a second, and no later than five after the first"); ALMasterIndex& made = open(); made.link(almasterindex_data::link(1, 2, "/s/door.lsl")); made.flush(); @@ -414,18 +431,23 @@ namespace tut sent.hash = "h1"; made.finished(almasterindex_data::key(1), almasterindex_data::key(2), sent); - ensure("waiting", made.dirty()); + UNSCOPED_INFO("waiting"); + CHECK(made.dirty()); pass(0.5); sent.hash = "h2"; made.finished(almasterindex_data::key(1), almasterindex_data::key(2), sent); pass(0.6); - ensure("a second after the first, not after the last: waiting still", made.dirty()); + UNSCOPED_INFO("a second after the first, not after the last: waiting still"); + CHECK(made.dirty()); made.awaitWrites(); - ensure("the file as it was", onDisk().of(almasterindex_data::key(1), almasterindex_data::key(2))->hash.empty()); + UNSCOPED_INFO("the file as it was"); + REQUIRE(onDisk().of(almasterindex_data::key(1), almasterindex_data::key(2))->hash.empty()); pass(0.5); - ensure("a second after the last: handed over", !made.dirty()); + UNSCOPED_INFO("a second after the last: handed over"); + CHECK_FALSE(made.dirty()); made.awaitWrites(); - ensure_equals("written, the newest", onDisk().of(almasterindex_data::key(1), almasterindex_data::key(2))->hash, std::string("h2")); + UNSCOPED_INFO("written, the newest"); + CHECK(onDisk().of(almasterindex_data::key(1), almasterindex_data::key(2))->hash == std::string("h2")); // A change every 0.8 s: written five seconds after the first all the // same. @@ -435,43 +457,47 @@ namespace tut made.finished(almasterindex_data::key(1), almasterindex_data::key(2), sent); pass(0.8); } - ensure("4.8 s on: waiting", made.dirty()); + UNSCOPED_INFO("4.8 s on: waiting"); + CHECK(made.dirty()); pass(0.3); - ensure("5.1 s on: handed over", !made.dirty()); + UNSCOPED_INFO("5.1 s on: handed over"); + CHECK_FALSE(made.dirty()); made.awaitWrites(); - ensure_equals("written", onDisk().of(almasterindex_data::key(1), almasterindex_data::key(2))->hash, std::string("run5")); + UNSCOPED_INFO("written"); + CHECK(onDisk().of(almasterindex_data::key(1), almasterindex_data::key(2))->hash == std::string("run5")); } - template<> template<> - void almasterindex_object::test<10>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: whatever waits is written to its own file as the index goes", "[almasterindex]") { - set_test_name("whatever waits is written as the index goes, to its own file, as for another account"); const std::string other = in("other_account.llsd"); ALMasterIndex& first = open(); first.link(almasterindex_data::link(1, 2, "/s/door.lsl")); ALMasterLink sent = *first.linkOf(almasterindex_data::key(1), almasterindex_data::key(2)); sent.hash = "last"; first.finished(almasterindex_data::key(1), almasterindex_data::key(2), sent); - ensure("waiting", first.dirty()); + UNSCOPED_INFO("waiting"); + CHECK(first.dirty()); // Another account's: this one's let go of, and written as it goes, // with no time passed. ALMasterIndex& second = open(other); - ensure("the other account's has none", second.empty()); - ensure_equals("the first's written to its own file", almasterindex_data::onDisk(file).of(almasterindex_data::key(1), almasterindex_data::key(2))->hash, + UNSCOPED_INFO("the other account's has none"); + REQUIRE(second.empty()); + UNSCOPED_INFO("the first's written to its own file"); + CHECK(almasterindex_data::onDisk(file).of(almasterindex_data::key(1), almasterindex_data::key(2))->hash == std::string("last")); second.link(almasterindex_data::link(5, 6, "/s/lamp.lsl")); index.reset(); - ensure_equals("the second's to its own", almasterindex_data::onDisk(other).size(), size_t(1)); - ensure("and not to the first's", !almasterindex_data::onDisk(file).of(almasterindex_data::key(5), almasterindex_data::key(6))); + UNSCOPED_INFO("the second's to its own"); + REQUIRE(almasterindex_data::onDisk(other).size() == size_t(1)); + UNSCOPED_INFO("and not to the first's"); + REQUIRE_FALSE(almasterindex_data::onDisk(file).of(almasterindex_data::key(5), almasterindex_data::key(6))); // What waits for a time gone by finds nobody. pass(10.0); } - template<> template<> - void almasterindex_object::test<11>() + TEST_CASE_METHOD(almasterindex_data, "almasterindex: what a person did is written at once and what a send learned later", "[almasterindex]") { - set_test_name("what a person did, or what could not be found again, is written at once; what a send learned a moment later"); ALMasterIndex& made = open(); // Each read as a crash would leave the file: what was handed over // written, nothing more, and no time passed. @@ -483,8 +509,9 @@ namespace tut }; made.link({ almasterindex_data::link(1, 1, "/s/a.lsl"), almasterindex_data::link(1, 2, "/s/b.lsl") }); - ensure("a link: handed over at once", !made.dirty()); - ensure("on disk", on_disk(1).has_value()); + UNSCOPED_INFO("a link: handed over at once"); + CHECK_FALSE(made.dirty()); + REQUIRE(on_disk(1).has_value()); // A send's bookkeeping waits, and a crash now loses it. ALMasterLink sent = *made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1)); @@ -492,31 +519,45 @@ namespace tut sent.base = almasterindex_data::key(10); sent.uses = { "disk:/s/util.lsl" }; made.finished(almasterindex_data::key(1), almasterindex_data::key(1), sent); - ensure("a send's bookkeeping waits", made.dirty()); - ensure("not on disk yet", on_disk(1)->hash.empty()); + UNSCOPED_INFO("a send's bookkeeping waits"); + CHECK(made.dirty()); + UNSCOPED_INFO("not on disk yet"); + REQUIRE(on_disk(1)->hash.empty()); // Something a person did takes it with it. made.markPending({ { almasterindex_data::key(1), almasterindex_data::key(2) } }); - ensure("pending: at once", !made.dirty()); - ensure("pending on disk", on_disk(2)->state == ALMasterLink::State::Pending); - ensure_equals("and what waited with it", on_disk(1)->hash, std::string("h1")); + UNSCOPED_INFO("pending: at once"); + CHECK_FALSE(made.dirty()); + CHECK(on_disk(2)->state == ALMasterLink::State::Pending); + UNSCOPED_INFO("and what waited with it"); + CHECK(on_disk(1)->hash == std::string("h1")); // A send that changes how a link stands: at once. sent = *made.linkOf(almasterindex_data::key(1), almasterindex_data::key(1)); sent.state = ALMasterLink::State::Suspended; made.finished(almasterindex_data::key(1), almasterindex_data::key(1), sent); - ensure("suspended: at once", !made.dirty()); - ensure("suspended on disk", on_disk(1)->state == ALMasterLink::State::Suspended); + UNSCOPED_INFO("suspended: at once"); + CHECK_FALSE(made.dirty()); + CHECK(on_disk(1)->state == ALMasterLink::State::Suspended); sent.state = ALMasterLink::State::Active; made.finished(almasterindex_data::key(1), almasterindex_data::key(1), sent); - ensure("active again: at once", !made.dirty() && on_disk(1)->state == ALMasterLink::State::Active); + { + INFO("active again: at once"); + CHECK_FALSE(made.dirty()); + CHECK(on_disk(1)->state == ALMasterLink::State::Active); + } // A send saved as another item: at once, or the link is lost with it. ALMasterLink moved = sent; moved.item = almasterindex_data::key(3); made.finished(almasterindex_data::key(1), almasterindex_data::key(1), moved); - ensure("moved: at once", !made.dirty()); - ensure("under the new item on disk", on_disk(3).has_value() && !on_disk(1).has_value()); + UNSCOPED_INFO("moved: at once"); + CHECK_FALSE(made.dirty()); + { + INFO("under the new item on disk"); + REQUIRE(on_disk(3).has_value()); + REQUIRE_FALSE(on_disk(1).has_value()); + } // A save heard from elsewhere: differing at once, the same later. sent = *made.linkOf(almasterindex_data::key(1), almasterindex_data::key(3)); @@ -524,20 +565,33 @@ namespace tut made.finished(almasterindex_data::key(1), almasterindex_data::key(3), sent); made.flush(); made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(3), almasterindex_data::key(11), "default {}"); - ensure("the same: later", made.dirty()); + UNSCOPED_INFO("the same: later"); + CHECK(made.dirty()); made.heardSaved(almasterindex_data::key(1), almasterindex_data::key(3), almasterindex_data::key(12), "default { touch_start(integer n) {} }"); - ensure("differing: at once", !made.dirty() && on_disk(3)->state == ALMasterLink::State::Differing); + { + INFO("differing: at once"); + CHECK_FALSE(made.dirty()); + CHECK(on_disk(3)->state == ALMasterLink::State::Differing); + } // A probe's answer, and a link let go of. made.link(almasterindex_data::link(1, 4, "/s/d.lsl")); made.adopted(almasterindex_data::key(1), almasterindex_data::key(4), "/s/d.lsl", { "disk:/s/util.lsl" }, false, std::string(), 0); - ensure("a probe's answer: later", made.dirty()); - ensure("not on disk yet", on_disk(4)->uses.empty()); + UNSCOPED_INFO("a probe's answer: later"); + CHECK(made.dirty()); + UNSCOPED_INFO("not on disk yet"); + REQUIRE(on_disk(4)->uses.empty()); made.unlink(almasterindex_data::key(1), almasterindex_data::key(2)); - ensure("let go of: at once", !made.dirty() && !on_disk(2).has_value()); - ensure_equals("the probe's answer with it", on_disk(4)->uses.size(), size_t(1)); + { + INFO("let go of: at once"); + CHECK_FALSE(made.dirty()); + REQUIRE_FALSE(on_disk(2).has_value()); + } + UNSCOPED_INFO("the probe's answer with it"); + REQUIRE(on_disk(4)->uses.size() == size_t(1)); // Nothing left for the time to write. pass(10.0); - ensure("nothing waiting", !made.dirty()); + UNSCOPED_INFO("nothing waiting"); + CHECK_FALSE(made.dirty()); } } diff --git a/indra/alscript/tests/almasterlinks_test.cpp b/indra/alscript/tests/almasterlinks_test.cpp index ef1cc38810..1f3ea7e4c1 100644 --- a/indra/alscript/tests/almasterlinks_test.cpp +++ b/indra/alscript/tests/almasterlinks_test.cpp @@ -28,13 +28,13 @@ #include "llsdserialize.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { struct almasterlinks_data { @@ -88,52 +88,62 @@ namespace tut } }; - typedef test_group almasterlinks_group; - typedef almasterlinks_group::object almasterlinks_object; - almasterlinks_group almasterlinks_instance("almasterlinks"); - - template<> template<> - void almasterlinks_object::test<1>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: a link is found by object and item then replaced in place and removed", "[almasterlinks]") { - set_test_name("a link is found by its object and item, put over, and removed"); ALMasterLinks links; - ensure("none at first", links.empty() && !links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))); + { + INFO("none at first"); + REQUIRE(links.empty()); + REQUIRE_FALSE(links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))); + } links.put(almasterlinks_data::link(1, 2, "/s/door.lsl")); links.put(almasterlinks_data::link(0, 2, "/s/inventory.lsl")); links.put(almasterlinks_data::link(3, 4, "/s/lamp.lsl")); - ensure_equals("three", links.size(), size_t(3)); - ensure_equals("by its object and item", links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))->master, std::string("/s/door.lsl")); - ensure_equals("the same item in the inventory is another", links.of(LLUUID::null, almasterlinks_data::key(2))->master, + REQUIRE(links.size() == size_t(3)); + UNSCOPED_INFO("by its object and item"); + CHECK(links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))->master == std::string("/s/door.lsl")); + UNSCOPED_INFO("the same item in the inventory is another"); + CHECK(links.of(LLUUID::null, almasterlinks_data::key(2))->master == std::string("/s/inventory.lsl")); - ensure("not by the item in another object", !links.of(almasterlinks_data::key(3), almasterlinks_data::key(2))); + UNSCOPED_INFO("not by the item in another object"); + REQUIRE_FALSE(links.of(almasterlinks_data::key(3), almasterlinks_data::key(2))); ALMasterLink again = almasterlinks_data::link(1, 2, "/s/door2.lsl"); again.made = ALMasterLink::Made::Hint; ALMasterLink& put = links.put(again); - ensure_equals("put over, not beside", links.size(), size_t(3)); - ensure_equals("what put answers is the one kept", put.master, std::string("/s/door2.lsl")); - ensure("and found so", links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))->made == ALMasterLink::Made::Hint); - ensure_equals("in its place", links.all()[0].master, std::string("/s/door2.lsl")); + UNSCOPED_INFO("put over, not beside"); + REQUIRE(links.size() == size_t(3)); + UNSCOPED_INFO("what put answers is the one kept"); + CHECK(put.master == std::string("/s/door2.lsl")); + UNSCOPED_INFO("and found so"); + CHECK(links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))->made == ALMasterLink::Made::Hint); + UNSCOPED_INFO("in its place"); + CHECK(links.all()[0].master == std::string("/s/door2.lsl")); ALMasterLink* found = links.find(almasterlinks_data::key(3), almasterlinks_data::key(4)); - ensure("found to change", found != nullptr); + UNSCOPED_INFO("found to change"); + REQUIRE(found != nullptr); found->hash = "xxh128:00"; - ensure_equals("changed where it is kept", links.of(almasterlinks_data::key(3), almasterlinks_data::key(4))->hash, std::string("xxh128:00")); - ensure("nothing to change of an item not linked", !links.find(almasterlinks_data::key(9), almasterlinks_data::key(9))); - - ensure("removed", links.remove(almasterlinks_data::key(1), almasterlinks_data::key(2))); - ensure("and gone", !links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))); - ensure("not twice", !links.remove(almasterlinks_data::key(1), almasterlinks_data::key(2))); - ensure_equals("the rest kept, in order", links.all()[0].master, std::string("/s/inventory.lsl")); - ensure_equals("and found where they now are", links.of(almasterlinks_data::key(3), almasterlinks_data::key(4))->master, + UNSCOPED_INFO("changed where it is kept"); + CHECK(links.of(almasterlinks_data::key(3), almasterlinks_data::key(4))->hash == std::string("xxh128:00")); + UNSCOPED_INFO("nothing to change of an item not linked"); + REQUIRE_FALSE(links.find(almasterlinks_data::key(9), almasterlinks_data::key(9))); + + CHECK(links.remove(almasterlinks_data::key(1), almasterlinks_data::key(2))); + UNSCOPED_INFO("and gone"); + REQUIRE_FALSE(links.of(almasterlinks_data::key(1), almasterlinks_data::key(2))); + UNSCOPED_INFO("not twice"); + CHECK_FALSE(links.remove(almasterlinks_data::key(1), almasterlinks_data::key(2))); + UNSCOPED_INFO("the rest kept, in order"); + CHECK(links.all()[0].master == std::string("/s/inventory.lsl")); + UNSCOPED_INFO("and found where they now are"); + CHECK(links.of(almasterlinks_data::key(3), almasterlinks_data::key(4))->master == std::string("/s/lamp.lsl")); } - template<> template<> - void almasterlinks_object::test<2>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: mastering lists a file's items comparing paths as their platform does", "[almasterlinks]") { - set_test_name("every item a master file masters, a path compared as its platform compares it"); ALMasterLinks links; links.put(almasterlinks_data::link(1, 10, "/s/net/door.lsl")); links.put(almasterlinks_data::link(2, 11, "/s/net/door.lsl")); @@ -141,33 +151,41 @@ namespace tut links.put(almasterlinks_data::link(4, 13, "C:\\Scripts\\Door.lsl")); links.put(almasterlinks_data::link(5, 14, "\\\\host\\share\\door.lsl")); - ensure_equals("one file masters several items, in the order put", almasterlinks_data::items(links.mastering("/s/net/door.lsl")), + UNSCOPED_INFO("one file masters several items, in the order put"); + CHECK(almasterlinks_data::items(links.mastering("/s/net/door.lsl")) == std::string("10 11")); - ensure("a path from a root of /, as written: the disk's own case", links.mastering("/s/net/Door.lsl").empty()); - ensure("nor another file", links.mastering("/s/net/window.lsl").empty()); - ensure_equals("Windows's, in any case and with either separator", - almasterlinks_data::items(links.mastering("c:/scripts/door.LSL")), std::string("13")); - ensure_equals("a share's the same", almasterlinks_data::items(links.mastering("//HOST/share/door.lsl")), + UNSCOPED_INFO("a path from a root of /, as written: the disk's own case"); + REQUIRE(links.mastering("/s/net/Door.lsl").empty()); + UNSCOPED_INFO("nor another file"); + REQUIRE(links.mastering("/s/net/window.lsl").empty()); + UNSCOPED_INFO("Windows's, in any case and with either separator"); + CHECK(almasterlinks_data::items(links.mastering("c:/scripts/door.LSL")) == std::string("13")); + UNSCOPED_INFO("a share's the same"); + CHECK(almasterlinks_data::items(links.mastering("//HOST/share/door.lsl")) == std::string("14")); - ensure_equals("the key of Windows's", ALMasterLinks::keyOf("C:/Scripts/Door.lsl"), std::string("c:\\scripts\\door.lsl")); - ensure_equals("and of any other", ALMasterLinks::keyOf("/S/Door.lsl"), std::string("/S/Door.lsl")); + UNSCOPED_INFO("the key of Windows's"); + CHECK(ALMasterLinks::keyOf("C:/Scripts/Door.lsl") == std::string("c:\\scripts\\door.lsl")); + UNSCOPED_INFO("and of any other"); + CHECK(ALMasterLinks::keyOf("/S/Door.lsl") == std::string("/S/Door.lsl")); // Changed through find(): the index follows. links.find(almasterlinks_data::key(2), almasterlinks_data::key(11))->master = "/s/net/lamp.lsl"; - ensure_equals("a master changed through find", almasterlinks_data::items(links.mastering("/s/net/door.lsl")), + UNSCOPED_INFO("a master changed through find"); + CHECK(almasterlinks_data::items(links.mastering("/s/net/door.lsl")) == std::string("10")); - ensure_equals("is found under the new", almasterlinks_data::items(links.mastering("/s/net/lamp.lsl")), + UNSCOPED_INFO("is found under the new"); + CHECK(almasterlinks_data::items(links.mastering("/s/net/lamp.lsl")) == std::string("11 12")); links.remove(almasterlinks_data::key(1), almasterlinks_data::key(10)); - ensure("and one removed is not", links.mastering("/s/net/door.lsl").empty()); - ensure_equals("nor are the others lost by it", almasterlinks_data::items(links.mastering("/s/net/lamp.lsl")), + UNSCOPED_INFO("and one removed is not"); + REQUIRE(links.mastering("/s/net/door.lsl").empty()); + UNSCOPED_INFO("nor are the others lost by it"); + CHECK(almasterlinks_data::items(links.mastering("/s/net/lamp.lsl")) == std::string("11 12")); } - template<> template<> - void almasterlinks_object::test<3>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: usersOf finds links that last read a file by its identity or its path", "[almasterlinks]") { - set_test_name("every link whose last expansion read a file, by the file's identity or its path"); ALMasterLinks links; ALMasterLink door = almasterlinks_data::link(1, 10, "/s/door.lsl"); door.uses = { "disk:/s/lib/util.lsl", "disk:/s/lib/net.lsl", "disk:/s/lib/util.lsl" }; @@ -179,28 +197,32 @@ namespace tut links.put(lamp); links.put(win); - ensure_equals("by its identity, each user once", almasterlinks_data::items(links.usersOf("disk:/s/lib/util.lsl")), + UNSCOPED_INFO("by its identity, each user once"); + CHECK(almasterlinks_data::items(links.usersOf("disk:/s/lib/util.lsl")) == std::string("10 11")); - ensure_equals("or by its path alone", almasterlinks_data::items(links.usersOf("/s/lib/net.lsl")), + UNSCOPED_INFO("or by its path alone"); + CHECK(almasterlinks_data::items(links.usersOf("/s/lib/net.lsl")) == std::string("10")); - ensure("a master is not a use of itself", links.usersOf("/s/door.lsl").empty()); - ensure_equals("Windows's in any case", almasterlinks_data::items(links.usersOf("disk:c:/s/lib/util.lsl")), + UNSCOPED_INFO("a master is not a use of itself"); + REQUIRE(links.usersOf("/s/door.lsl").empty()); + UNSCOPED_INFO("Windows's in any case"); + CHECK(almasterlinks_data::items(links.usersOf("disk:c:/s/lib/util.lsl")) == std::string("12")); // The uses of the next expansion, written through find(). links.find(almasterlinks_data::key(1), almasterlinks_data::key(10))->uses = { "disk:/s/lib/net.lsl" }; - ensure_equals("uses changed through find", almasterlinks_data::items(links.usersOf("disk:/s/lib/util.lsl")), + UNSCOPED_INFO("uses changed through find"); + CHECK(almasterlinks_data::items(links.usersOf("disk:/s/lib/util.lsl")) == std::string("11")); ALMasterLink again = lamp; again.uses.clear(); links.put(again); - ensure("and through put", links.usersOf("disk:/s/lib/util.lsl").empty()); + UNSCOPED_INFO("and through put"); + REQUIRE(links.usersOf("disk:/s/lib/util.lsl").empty()); } - template<> template<> - void almasterlinks_object::test<4>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: orphaned links are kept ninety days and then pruned; no other is", "[almasterlinks]") { - set_test_name("orphaned links are kept ninety days and then pruned; no other is"); const LLDate now(1800000000.0); const F64 day = 24.0 * 60.0 * 60.0; ALMasterLinks links; @@ -223,26 +245,36 @@ namespace tut links.put(one); } - ensure_equals("one pruned", links.prune(now), size_t(1)); - ensure("the old orphan", !links.of(almasterlinks_data::key(1), almasterlinks_data::key(10))); - ensure("not the young one", links.of(almasterlinks_data::key(1), almasterlinks_data::key(11)) != nullptr); - ensure("nor any not orphaned, however old", - links.of(almasterlinks_data::key(1), almasterlinks_data::key(12)) && links.of(almasterlinks_data::key(1), almasterlinks_data::key(13))); + CHECK(links.prune(now) == size_t(1)); + UNSCOPED_INFO("the old orphan"); + REQUIRE_FALSE(links.of(almasterlinks_data::key(1), almasterlinks_data::key(10))); + UNSCOPED_INFO("not the young one"); + REQUIRE(links.of(almasterlinks_data::key(1), almasterlinks_data::key(11)) != nullptr); + { + INFO("nor any not orphaned, however old"); + REQUIRE(links.of(almasterlinks_data::key(1), almasterlinks_data::key(12))); + REQUIRE(links.of(almasterlinks_data::key(1), almasterlinks_data::key(13))); + } const ALMasterLink* dated = links.of(almasterlinks_data::key(1), almasterlinks_data::key(14)); - ensure("one with no date is kept", dated != nullptr); - ensure("and dated now", dated->orphanedSince == now); - ensure_equals("found by its master still", links.mastering("/s/b.lsl").size(), size_t(1)); - - ensure_equals("fewer days where asked", links.prune(now, 30.0), size_t(1)); - ensure("the young one now", !links.of(almasterlinks_data::key(1), almasterlinks_data::key(11))); - ensure_equals("the one dated now goes in its time", links.prune(LLDate(now.secondsSinceEpoch() + 91 * day)), size_t(1)); - ensure_equals("and nothing else", links.size(), size_t(2)); + UNSCOPED_INFO("one with no date is kept"); + REQUIRE(dated != nullptr); + UNSCOPED_INFO("and dated now"); + CHECK(dated->orphanedSince == now); + UNSCOPED_INFO("found by its master still"); + REQUIRE(links.mastering("/s/b.lsl").size() == size_t(1)); + + UNSCOPED_INFO("fewer days where asked"); + CHECK(links.prune(now, 30.0) == size_t(1)); + UNSCOPED_INFO("the young one now"); + REQUIRE_FALSE(links.of(almasterlinks_data::key(1), almasterlinks_data::key(11))); + UNSCOPED_INFO("the one dated now goes in its time"); + CHECK(links.prune(LLDate(now.secondsSinceEpoch() + 91 * day)) == size_t(1)); + UNSCOPED_INFO("and nothing else"); + REQUIRE(links.size() == size_t(2)); } - template<> template<> - void almasterlinks_object::test<5>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: the LLSD is versioned and every field round-trips through notation", "[almasterlinks]") { - set_test_name("the LLSD is versioned, and every field comes back, through notation too"); ALMasterLinks links; ALMasterLink full; full.object = almasterlinks_data::key(1); @@ -275,54 +307,73 @@ namespace tut } const LLSD llsd = links.toLLSD(); - ensure_equals("version 1", llsd["version"].asInteger(), 1); - ensure_equals("every link", llsd["links"].size(), 5); + CHECK(llsd["version"].asInteger() == 1); + UNSCOPED_INFO("every link"); + REQUIRE(llsd["links"].size() == 5); std::ostringstream out; LLSDSerialize::toNotation(llsd, out); std::istringstream in(out.str()); LLSD back; - ensure("as notation", LLSDSerialize::fromNotation(back, in, static_cast(out.str().size())) > 0); + REQUIRE(LLSDSerialize::fromNotation(back, in, static_cast(out.str().size())) > 0); const ALMasterLinks read = ALMasterLinks::fromLLSD(back); - ensure_equals("all read", read.size(), size_t(5)); + UNSCOPED_INFO("all read"); + REQUIRE(read.size() == size_t(5)); const ALMasterLink* got = read.of(full.object, full.item); - ensure("the full one", got != nullptr); - ensure_equals("master", got->master, full.master); - ensure("made", got->made == full.made); - ensure("lua", got->lua); - ensure_equals("target", got->target, full.target); - ensure_equals("base", got->base, full.base); - ensure_equals("hash", got->hash, full.hash); - ensure_equals("stamp, to the nanosecond", got->stamp, full.stamp); - ensure_equals("uses", almasterlinks_data::joined(got->uses), almasterlinks_data::joined(full.uses)); - ensure_equals("object name", got->objectName, full.objectName); - ensure_equals("item name", got->itemName, full.itemName); - ensure_equals("region name", got->regionName, full.regionName); - ensure("linked", got->linked == full.linked); - ensure("state", got->state == full.state); - ensure("orphaned since", got->orphanedSince == full.orphanedSince); + UNSCOPED_INFO("the full one"); + REQUIRE(got != nullptr); + CHECK(got->master == full.master); + CHECK(got->made == full.made); + CHECK(got->lua); + CHECK(got->target == full.target); + CHECK(got->base == full.base); + CHECK(got->hash == full.hash); + UNSCOPED_INFO("stamp, to the nanosecond"); + CHECK(got->stamp == full.stamp); + CHECK(almasterlinks_data::joined(got->uses) == almasterlinks_data::joined(full.uses)); + CHECK(got->objectName == full.objectName); + CHECK(got->itemName == full.itemName); + CHECK(got->regionName == full.regionName); + CHECK(got->linked == full.linked); + CHECK(got->state == full.state); + CHECK(got->orphanedSince == full.orphanedSince); const ALMasterLink* plain = read.of(LLUUID::null, inventory.item); - ensure("the inventory's, with no object", plain && plain->object.isNull() && plain->state == ALMasterLink::State::Differing); - ensure("no date where none was", plain->orphanedSince.isNull() && !back["links"][1].has("orphaned_since")); + { + INFO("the inventory's, with no object"); + REQUIRE(plain); + CHECK(plain->object.isNull()); + CHECK(plain->state == ALMasterLink::State::Differing); + } + { + INFO("no date where none was"); + CHECK(plain->orphanedSince.isNull()); + REQUIRE_FALSE(back["links"][1].has("orphaned_since")); + } for (const ALMasterLink::State state : { ALMasterLink::State::Active, ALMasterLink::State::Pending, ALMasterLink::State::Suspended }) { const ALMasterLink* one = read.of(almasterlinks_data::key(5), almasterlinks_data::key(10 + static_cast(state))); - ensure("each state", one && one->state == state && one->made == ALMasterLink::Made::Hint); + CAPTURE(state); + REQUIRE(one); + REQUIRE(one->state == state); + REQUIRE(one->made == ALMasterLink::Made::Hint); } - ensure_equals("found by their masters", read.mastering("/s/x.lsl").size(), size_t(3)); - ensure_equals("and their uses", read.usersOf("disk:/s/lib/b.luau").size(), size_t(1)); + UNSCOPED_INFO("found by their masters"); + REQUIRE(read.mastering("/s/x.lsl").size() == size_t(3)); + UNSCOPED_INFO("and their uses"); + REQUIRE(read.usersOf("disk:/s/lib/b.luau").size() == size_t(1)); } - template<> template<> - void almasterlinks_object::test<6>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: malformed records are skipped and unknown values are read carefully", "[almasterlinks]") { - set_test_name("a malformed record is passed over and the rest read; what is not known is read carefully"); - ensure("not a map", ALMasterLinks::fromLLSD(LLSD("links")).empty()); - ensure("no links", ALMasterLinks::fromLLSD(LLSD::emptyMap()).empty()); + UNSCOPED_INFO("not a map"); + REQUIRE(ALMasterLinks::fromLLSD(LLSD("links")).empty()); + UNSCOPED_INFO("no links"); + REQUIRE(ALMasterLinks::fromLLSD(LLSD::emptyMap()).empty()); LLSD wrong; wrong["links"] = "none"; - ensure("links not a list", ALMasterLinks::fromLLSD(wrong).empty()); + UNSCOPED_INFO("links not a list"); + REQUIRE(ALMasterLinks::fromLLSD(wrong).empty()); const auto record = [](const std::string& object, const std::string& item, const LLSD& master) { LLSD out; @@ -362,33 +413,49 @@ namespace tut links.append(record(object, almasterlinks_data::key(5).asString(), "/s/later.lsl")); const ALMasterLinks read = ALMasterLinks::fromLLSD(llsd); - ensure_equals("the two whole records", read.size(), size_t(2)); - ensure("not one whose object is no key, read as the inventory's", !read.of(LLUUID::null, almasterlinks_data::key(4))); + UNSCOPED_INFO("the two whole records"); + REQUIRE(read.size() == size_t(2)); + UNSCOPED_INFO("not one whose object is no key, read as the inventory's"); + REQUIRE_FALSE(read.of(LLUUID::null, almasterlinks_data::key(4))); const ALMasterLink* later = read.of(almasterlinks_data::key(1), almasterlinks_data::key(5)); - ensure("an item twice is read once, the later", later && later->master == "/s/later.lsl"); + { + INFO("an item twice is read once, the later"); + REQUIRE(later); + CHECK(later->master == "/s/later.lsl"); + } const ALMasterLink* plain = read.of(LLUUID::null, almasterlinks_data::key(6)); - ensure("one with no object is the inventory's", plain != nullptr); - ensure("what it does not say is as a new link has it", plain->made == ALMasterLink::Made::Picked && plain->state == ALMasterLink::State::Active); - ensure_equals("a stamp written as an integer", plain->stamp, S64(12345)); + UNSCOPED_INFO("one with no object is the inventory's"); + REQUIRE(plain != nullptr); + { + INFO("what it does not say is as a new link has it"); + CHECK(plain->made == ALMasterLink::Made::Picked); + CHECK(plain->state == ALMasterLink::State::Active); + } + UNSCOPED_INFO("a stamp written as an integer"); + CHECK(plain->stamp == S64(12345)); // The earlier of the item twice, alone. LLSD once; once["links"].append(good); const ALMasterLinks careful = ALMasterLinks::fromLLSD(once); const ALMasterLink* odd = careful.of(almasterlinks_data::key(1), almasterlinks_data::key(5)); - ensure("read", odd != nullptr); - ensure("a way of making not known is a hint's", odd->made == ALMasterLink::Made::Hint); - ensure("a state not known is suspended", odd->state == ALMasterLink::State::Suspended); - ensure("a stamp not a number is none", odd->stamp == 0); - ensure("a base not a key is none", odd->base.isNull()); - ensure_equals("only the uses that are names", almasterlinks_data::joined(odd->uses), std::string("disk:/s/lib/a.lsl")); - ensure("a version not given is read all the same", careful.size() == 1); + REQUIRE(odd != nullptr); + UNSCOPED_INFO("a way of making not known is a hint's"); + CHECK(odd->made == ALMasterLink::Made::Hint); + UNSCOPED_INFO("a state not known is suspended"); + CHECK(odd->state == ALMasterLink::State::Suspended); + UNSCOPED_INFO("a stamp not a number is none"); + CHECK(odd->stamp == 0); + UNSCOPED_INFO("a base not a key is none"); + CHECK(odd->base.isNull()); + UNSCOPED_INFO("only the uses that are names"); + CHECK(almasterlinks_data::joined(odd->uses) == std::string("disk:/s/lib/a.lsl")); + UNSCOPED_INFO("a version not given is read all the same"); + REQUIRE(careful.size() == 1); } - template<> template<> - void almasterlinks_object::test<7>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: thousands of links over a few masters and includes are found by each", "[almasterlinks]") { - set_test_name("thousands of links over a few masters and includes are found by each"); ALMasterLinks links; constexpr int LINKS = 5000; constexpr int MASTERS = 50; @@ -398,29 +465,35 @@ namespace tut one.uses = { "disk:/s/lib/common.lsl", "disk:/s/lib/part" + std::to_string(n % 7) + ".lsl" }; links.put(one); } - ensure_equals("all kept", links.size(), size_t(LINKS)); + UNSCOPED_INFO("all kept"); + REQUIRE(links.size() == size_t(LINKS)); for (int m = 0; m < MASTERS; ++m) { const std::vector found = links.mastering("/s/master" + std::to_string(m) + ".lsl"); - ensure_equals("each master's", found.size(), size_t(LINKS / MASTERS)); - ensure("each the master's", found.front()->master == "/s/master" + std::to_string(m) + ".lsl"); + CAPTURE(m); + REQUIRE(found.size() == size_t(LINKS / MASTERS)); + REQUIRE(found.front()->master == "/s/master" + std::to_string(m) + ".lsl"); } - ensure_equals("every user of the include they share", links.usersOf("disk:/s/lib/common.lsl").size(), size_t(LINKS)); - ensure_equals("and of one a seventh read", links.usersOf("/s/lib/part3.lsl").size(), size_t((LINKS - 3 + 6) / 7)); + UNSCOPED_INFO("every user of the include they share"); + REQUIRE(links.usersOf("disk:/s/lib/common.lsl").size() == size_t(LINKS)); + UNSCOPED_INFO("and of one a seventh read"); + REQUIRE(links.usersOf("/s/lib/part3.lsl").size() == size_t((LINKS - 3 + 6) / 7)); for (int n = 0; n < LINKS; n += 2) { links.remove(almasterlinks_data::key(1 + n / 100), almasterlinks_data::key(100000 + n)); } - ensure_equals("half removed", links.size(), size_t(LINKS / 2)); - ensure_equals("the rest found by their master", links.mastering("/s/master1.lsl").size(), size_t(LINKS / MASTERS)); - ensure("none removed found", links.mastering("/s/master0.lsl").empty()); - ensure("each found by its item", links.of(almasterlinks_data::key(1 + 4999 / 100), almasterlinks_data::key(104999)) != nullptr); + UNSCOPED_INFO("half removed"); + REQUIRE(links.size() == size_t(LINKS / 2)); + UNSCOPED_INFO("the rest found by their master"); + REQUIRE(links.mastering("/s/master1.lsl").size() == size_t(LINKS / MASTERS)); + UNSCOPED_INFO("none removed found"); + REQUIRE(links.mastering("/s/master0.lsl").empty()); + UNSCOPED_INFO("each found by its item"); + REQUIRE(links.of(almasterlinks_data::key(1 + 4999 / 100), almasterlinks_data::key(104999)) != nullptr); } - template<> template<> - void almasterlinks_object::test<8>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: the missed flag round-trips through the LLSD and is written only where set", "[almasterlinks]") { - set_test_name("whether the last expansion missed something comes back through the LLSD, and is written only where set"); ALMasterLinks links; ALMasterLink missing = almasterlinks_data::link(1, 10, "/s/door.lsl"); missing.missed = true; @@ -428,18 +501,23 @@ namespace tut links.put(almasterlinks_data::link(1, 11, "/s/lamp.lsl")); const LLSD llsd = links.toLLSD(); - ensure_equals("still version 1", llsd["version"].asInteger(), 1); - ensure("written where set", llsd["links"][0]["missed"].asBoolean()); - ensure("and not where not", !llsd["links"][1].has("missed")); + UNSCOPED_INFO("still version 1"); + CHECK(llsd["version"].asInteger() == 1); + UNSCOPED_INFO("written where set"); + CHECK(llsd["links"][0]["missed"].asBoolean()); + UNSCOPED_INFO("and not where not"); + REQUIRE_FALSE(llsd["links"][1].has("missed")); std::ostringstream out; LLSDSerialize::toNotation(llsd, out); std::istringstream in(out.str()); LLSD back; - ensure("as notation", LLSDSerialize::fromNotation(back, in, static_cast(out.str().size())) > 0); + REQUIRE(LLSDSerialize::fromNotation(back, in, static_cast(out.str().size())) > 0); const ALMasterLinks read = ALMasterLinks::fromLLSD(back); - ensure("read back set", read.of(almasterlinks_data::key(1), almasterlinks_data::key(10))->missed); - ensure("and not", !read.of(almasterlinks_data::key(1), almasterlinks_data::key(11))->missed); + UNSCOPED_INFO("read back set"); + CHECK(read.of(almasterlinks_data::key(1), almasterlinks_data::key(10))->missed); + UNSCOPED_INFO("and not"); + CHECK_FALSE(read.of(almasterlinks_data::key(1), almasterlinks_data::key(11))->missed); // As an older version wrote it, with no such field, and as it might // be written otherwise. @@ -455,16 +533,18 @@ namespace tut record["missed"] = LLSD::emptyMap(); older["links"].append(record); const ALMasterLinks old = ALMasterLinks::fromLLSD(older); - ensure_equals("all read", old.size(), size_t(3)); - ensure("none said is none missed", !old.of(LLUUID::null, almasterlinks_data::key(12))->missed); - ensure("a number is read as one", old.of(LLUUID::null, almasterlinks_data::key(13))->missed); - ensure("what is no value is none", !old.of(LLUUID::null, almasterlinks_data::key(14))->missed); + UNSCOPED_INFO("all read"); + REQUIRE(old.size() == size_t(3)); + UNSCOPED_INFO("none said is none missed"); + CHECK_FALSE(old.of(LLUUID::null, almasterlinks_data::key(12))->missed); + UNSCOPED_INFO("a number is read as one"); + CHECK(old.of(LLUUID::null, almasterlinks_data::key(13))->missed); + UNSCOPED_INFO("what is no value is none"); + CHECK_FALSE(old.of(LLUUID::null, almasterlinks_data::key(14))->missed); } - template<> template<> - void almasterlinks_object::test<9>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: affectedBy lists users and links that missed but none suspended or orphaned", "[almasterlinks]") { - set_test_name("an include's change may send its users and every link that missed something, each once, none suspended or orphaned"); ALMasterLinks links; const auto with = [&links](int item, const std::vector& uses, bool missed, ALMasterLink::State state) { ALMasterLink one = almasterlinks_data::link(1, item, "/s/master" + std::to_string(item) + ".lsl"); @@ -483,25 +563,28 @@ namespace tut with(16, { "disk:/s/lib/util.lsl" }, false, State::Differing); with(17, {}, true, State::Suspended); - ensure_equals("its users and those that missed, in the order put", - almasterlinks_data::items(links.affectedBy("disk:/s/lib/util.lsl")), std::string("10 11 14 16")); - ensure_equals("by its path alone the same", almasterlinks_data::items(links.affectedBy("/s/lib/util.lsl")), + UNSCOPED_INFO("its users and those that missed, in the order put"); + CHECK(almasterlinks_data::items(links.affectedBy("disk:/s/lib/util.lsl")) == std::string("10 11 14 16")); + UNSCOPED_INFO("by its path alone the same"); + CHECK(almasterlinks_data::items(links.affectedBy("/s/lib/util.lsl")) == std::string("10 11 14 16")); - ensure_equals("a file nobody read: those that missed", almasterlinks_data::items(links.affectedBy("/s/lib/new.lsl")), + UNSCOPED_INFO("a file nobody read: those that missed"); + CHECK(almasterlinks_data::items(links.affectedBy("/s/lib/new.lsl")) == std::string("11 14")); - ensure_equals("one both a user and missing is one", almasterlinks_data::items(links.affectedBy("/s/lib/net.lsl")), + UNSCOPED_INFO("one both a user and missing is one"); + CHECK(almasterlinks_data::items(links.affectedBy("/s/lib/net.lsl")) == std::string("11 14 15")); // Expanded again, and nothing missed now. links.find(almasterlinks_data::key(1), almasterlinks_data::key(14))->missed = false; - ensure_equals("no longer", almasterlinks_data::items(links.affectedBy("/s/lib/new.lsl")), std::string("11")); - ensure("nothing at all with nothing linked", ALMasterLinks().affectedBy("/s/lib/util.lsl").empty()); + UNSCOPED_INFO("no longer"); + CHECK(almasterlinks_data::items(links.affectedBy("/s/lib/new.lsl")) == std::string("11")); + UNSCOPED_INFO("nothing at all with nothing linked"); + REQUIRE(ALMasterLinks().affectedBy("/s/lib/util.lsl").empty()); } - template<> template<> - void almasterlinks_object::test<10>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: watched lists live and suspended masters and live disk includes once each", "[almasterlinks]") { - set_test_name("the files to watch: live and suspended links' masters, live ones' disk includes, each once, a master as a master"); ALMasterLinks links; const auto with = [&links](int item, const std::string& master, const std::vector& uses, ALMasterLink::State state) { ALMasterLink one = almasterlinks_data::link(1, item, master); @@ -524,21 +607,22 @@ namespace tut with(16, "C:\\S\\G.lsl", { "disk:/s/a.lsl" }, State::Active); with(17, "c:/s/g.lsl", {}, State::Active); - ensure_equals("each once, in the order met", almasterlinks_data::watched(links.watched()), + UNSCOPED_INFO("each once, in the order met"); + CHECK(almasterlinks_data::watched(links.watched()) == std::string("/s/a.lsl* /s/lib/u.lsl /s/lib/v.lsl /s/b.lsl* /s/c.lsl* /s/c2.lsl* /s/e.lsl* C:\\S\\G.lsl*")); - ensure("nothing with nothing linked", ALMasterLinks().watched().empty()); + UNSCOPED_INFO("nothing with nothing linked"); + REQUIRE(ALMasterLinks().watched().empty()); // Orphaned, the file it masters is watched no longer. links.find(almasterlinks_data::key(1), almasterlinks_data::key(14))->state = State::Orphaned; links.find(almasterlinks_data::key(1), almasterlinks_data::key(13))->state = State::Orphaned; - ensure_equals("an include again where it no longer masters", almasterlinks_data::watched(links.watched()), + UNSCOPED_INFO("an include again where it no longer masters"); + CHECK(almasterlinks_data::watched(links.watched()) == std::string("/s/a.lsl* /s/lib/u.lsl /s/lib/v.lsl /s/b.lsl* /s/c.lsl* /s/c2.lsl C:\\S\\G.lsl*")); } - template<> template<> - void almasterlinks_object::test<11>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: orphansNamed lists an item name's orphans by object name then latest", "[almasterlinks]") { - set_test_name("the orphaned links of an item's name, those of the object's name first, then the latest"); ALMasterLinks links; const auto orphan = [&links](int item, const std::string& item_name, const std::string& object_name, F64 since) { ALMasterLink one = almasterlinks_data::link(1, item, "/s/" + std::to_string(item) + ".lsl"); @@ -561,29 +645,34 @@ namespace tut links.put(live); orphan(17, "door", "Shed", 3000.0); - ensure_equals("the object's first, then the latest, as put among equals", - almasterlinks_data::items(links.orphansNamed("door", "Door")), std::string("12 11 13 17 10")); - ensure_equals("an object of no such name: the latest first", - almasterlinks_data::items(links.orphansNamed("door", "Window")), std::string("13 17 12 10 11")); - ensure_equals("names compared as written", almasterlinks_data::items(links.orphansNamed("Door", "door")), std::string("15")); - ensure("an item of no such name has none", links.orphansNamed("window", "Door").empty()); - ensure("nor an item with no name", links.orphansNamed("", "Door").empty()); + UNSCOPED_INFO("the object's first, then the latest, as put among equals"); + CHECK(almasterlinks_data::items(links.orphansNamed("door", "Door")) == std::string("12 11 13 17 10")); + UNSCOPED_INFO("an object of no such name: the latest first"); + CHECK(almasterlinks_data::items(links.orphansNamed("door", "Window")) == std::string("13 17 12 10 11")); + UNSCOPED_INFO("names compared as written"); + CHECK(almasterlinks_data::items(links.orphansNamed("Door", "door")) == std::string("15")); + UNSCOPED_INFO("an item of no such name has none"); + REQUIRE(links.orphansNamed("window", "Door").empty()); + UNSCOPED_INFO("nor an item with no name"); + REQUIRE(links.orphansNamed("", "Door").empty()); } - template<> template<> - void almasterlinks_object::test<12>() + TEST_CASE_METHOD(almasterlinks_data, "almasterlinks: a notecard link round-trips through the LLSD and a script's omits the flag", "[almasterlinks]") { - set_test_name("a notecard's link comes back through the LLSD as one, and a script's says nothing of it"); ALMasterLinks links; ALMasterLink note = almasterlinks_data::link(1, 10, "/s/readme.txt"); note.notecard = true; links.put(note); links.put(almasterlinks_data::link(1, 11, "/s/lamp.lsl")); const LLSD llsd = links.toLLSD(); - ensure("written where set", llsd["links"][0]["notecard"].asBoolean()); - ensure("and not where not", !llsd["links"][1].has("notecard")); + UNSCOPED_INFO("written where set"); + CHECK(llsd["links"][0]["notecard"].asBoolean()); + UNSCOPED_INFO("and not where not"); + REQUIRE_FALSE(llsd["links"][1].has("notecard")); const ALMasterLinks read = ALMasterLinks::fromLLSD(llsd); - ensure("read back a notecard's", read.of(almasterlinks_data::key(1), almasterlinks_data::key(10))->notecard); - ensure("and a script's", !read.of(almasterlinks_data::key(1), almasterlinks_data::key(11))->notecard); + UNSCOPED_INFO("read back a notecard's"); + CHECK(read.of(almasterlinks_data::key(1), almasterlinks_data::key(10))->notecard); + UNSCOPED_INFO("and a script's"); + CHECK_FALSE(read.of(almasterlinks_data::key(1), almasterlinks_data::key(11))->notecard); } } diff --git a/indra/alscript/tests/almastermatch_test.cpp b/indra/alscript/tests/almastermatch_test.cpp index 074b278a47..ff03c3bbfa 100644 --- a/indra/alscript/tests/almastermatch_test.cpp +++ b/indra/alscript/tests/almastermatch_test.cpp @@ -29,7 +29,7 @@ #include "fsyspath.h" #include "llfile.h" -#include "../test/lltut.h" +#include #include #include @@ -76,7 +76,7 @@ namespace using Aliases = std::vector>; } -namespace tut +namespace { struct almastermatch_data { @@ -103,21 +103,19 @@ namespace tut } }; - typedef test_group almastermatch_group; - typedef almastermatch_group::object almastermatch_object; - almastermatch_group almastermatch_instance("almastermatch"); - - template<> template<> - void almastermatch_object::test<1>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: a hint is an @file comment in the first ten lines or an upload header's", "[almastermatch]") { - set_test_name("a hint is an @file comment among the first ten lines of the source, else an upload header's"); - ensure_equals("LSL's", ALMasterMatch::hintOf("// @file net/door.lsl\ndefault {}\n", "", false).value_or("-"), std::string("net/door.lsl")); - ensure_equals("SLua's", ALMasterMatch::hintOf("-- @file net/door.luau\nprint(1)\n", "", true).value_or("-"), std::string("net/door.luau")); - ensure_equals("indented, close up, in any case, the line's end trimmed", - ALMasterMatch::hintOf(" //@FILE\tnet/door.lsl \r\n", "", false).value_or("-"), std::string("net/door.lsl")); - ensure_equals("quoted for its spaces", ALMasterMatch::hintOf("// @file \"my scripts/door.lsl\"\n", "", false).value_or("-"), + UNSCOPED_INFO("LSL's"); + CHECK(ALMasterMatch::hintOf("// @file net/door.lsl\ndefault {}\n", "", false).value_or("-") == std::string("net/door.lsl")); + UNSCOPED_INFO("SLua's"); + CHECK(ALMasterMatch::hintOf("-- @file net/door.luau\nprint(1)\n", "", true).value_or("-") == std::string("net/door.luau")); + UNSCOPED_INFO("indented, close up, in any case, the line's end trimmed"); + CHECK(ALMasterMatch::hintOf(" //@FILE\tnet/door.lsl \r\n", "", false).value_or("-") == std::string("net/door.lsl")); + UNSCOPED_INFO("quoted for its spaces"); + CHECK(ALMasterMatch::hintOf("// @file \"my scripts/door.lsl\"\n", "", false).value_or("-") == std::string("my scripts/door.lsl")); - ensure_equals("past a byte order mark", ALMasterMatch::hintOf("\xEF\xBB\xBF// @file door.lsl\n", "", false).value_or("-"), + UNSCOPED_INFO("past a byte order mark"); + CHECK(ALMasterMatch::hintOf("\xEF\xBB\xBF// @file door.lsl\n", "", false).value_or("-") == std::string("door.lsl")); std::string ninth; @@ -125,15 +123,22 @@ namespace tut { ninth += "integer x" + std::to_string(line) + ";\n"; } - ensure_equals("the tenth line", ALMasterMatch::hintOf(ninth + "// @file late.lsl\n", "", false).value_or("-"), std::string("late.lsl")); - ensure("not the eleventh", !ALMasterMatch::hintOf(ninth + "\n// @file late.lsl\n", "", false)); - ensure("not the other language's comment", !ALMasterMatch::hintOf("// @file door.luau\n", "", true)); - ensure("nor code", !ALMasterMatch::hintOf("string f = \"// @file door.lsl\";\n", "", false)); - ensure("nor a word that only begins so", !ALMasterMatch::hintOf("// @filename door.lsl\n", "", false)); - ensure("nor a doc comment's third dash", !ALMasterMatch::hintOf("--- @file door.luau\n", "", true)); - ensure("nor one naming nothing", !ALMasterMatch::hintOf("// @file \n", "", false)); - ensure_equals("passing one naming nothing for the next", - ALMasterMatch::hintOf("// @file\t\n// @file door.lsl\n", "", false).value_or("-"), std::string("door.lsl")); + UNSCOPED_INFO("the tenth line"); + CHECK(ALMasterMatch::hintOf(ninth + "// @file late.lsl\n", "", false).value_or("-") == std::string("late.lsl")); + UNSCOPED_INFO("not the eleventh"); + REQUIRE_FALSE(ALMasterMatch::hintOf(ninth + "\n// @file late.lsl\n", "", false)); + UNSCOPED_INFO("not the other language's comment"); + REQUIRE_FALSE(ALMasterMatch::hintOf("// @file door.luau\n", "", true)); + UNSCOPED_INFO("nor code"); + REQUIRE_FALSE(ALMasterMatch::hintOf("string f = \"// @file door.lsl\";\n", "", false)); + UNSCOPED_INFO("nor a word that only begins so"); + REQUIRE_FALSE(ALMasterMatch::hintOf("// @filename door.lsl\n", "", false)); + UNSCOPED_INFO("nor a doc comment's third dash"); + REQUIRE_FALSE(ALMasterMatch::hintOf("--- @file door.luau\n", "", true)); + UNSCOPED_INFO("nor one naming nothing"); + REQUIRE_FALSE(ALMasterMatch::hintOf("// @file \n", "", false)); + UNSCOPED_INFO("passing one naming nothing for the next"); + CHECK(ALMasterMatch::hintOf("// @file\t\n// @file door.lsl\n", "", false).value_or("-") == std::string("door.lsl")); const std::string ours = "// ================ alchemy meta ================\n" "// @file net/door.lsl\n" @@ -143,23 +148,27 @@ namespace tut "-- @file src/door.luau\n" "-- @hash 00\n" "-- ==================================================\n"; - ensure_equals("our header in the compiled half", ALMasterMatch::hintOf("default {}\n", ours + "default {}\n", false).value_or("-"), + UNSCOPED_INFO("our header in the compiled half"); + CHECK(ALMasterMatch::hintOf("default {}\n", ours + "default {}\n", false).value_or("-") == std::string("net/door.lsl")); - ensure_equals("past the target line", ALMasterMatch::hintOf("default {}\n", "//mono\n" + ours + "default {}\n", false).value_or("-"), + UNSCOPED_INFO("past the target line"); + CHECK(ALMasterMatch::hintOf("default {}\n", "//mono\n" + ours + "default {}\n", false).value_or("-") == std::string("net/door.lsl")); - ensure_equals("the plugin's", ALMasterMatch::hintOf("print(1)\n", "--luau\n" + theirs + "print(1)\n", true).value_or("-"), + UNSCOPED_INFO("the plugin's"); + CHECK(ALMasterMatch::hintOf("print(1)\n", "--luau\n" + theirs + "print(1)\n", true).value_or("-") == std::string("src/door.luau")); - ensure_equals("the source's own first", ALMasterMatch::hintOf("// @file mine.lsl\n", ours, false).value_or("-"), std::string("mine.lsl")); - ensure("not a header further down", !ALMasterMatch::hintOf("default {}\n", "default {}\n" + ours, false)); - ensure("nor one with no @file", - !ALMasterMatch::hintOf("x\n", "// ================ alchemy meta ================\n// @hash xxh128:00\n// ====\n", false)); - ensure("nothing where nothing says", !ALMasterMatch::hintOf("default {}\n", "default {}\n", false)); + UNSCOPED_INFO("the source's own first"); + CHECK(ALMasterMatch::hintOf("// @file mine.lsl\n", ours, false).value_or("-") == std::string("mine.lsl")); + UNSCOPED_INFO("not a header further down"); + REQUIRE_FALSE(ALMasterMatch::hintOf("default {}\n", "default {}\n" + ours, false)); + UNSCOPED_INFO("nor one with no @file"); + REQUIRE_FALSE(ALMasterMatch::hintOf("x\n", "// ================ alchemy meta ================\n// @hash xxh128:00\n// ====\n", false)); + UNSCOPED_INFO("nothing where nothing says"); + REQUIRE_FALSE(ALMasterMatch::hintOf("default {}\n", "default {}\n", false)); } - template<> template<> - void almastermatch_object::test<2>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: a relative hint resolves in the first blessed folder that has it", "[almastermatch]") { - set_test_name("a relative hint is looked for in each blessed folder in turn, and the first found wins"); Scratch s; const std::string first = s.write("a/net/door.lsl"); const std::string only = s.write("b/only.lsl"); @@ -168,26 +177,32 @@ namespace tut blessed.bless(s.at("a")); blessed.bless(s.at("b")); - ensure_equals("in both, the first folder's", almastermatch_data::resolved("net/door.lsl", blessed, false), first); - ensure_equals("with Windows's separators", almastermatch_data::resolved("net\\door.lsl", blessed, false), first); - ensure_equals("in the second only, the second's", almastermatch_data::resolved("only.lsl", blessed, false), only); - ensure_equals("a `.` on the way", almastermatch_data::resolved("./net/./door.lsl", blessed, false), first); - ensure_equals("in neither", almastermatch_data::resolved("missing.lsl", blessed, false), + UNSCOPED_INFO("in both, the first folder's"); + CHECK(almastermatch_data::resolved("net/door.lsl", blessed, false) == first); + UNSCOPED_INFO("with Windows's separators"); + CHECK(almastermatch_data::resolved("net\\door.lsl", blessed, false) == first); + UNSCOPED_INFO("in the second only, the second's"); + CHECK(almastermatch_data::resolved("only.lsl", blessed, false) == only); + UNSCOPED_INFO("a `.` on the way"); + CHECK(almastermatch_data::resolved("./net/./door.lsl", blessed, false) == first); + UNSCOPED_INFO("in neither"); + CHECK(almastermatch_data::resolved("missing.lsl", blessed, false) == std::string("- 'missing.lsl' is not a file in the include folders, or not an ordinary file of at most 4 MB")); fs::create_directories(s.under("a/folder.lsl")); - ensure_equals("a folder is no file, whatever it is called", almastermatch_data::resolved("folder.lsl", blessed, false).substr(0, 1), + UNSCOPED_INFO("a folder is no file, whatever it is called"); + CHECK(almastermatch_data::resolved("folder.lsl", blessed, false).substr(0, 1) == std::string("-")); ALDiskIncludes none; - ensure_equals("no folder blessed, looked for nowhere", almastermatch_data::resolved("only.lsl", none, false), + UNSCOPED_INFO("no folder blessed, looked for nowhere"); + CHECK(almastermatch_data::resolved("only.lsl", none, false) == std::string("- no include folders are set, so 'only.lsl' is looked for nowhere")); - ensure_equals("a hint naming nothing", almastermatch_data::resolved(" ", blessed, false), std::string("- the @file hint names no file")); + UNSCOPED_INFO("a hint naming nothing"); + CHECK(almastermatch_data::resolved(" ", blessed, false) == std::string("- the @file hint names no file")); } - template<> template<> - void almastermatch_object::test<3>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: no climb or root or link or share reaches outside the blessed folders", "[almastermatch]") { - set_test_name("nothing is reached outside the blessed folders: not by climbing, from a root, through a link, nor a share"); Scratch s; const std::string secret = s.write("private/secret.lsl", "// the author's own\n"); const std::string inside = s.write("a/net/door.lsl"); @@ -196,51 +211,60 @@ namespace tut blessed.bless(s.at("a")); blessed.bless(s.at("b")); - ensure_equals("climbing out of every blessed folder", almastermatch_data::resolved("../private/secret.lsl", blessed, false), + UNSCOPED_INFO("climbing out of every blessed folder"); + CHECK(almastermatch_data::resolved("../private/secret.lsl", blessed, false) == std::string("- '../private/secret.lsl' is outside the include folders")); - ensure_equals("however deep it starts", almastermatch_data::resolved("net/../../private/secret.lsl", blessed, false), + UNSCOPED_INFO("however deep it starts"); + CHECK(almastermatch_data::resolved("net/../../private/secret.lsl", blessed, false) == std::string("- 'net/../../private/secret.lsl' is outside the include folders")); - ensure_equals("climbing within one is no climb", almastermatch_data::resolved("net/../net/door.lsl", blessed, false), inside); - ensure_equals("nor into another blessed folder, as an include could read it", - almastermatch_data::resolved("../b/other.lsl", blessed, false), other); - - ensure_equals("from a root, under a blessed folder", almastermatch_data::resolved(inside, blessed, false), inside); - ensure_equals("from a root, outside", almastermatch_data::resolved(secret, blessed, false), + UNSCOPED_INFO("climbing within one is no climb"); + CHECK(almastermatch_data::resolved("net/../net/door.lsl", blessed, false) == inside); + UNSCOPED_INFO("nor into another blessed folder, as an include could read it"); + CHECK(almastermatch_data::resolved("../b/other.lsl", blessed, false) == other); + + UNSCOPED_INFO("from a root, under a blessed folder"); + CHECK(almastermatch_data::resolved(inside, blessed, false) == inside); + UNSCOPED_INFO("from a root, outside"); + CHECK(almastermatch_data::resolved(secret, blessed, false) == "- '" + secret + "' is outside the include folders"); - ensure_equals("from a root, climbing out", almastermatch_data::resolved(s.at("a/../private/secret.lsl"), blessed, false).substr(0, 1), + UNSCOPED_INFO("from a root, climbing out"); + CHECK(almastermatch_data::resolved(s.at("a/../private/secret.lsl"), blessed, false).substr(0, 1) == std::string("-")); for (const char* elsewhere : { "\\\\host\\share\\door.lsl", "//host/share/door.lsl", "\\\\?\\UNC\\host\\share\\door.lsl", "C:\\door.lsl", "c:door.lsl" }) { - ensure_equals(std::string("never asked of: ") + elsewhere, almastermatch_data::resolved(elsewhere, blessed, false), + CAPTURE(elsewhere); + REQUIRE(almastermatch_data::resolved(elsewhere, blessed, false) == "- '" + std::string(elsewhere) + "' is outside the include folders"); } - ensure_equals("nor a home folder", almastermatch_data::resolved("~/door.lsl", blessed, false), + UNSCOPED_INFO("nor a home folder"); + CHECK(almastermatch_data::resolved("~/door.lsl", blessed, false) == std::string("- '~/door.lsl' is from a home folder, which a hint is not followed to")); std::error_code ec; fs::create_symlink(fsyspath(secret), s.root / "a" / "innocent.lsl", ec); if (!ec) { - ensure_equals("a link out of the folder", almastermatch_data::resolved("innocent.lsl", blessed, false).substr(0, 1), std::string("-")); + UNSCOPED_INFO("a link out of the folder"); + CHECK(almastermatch_data::resolved("innocent.lsl", blessed, false).substr(0, 1) == std::string("-")); } fs::create_directory_symlink(s.root / "private", s.root / "a" / "door", ec); if (!ec) { - ensure_equals("nor a file through a linked folder", almastermatch_data::resolved("door/secret.lsl", blessed, false).substr(0, 1), + UNSCOPED_INFO("nor a file through a linked folder"); + CHECK(almastermatch_data::resolved("door/secret.lsl", blessed, false).substr(0, 1) == std::string("-")); } fs::create_symlink(fsyspath(other), s.root / "a" / "inward.lsl", ec); if (!ec) { - ensure_equals("a link to another blessed folder is followed", almastermatch_data::resolved("inward.lsl", blessed, false), other); + UNSCOPED_INFO("a link to another blessed folder is followed"); + CHECK(almastermatch_data::resolved("inward.lsl", blessed, false) == other); } } - template<> template<> - void almastermatch_object::test<4>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: only a script of the item's language and a sensible size", "[almastermatch]") { - set_test_name("only a script of the item's language, of a sensible size"); Scratch s; const std::string notes = s.write("a/notes.txt", "keys\n"); const std::string door = s.write("a/door.lsl"); @@ -251,37 +275,46 @@ namespace tut ALDiskIncludes blessed; blessed.bless(s.at("a")); - ensure_equals("not another kind of file", almastermatch_data::resolved("notes.txt", blessed, false), + UNSCOPED_INFO("not another kind of file"); + CHECK(almastermatch_data::resolved("notes.txt", blessed, false) == std::string("- 'notes.txt' is not an LSL script (.lsl, .lslh or .lsli)")); - ensure_equals("nor a name with no extension", almastermatch_data::resolved("door", blessed, false).substr(0, 1), std::string("-")); - ensure_equals("LSL's own", almastermatch_data::resolved("door.lsl", blessed, false), door); + UNSCOPED_INFO("nor a name with no extension"); + CHECK(almastermatch_data::resolved("door", blessed, false).substr(0, 1) == std::string("-")); + UNSCOPED_INFO("LSL's own"); + CHECK(almastermatch_data::resolved("door.lsl", blessed, false) == door); // As the disk finds it: where it is the same file in any case, by // the case the disk keeps it in. const std::string upper = almastermatch_data::resolved("door.LSL", blessed, false); - ensure("an extension in any case", fs::exists(s.under("a/door.LSL")) ? upper == door : upper.substr(0, 1) == "-"); - ensure_equals("and its includes'", almastermatch_data::resolved("lib.lslh", blessed, false), header); - ensure_equals("not SLua's for LSL", almastermatch_data::resolved("util.luau", blessed, false), + UNSCOPED_INFO("an extension in any case"); + CHECK((fs::exists(s.under("a/door.LSL")) ? upper == door : upper.substr(0, 1) == "-")); + UNSCOPED_INFO("and its includes'"); + CHECK(almastermatch_data::resolved("lib.lslh", blessed, false) == header); + UNSCOPED_INFO("not SLua's for LSL"); + CHECK(almastermatch_data::resolved("util.luau", blessed, false) == std::string("- 'util.luau' is not an LSL script (.lsl, .lslh or .lsli)")); - ensure_equals("nor LSL's for SLua", almastermatch_data::resolved("door.lsl", blessed, true), + UNSCOPED_INFO("nor LSL's for SLua"); + CHECK(almastermatch_data::resolved("door.lsl", blessed, true) == std::string("- 'door.lsl' is not an SLua script (.luau or .lua)")); - ensure_equals("SLua's own", almastermatch_data::resolved("util.luau", blessed, true), util); - ensure_equals("and its older", almastermatch_data::resolved("old.lua", blessed, true), old); - ensure_equals("not one too big", almastermatch_data::resolved("big.lsl", blessed, false), + UNSCOPED_INFO("SLua's own"); + CHECK(almastermatch_data::resolved("util.luau", blessed, true) == util); + UNSCOPED_INFO("and its older"); + CHECK(almastermatch_data::resolved("old.lua", blessed, true) == old); + UNSCOPED_INFO("not one too big"); + CHECK(almastermatch_data::resolved("big.lsl", blessed, false) == std::string("- 'big.lsl' is not a file in the include folders, or not an ordinary file of at most 4 MB")); std::error_code ec; fs::create_symlink(fsyspath(notes), s.root / "a" / "fake.lsl", ec); if (!ec) { - ensure_equals("a script's name for another kind of file", almastermatch_data::resolved("fake.lsl", blessed, false), + UNSCOPED_INFO("a script's name for another kind of file"); + CHECK(almastermatch_data::resolved("fake.lsl", blessed, false) == std::string("- 'fake.lsl' leads to a file that is not an LSL script (.lsl, .lslh or .lsli)")); } } - template<> template<> - void almastermatch_object::test<5>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: an alias names a folder but is held to the blessed folders all the same", "[almastermatch]") { - set_test_name("an alias names a folder of the studio's or a configuration's, and is held to the blessed folders all the same"); Scratch s; const std::string util = s.write("a/lib/util.luau", "return {}\n"); s.write("private/x.luau", "return {}\n"); @@ -289,23 +322,28 @@ namespace tut blessed.bless(s.at("a")); const Aliases aliases{ { "lib", s.at("a/lib") }, { "@out", s.at("private") }, { "loose", "lib" } }; - ensure_equals("through the alias", almastermatch_data::resolved("@lib/util.luau", blessed, true, aliases), util); - ensure_equals("named in any case", almastermatch_data::resolved("@LIB/util.luau", blessed, true, aliases), util); - ensure_equals("one not set", almastermatch_data::resolved("@nope/util.luau", blessed, true, aliases), + UNSCOPED_INFO("through the alias"); + CHECK(almastermatch_data::resolved("@lib/util.luau", blessed, true, aliases) == util); + UNSCOPED_INFO("named in any case"); + CHECK(almastermatch_data::resolved("@LIB/util.luau", blessed, true, aliases) == util); + UNSCOPED_INFO("one not set"); + CHECK(almastermatch_data::resolved("@nope/util.luau", blessed, true, aliases) == std::string("- '@nope/util.luau' names the alias @nope, which is not set")); - ensure_equals("one whose folder is not blessed", almastermatch_data::resolved("@out/x.luau", blessed, true, aliases), + UNSCOPED_INFO("one whose folder is not blessed"); + CHECK(almastermatch_data::resolved("@out/x.luau", blessed, true, aliases) == std::string("- '@out/x.luau' is outside the include folders")); - ensure_equals("one climbed out of", almastermatch_data::resolved("@lib/../../private/x.luau", blessed, true, aliases), + UNSCOPED_INFO("one climbed out of"); + CHECK(almastermatch_data::resolved("@lib/../../private/x.luau", blessed, true, aliases) == std::string("- '@lib/../../private/x.luau' is outside the include folders")); - ensure_equals("one that is no folder on disk", almastermatch_data::resolved("@loose/util.luau", blessed, true, aliases), + UNSCOPED_INFO("one that is no folder on disk"); + CHECK(almastermatch_data::resolved("@loose/util.luau", blessed, true, aliases) == std::string("- the alias @loose is no folder on disk")); - ensure_equals("the alias alone is no file", almastermatch_data::resolved("@lib", blessed, true, aliases).substr(0, 1), std::string("-")); + UNSCOPED_INFO("the alias alone is no file"); + CHECK(almastermatch_data::resolved("@lib", blessed, true, aliases).substr(0, 1) == std::string("-")); } - template<> template<> - void almastermatch_object::test<6>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: byName lists . then the plugin's loose forms best first", "[almastermatch]") { - set_test_name("by name: . anywhere under a blessed folder, then the plugin's loose forms, every one best first"); Scratch s; const std::string door = s.write("a/door.lsl"); const std::string net_door = s.write("a/net/door.lsl"); @@ -322,34 +360,42 @@ namespace tut blessed.bless(s.at("a")); blessed.bless(s.at("b")); - ensure_equals("the folder blessed first, then the nearer its top; none hidden, none not blessed", - almastermatch_data::joined(ALMasterMatch::byName("door", blessed, false)), + UNSCOPED_INFO("the folder blessed first, then the nearer its top; none hidden, none not blessed"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("door", blessed, false)) == almastermatch_data::joined({ door, net_door, b_door })); - ensure_equals("a name with a folder in it, by its last parts", almastermatch_data::joined(ALMasterMatch::byName("net/door", blessed, false)), + UNSCOPED_INFO("a name with a folder in it, by its last parts"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("net/door", blessed, false)) == net_door); - ensure_equals("a name with its extension already", almastermatch_data::joined(ALMasterMatch::byName("door.lsl", blessed, false)), + UNSCOPED_INFO("a name with its extension already"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("door.lsl", blessed, false)) == almastermatch_data::joined({ door, net_door, b_door })); - ensure_equals("a name, then its folders run into it with _", almastermatch_data::joined(ALMasterMatch::byName("net_gate", blessed, false)), + UNSCOPED_INFO("a name, then its folders run into it with _"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("net_gate", blessed, false)) == almastermatch_data::joined({ flat, gate })); - ensure_equals("or with nothing", almastermatch_data::joined(ALMasterMatch::byName("netgate", blessed, false)), gate); - ensure_equals("or with a space", almastermatch_data::joined(ALMasterMatch::byName("net gate", blessed, false)), gate); - ensure_equals("its own case first, then any", almastermatch_data::joined(ALMasterMatch::byName("lamp", blessed, false)), + UNSCOPED_INFO("or with nothing"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("netgate", blessed, false)) == gate); + UNSCOPED_INFO("or with a space"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("net gate", blessed, false)) == gate); + UNSCOPED_INFO("its own case first, then any"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("lamp", blessed, false)) == almastermatch_data::joined({ lamp, big_lamp })); - ensure("not an include's header", ALMasterMatch::byName("util", blessed, false).empty()); - ensure("nor for no name", ALMasterMatch::byName("", blessed, false).empty()); - ensure("nor in no folder", ALMasterMatch::byName("door", ALDiskIncludes(), false).empty()); + UNSCOPED_INFO("not an include's header"); + REQUIRE(ALMasterMatch::byName("util", blessed, false).empty()); + UNSCOPED_INFO("nor for no name"); + REQUIRE(ALMasterMatch::byName("", blessed, false).empty()); + UNSCOPED_INFO("nor in no folder"); + REQUIRE(ALMasterMatch::byName("door", ALDiskIncludes(), false).empty()); const std::vector listed = ALMasterMatch::listing(blessed, false); - ensure_equals("listed once for many names, the same", - almastermatch_data::joined(ALMasterMatch::byName("door", listed, false)), + UNSCOPED_INFO("listed once for many names, the same"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("door", listed, false)) == almastermatch_data::joined(ALMasterMatch::byName("door", blessed, false))); - ensure("and given up where asked", ALMasterMatch::listing(blessed, false, [] { return true; }).empty()); + UNSCOPED_INFO("and given up where asked"); + REQUIRE(ALMasterMatch::listing(blessed, false, [] { return true; }).empty()); } - template<> template<> - void almastermatch_object::test<7>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: by name in SLua: .luau then .lua", "[almastermatch]") { - set_test_name("by name in SLua: .luau, then .lua"); Scratch s; const std::string luau = s.write("a/util.luau", "return {}\n"); const std::string lua = s.write("a/util.lua", "return {}\n"); @@ -357,20 +403,19 @@ namespace tut s.write("a/util.lsl"); ALDiskIncludes blessed; blessed.bless(s.at("a")); - ensure_equals("the modern first, nearer the top first among those", - almastermatch_data::joined(ALMasterMatch::byName("util", blessed, true)), almastermatch_data::joined({ luau, deep, lua })); - ensure_equals("and LSL's for LSL", ALMasterMatch::byName("util", blessed, false).size(), size_t(1)); + UNSCOPED_INFO("the modern first, nearer the top first among those"); + CHECK(almastermatch_data::joined(ALMasterMatch::byName("util", blessed, true)) == almastermatch_data::joined({ luau, deep, lua })); + UNSCOPED_INFO("and LSL's for LSL"); + REQUIRE(ALMasterMatch::byName("util", blessed, false).size() == size_t(1)); } - template<> template<> - void almastermatch_object::test<8>() + TEST_CASE_METHOD(almastermatch_data, "almastermatch: a notecard's master is text or notecard or JSON in any case; never a script", "[almastermatch]") { - set_test_name("a notecard's master is text, a notecard's own, or JSON, in any case, and never a script"); const std::vector& notecards = ALMasterMatch::notecardExtensions(); - ensure("text", ALDiskIncludes::extensionOf("readme.txt", notecards) != 0); - ensure("a notecard", ALDiskIncludes::extensionOf("Config.NOTECARD", notecards) != 0); - ensure("JSON", ALDiskIncludes::extensionOf("data/settings.json", notecards) != 0); - ensure("not a script", ALDiskIncludes::extensionOf("door.lsl", notecards) == 0); - ensure("nor SLua", ALDiskIncludes::extensionOf("door.luau", notecards) == 0); + CHECK(ALDiskIncludes::extensionOf("readme.txt", notecards) != 0); + CHECK(ALDiskIncludes::extensionOf("Config.NOTECARD", notecards) != 0); + CHECK(ALDiskIncludes::extensionOf("data/settings.json", notecards) != 0); + CHECK(ALDiskIncludes::extensionOf("door.lsl", notecards) == 0); + CHECK(ALDiskIncludes::extensionOf("door.luau", notecards) == 0); } } diff --git a/indra/alscript/tests/almasterplan_test.cpp b/indra/alscript/tests/almasterplan_test.cpp index 5dde281d26..a1d787774a 100644 --- a/indra/alscript/tests/almasterplan_test.cpp +++ b/indra/alscript/tests/almasterplan_test.cpp @@ -26,12 +26,12 @@ #include "../masters/almasterplan.h" -#include "../test/lltut.h" +#include #include #include -namespace tut +namespace { struct almasterplan_data { @@ -55,14 +55,8 @@ namespace tut } }; - typedef test_group almasterplan_group; - typedef almasterplan_group::object almasterplan_object; - almasterplan_group almasterplan_instance("almasterplan"); - - template<> template<> - void almasterplan_object::test<1>() + TEST_CASE_METHOD(almasterplan_data, "almasterplan: every send for every state of the world and the file and either setting", "[almasterplan]") { - set_test_name("every send, every state of the world and the file, and the setting"); using Send = almasterplan_data::Send; using Do = almasterplan_data::Do; struct Row @@ -114,49 +108,59 @@ namespace tut { Send::Derived, true, true, true, false, Do::Skip }, { Send::Derived, true, true, true, true, Do::Skip }, }; - ensure_equals("every row", sizeof(table) / sizeof(table[0]), size_t(32)); + UNSCOPED_INFO("every row"); + CHECK(sizeof(table) / sizeof(table[0]) == size_t(32)); for (const Row& row : table) { - const std::string name = std::string(row.kind == Send::Direct ? "direct" : "derived") + (row.moved ? ", moved" : ", not moved") + - (row.same ? ", the same" : ", not the same") + (row.unchanged ? ", unchanged" : ", changed") + - (row.skip ? ", skipping" : ", not skipping"); - ensure_equals(name, almasterplan_data::nameOf(ALMasterPlan::decide(row.kind, row.moved, row.same, row.unchanged, row.skip)), + CAPTURE(row.kind == Send::Direct, row.moved, row.same, row.unchanged, row.skip); + REQUIRE(almasterplan_data::nameOf(ALMasterPlan::decide(row.kind, row.moved, row.same, row.unchanged, row.skip)) == almasterplan_data::nameOf(row.expected)); } } - template<> template<> - void almasterplan_object::test<2>() + TEST_CASE_METHOD(almasterplan_data, "almasterplan: askFirst: past the count or across two objects; never with none changing", "[almasterplan]") { - set_test_name("an include's users are asked about over the count, or across two objects; never with none changing"); - ensure("none changing, nothing to ask", !ALMasterPlan::askFirst(0, 0, 8)); - ensure("one in one object", !ALMasterPlan::askFirst(1, 1, 8)); - ensure("exactly the count is not over it", !ALMasterPlan::askFirst(8, 1, 8)); - ensure("one past it is", ALMasterPlan::askFirst(9, 1, 8)); - ensure("many in one object", ALMasterPlan::askFirst(40, 1, 8)); - ensure("two in two objects", ALMasterPlan::askFirst(2, 2, 8)); - ensure("a count of none asks for one", ALMasterPlan::askFirst(1, 1, 0)); - ensure("and not for none", !ALMasterPlan::askFirst(0, 1, 0)); - ensure("objects counted for nothing changing ask nothing", !ALMasterPlan::askFirst(0, 3, 8)); + UNSCOPED_INFO("none changing, nothing to ask"); + CHECK_FALSE(ALMasterPlan::askFirst(0, 0, 8)); + UNSCOPED_INFO("one in one object"); + CHECK_FALSE(ALMasterPlan::askFirst(1, 1, 8)); + UNSCOPED_INFO("exactly the count is not over it"); + CHECK_FALSE(ALMasterPlan::askFirst(8, 1, 8)); + UNSCOPED_INFO("one past it is"); + CHECK(ALMasterPlan::askFirst(9, 1, 8)); + UNSCOPED_INFO("many in one object"); + CHECK(ALMasterPlan::askFirst(40, 1, 8)); + UNSCOPED_INFO("two in two objects"); + CHECK(ALMasterPlan::askFirst(2, 2, 8)); + UNSCOPED_INFO("a count of none asks for one"); + CHECK(ALMasterPlan::askFirst(1, 1, 0)); + UNSCOPED_INFO("and not for none"); + CHECK_FALSE(ALMasterPlan::askFirst(0, 1, 0)); + UNSCOPED_INFO("objects counted for nothing changing ask nothing"); + CHECK_FALSE(ALMasterPlan::askFirst(0, 3, 8)); } - template<> template<> - void almasterplan_object::test<3>() + TEST_CASE_METHOD(almasterplan_data, "almasterplan: objectsOf counts each object once and the inventory as one", "[almasterplan]") { - set_test_name("the objects scripts are in, each once, the inventory one of them"); const LLUUID door("00000001-0000-4000-8000-000000000000"); const LLUUID lamp("00000002-0000-4000-8000-000000000000"); - ensure_equals("none", ALMasterPlan::objectsOf({}), size_t(0)); - ensure_equals("one script", ALMasterPlan::objectsOf({ door }), size_t(1)); - ensure_equals("many scripts in one object", ALMasterPlan::objectsOf({ door, door, door, door, door }), size_t(1)); - ensure_equals("the inventory once", ALMasterPlan::objectsOf({ LLUUID::null, LLUUID::null }), size_t(1)); - ensure_equals("and as one beside the others", ALMasterPlan::objectsOf({ door, LLUUID::null, lamp, door, LLUUID::null }), size_t(3)); + CHECK(ALMasterPlan::objectsOf({}) == size_t(0)); + CHECK(ALMasterPlan::objectsOf({ door }) == size_t(1)); + UNSCOPED_INFO("many scripts in one object"); + CHECK(ALMasterPlan::objectsOf({ door, door, door, door, door }) == size_t(1)); + UNSCOPED_INFO("the inventory once"); + CHECK(ALMasterPlan::objectsOf({ LLUUID::null, LLUUID::null }) == size_t(1)); + UNSCOPED_INFO("and as one beside the others"); + CHECK(ALMasterPlan::objectsOf({ door, LLUUID::null, lamp, door, LLUUID::null }) == size_t(3)); // Twenty scripts in one object do not ask until past the count; // two in two objects do at once. std::vector one(20, door); - ensure("twenty in one object, over eight", ALMasterPlan::askFirst(one.size(), ALMasterPlan::objectsOf(one), 8)); - ensure("eight in one object, not", !ALMasterPlan::askFirst(8, ALMasterPlan::objectsOf(std::vector(8, door)), 8)); - ensure("two in the inventory and an object", ALMasterPlan::askFirst(2, ALMasterPlan::objectsOf({ LLUUID::null, lamp }), 8)); + UNSCOPED_INFO("twenty in one object, over eight"); + CHECK(ALMasterPlan::askFirst(one.size(), ALMasterPlan::objectsOf(one), 8)); + UNSCOPED_INFO("eight in one object, not"); + CHECK_FALSE(ALMasterPlan::askFirst(8, ALMasterPlan::objectsOf(std::vector(8, door)), 8)); + UNSCOPED_INFO("two in the inventory and an object"); + CHECK(ALMasterPlan::askFirst(2, ALMasterPlan::objectsOf({ LLUUID::null, lamp }), 8)); } } diff --git a/indra/alscript/tests/almasterqueue_test.cpp b/indra/alscript/tests/almasterqueue_test.cpp index 84a5c83f65..86eca7cdc6 100644 --- a/indra/alscript/tests/almasterqueue_test.cpp +++ b/indra/alscript/tests/almasterqueue_test.cpp @@ -26,13 +26,13 @@ #include "../masters/almasterqueue.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { struct almasterqueue_data { @@ -52,87 +52,113 @@ namespace tut } }; - typedef test_group almasterqueue_group; - typedef almasterqueue_group::object almasterqueue_object; - almasterqueue_group almasterqueue_instance("almasterqueue"); - - template<> template<> - void almasterqueue_object::test<1>() + TEST_CASE_METHOD(almasterqueue_data, "almasterqueue: four go at once and the rest wait their turn in the order asked", "[almasterqueue]") { - set_test_name("four go at once, the rest wait their turn, first come, first served"); using Send = almasterqueue_data::Send; ALMasterQueue queue; - ensure_equals("four by default", queue.limit(), size_t(4)); + UNSCOPED_INFO("four by default"); + CHECK(queue.limit() == size_t(4)); for (const char* key : { "a", "b", "c", "d" }) { - ensure(std::string("starts: ") + key, queue.ask(key, Send::Derived)); - ensure(std::string("is underway: ") + key, queue.underway(key) && !queue.waiting(key)); + CAPTURE(key); + REQUIRE(queue.ask(key, Send::Derived)); + REQUIRE(queue.underway(key)); + REQUIRE_FALSE(queue.waiting(key)); } - ensure_equals("four busy", queue.busy(), size_t(4)); - ensure("a fifth waits", !queue.ask("e", Send::Derived)); - ensure("a sixth too", !queue.ask("f", Send::Direct)); - ensure("waiting, not underway", queue.waiting("e") && !queue.underway("e")); - ensure_equals("still four busy", queue.busy(), size_t(4)); - - ensure_equals("one ends, the first waiting starts", almasterqueue_data::started(queue.finished("c")), std::string("e*")); - ensure("the one ended is not underway", !queue.underway("c") && !queue.waiting("c")); - ensure("the one started is", queue.underway("e") && !queue.waiting("e")); - ensure_equals("then the next", almasterqueue_data::started(queue.finished("a")), std::string("f")); - ensure_equals("then nothing waits", almasterqueue_data::started(queue.finished("b")), std::string()); - ensure_equals("three busy", queue.busy(), size_t(3)); - ensure("one asked now starts at once", queue.ask("g", Send::Derived)); - ensure_equals("one ended that was never asked starts nothing", almasterqueue_data::started(queue.finished("zz")), std::string()); - ensure_equals("and frees nothing", queue.busy(), size_t(4)); + CHECK(queue.busy() == size_t(4)); + UNSCOPED_INFO("a fifth waits"); + CHECK_FALSE(queue.ask("e", Send::Derived)); + UNSCOPED_INFO("a sixth too"); + CHECK_FALSE(queue.ask("f", Send::Direct)); + CHECK(queue.waiting("e")); + CHECK_FALSE(queue.underway("e")); + CHECK(queue.busy() == size_t(4)); + + UNSCOPED_INFO("one ends, the first waiting starts"); + CHECK(almasterqueue_data::started(queue.finished("c")) == std::string("e*")); + CHECK_FALSE(queue.underway("c")); + CHECK_FALSE(queue.waiting("c")); + CHECK(queue.underway("e")); + CHECK_FALSE(queue.waiting("e")); + UNSCOPED_INFO("then the next"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("f")); + UNSCOPED_INFO("then nothing waits"); + CHECK(almasterqueue_data::started(queue.finished("b")) == std::string()); + CHECK(queue.busy() == size_t(3)); + UNSCOPED_INFO("one asked now starts at once"); + CHECK(queue.ask("g", Send::Derived)); + UNSCOPED_INFO("one ended that was never asked starts nothing"); + CHECK(almasterqueue_data::started(queue.finished("zz")) == std::string()); + UNSCOPED_INFO("and frees nothing"); + CHECK(queue.busy() == size_t(4)); } - template<> template<> - void almasterqueue_object::test<2>() + TEST_CASE_METHOD(almasterqueue_data, "almasterqueue: a send asked while on its way goes again once after it", "[almasterqueue]") { - set_test_name("asked while on its way, a send goes again after it, once"); using Send = almasterqueue_data::Send; ALMasterQueue queue; - ensure("starts", queue.ask("a", Send::Direct)); - ensure("asked again, waits", !queue.ask("a", Send::Direct)); - ensure("and again", !queue.ask("a", Send::Direct)); - ensure("underway, and to go again", queue.underway("a") && queue.waiting("a")); - ensure_equals("one busy, not three", queue.busy(), size_t(1)); - ensure_equals("ended: goes again at once", almasterqueue_data::started(queue.finished("a")), std::string("a")); - ensure("underway again, not to go a third time", queue.underway("a") && !queue.waiting("a")); - ensure_equals("ended: nothing more", almasterqueue_data::started(queue.finished("a")), std::string()); - ensure("and gone", !queue.underway("a") && queue.busy() == 0); + REQUIRE(queue.ask("a", Send::Direct)); + UNSCOPED_INFO("asked again, waits"); + CHECK_FALSE(queue.ask("a", Send::Direct)); + UNSCOPED_INFO("and again"); + CHECK_FALSE(queue.ask("a", Send::Direct)); + { + INFO("underway, and to go again"); + CHECK(queue.underway("a")); + CHECK(queue.waiting("a")); + } + UNSCOPED_INFO("one busy, not three"); + CHECK(queue.busy() == size_t(1)); + UNSCOPED_INFO("ended: goes again at once"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("a")); + { + INFO("underway again, not to go a third time"); + CHECK(queue.underway("a")); + CHECK_FALSE(queue.waiting("a")); + } + UNSCOPED_INFO("ended: nothing more"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string()); + CHECK_FALSE(queue.underway("a")); + CHECK(queue.busy() == 0); } - template<> template<> - void almasterqueue_object::test<3>() + TEST_CASE_METHOD(almasterqueue_data, "almasterqueue: a send asked while it waits stays in its place", "[almasterqueue]") { - set_test_name("asked while it waits, a send stays in its place"); using Send = almasterqueue_data::Send; ALMasterQueue queue(1); - ensure("one place", queue.ask("a", Send::Derived)); - ensure("b waits", !queue.ask("b", Send::Derived)); - ensure("c waits", !queue.ask("c", Send::Derived)); - ensure("b again, in its place", !queue.ask("b", Send::Derived)); - ensure_equals("b first", almasterqueue_data::started(queue.finished("a")), std::string("b*")); - ensure_equals("then c", almasterqueue_data::started(queue.finished("b")), std::string("c*")); - ensure_equals("and b went once", almasterqueue_data::started(queue.finished("c")), std::string()); + UNSCOPED_INFO("one place"); + CHECK(queue.ask("a", Send::Derived)); + UNSCOPED_INFO("b waits"); + CHECK_FALSE(queue.ask("b", Send::Derived)); + UNSCOPED_INFO("c waits"); + CHECK_FALSE(queue.ask("c", Send::Derived)); + UNSCOPED_INFO("b again, in its place"); + CHECK_FALSE(queue.ask("b", Send::Derived)); + UNSCOPED_INFO("b first"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("b*")); + UNSCOPED_INFO("then c"); + CHECK(almasterqueue_data::started(queue.finished("b")) == std::string("c*")); + UNSCOPED_INFO("and b went once"); + CHECK(almasterqueue_data::started(queue.finished("c")) == std::string()); } - template<> template<> - void almasterqueue_object::test<4>() + TEST_CASE_METHOD(almasterqueue_data, "almasterqueue: a save of the master outranks the studio's own send whichever came first", "[almasterqueue]") { - set_test_name("a save of the master outranks the studio's own send, whichever was asked first"); using Send = almasterqueue_data::Send; ALMasterQueue queue(1); queue.ask("a", Send::Derived); // On its way: to go again as a save, asked either way round. queue.ask("a", Send::Derived); queue.ask("a", Send::Direct); - ensure_equals("a save after the studio's", almasterqueue_data::started(queue.finished("a")), std::string("a")); + UNSCOPED_INFO("a save after the studio's"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("a")); queue.ask("a", Send::Direct); queue.ask("a", Send::Derived); - ensure_equals("the studio's after a save", almasterqueue_data::started(queue.finished("a")), std::string("a")); + UNSCOPED_INFO("the studio's after a save"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("a")); queue.ask("a", Send::Derived); - ensure_equals("the studio's alone stays its own", almasterqueue_data::started(queue.finished("a")), std::string("a*")); + UNSCOPED_INFO("the studio's alone stays its own"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("a*")); // Waiting: the same. queue.ask("b", Send::Derived); @@ -141,15 +167,16 @@ namespace tut queue.ask("c", Send::Derived); queue.ask("d", Send::Derived); queue.ask("d", Send::Derived); - ensure_equals("b as a save", almasterqueue_data::started(queue.finished("a")), std::string("b")); - ensure_equals("c as a save", almasterqueue_data::started(queue.finished("b")), std::string("c")); - ensure_equals("d as the studio's", almasterqueue_data::started(queue.finished("c")), std::string("d*")); + UNSCOPED_INFO("b as a save"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("b")); + UNSCOPED_INFO("c as a save"); + CHECK(almasterqueue_data::started(queue.finished("b")) == std::string("c")); + UNSCOPED_INFO("d as the studio's"); + CHECK(almasterqueue_data::started(queue.finished("c")) == std::string("d*")); } - template<> template<> - void almasterqueue_object::test<5>() + TEST_CASE_METHOD(almasterqueue_data, "almasterqueue: a send asked again on its way goes behind all that wait with its newest text", "[almasterqueue]") { - set_test_name("a send asked again while on its way goes behind all that wait, its newest text last"); using Send = almasterqueue_data::Send; ALMasterQueue queue; for (const char* key : { "a", "b", "c", "d" }) @@ -159,18 +186,22 @@ namespace tut queue.ask("e", Send::Derived); queue.ask("f", Send::Derived); queue.ask("a", Send::Direct); - ensure_equals("a ends, the first waiting goes, not a", almasterqueue_data::started(queue.finished("a")), std::string("e*")); - ensure("a waits", queue.waiting("a") && !queue.underway("a")); - ensure_equals("then f", almasterqueue_data::started(queue.finished("b")), std::string("f*")); - ensure_equals("then a, as it was asked again", almasterqueue_data::started(queue.finished("c")), std::string("a")); - ensure_equals("four busy throughout", queue.busy(), size_t(4)); - ensure_equals("then nothing", almasterqueue_data::started(queue.finished("d")), std::string()); + UNSCOPED_INFO("a ends, the first waiting goes, not a"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("e*")); + CHECK(queue.waiting("a")); + CHECK_FALSE(queue.underway("a")); + UNSCOPED_INFO("then f"); + CHECK(almasterqueue_data::started(queue.finished("b")) == std::string("f*")); + UNSCOPED_INFO("then a, as it was asked again"); + CHECK(almasterqueue_data::started(queue.finished("c")) == std::string("a")); + UNSCOPED_INFO("four busy throughout"); + CHECK(queue.busy() == size_t(4)); + UNSCOPED_INFO("then nothing"); + CHECK(almasterqueue_data::started(queue.finished("d")) == std::string()); } - template<> template<> - void almasterqueue_object::test<6>() + TEST_CASE_METHOD(almasterqueue_data, "almasterqueue: a dropped script waiting or due again is not sent; one on its way goes on", "[almasterqueue]") { - set_test_name("a script dropped is not sent after all, waiting or to go again; one on its way goes on"); using Send = almasterqueue_data::Send; ALMasterQueue queue(2); queue.ask("a", Send::Direct); @@ -181,17 +212,26 @@ namespace tut queue.drop("c"); queue.drop("a"); queue.drop("never"); - ensure("c waits no longer", !queue.waiting("c") && !queue.underway("c")); - ensure("a goes on, but not again", queue.underway("a") && !queue.waiting("a")); - ensure_equals("a ends: d, not c, and not a", almasterqueue_data::started(queue.finished("a")), std::string("d*")); - ensure("c asked again goes in at the back", !queue.ask("c", Send::Direct)); - ensure_equals("and goes in its turn", almasterqueue_data::started(queue.finished("b")), std::string("c")); + { + INFO("c waits no longer"); + CHECK_FALSE(queue.waiting("c")); + CHECK_FALSE(queue.underway("c")); + } + { + INFO("a goes on, but not again"); + CHECK(queue.underway("a")); + CHECK_FALSE(queue.waiting("a")); + } + UNSCOPED_INFO("a ends: d, not c, and not a"); + CHECK(almasterqueue_data::started(queue.finished("a")) == std::string("d*")); + UNSCOPED_INFO("c asked again goes in at the back"); + CHECK_FALSE(queue.ask("c", Send::Direct)); + UNSCOPED_INFO("and goes in its turn"); + CHECK(almasterqueue_data::started(queue.finished("b")) == std::string("c")); } - template<> template<> - void almasterqueue_object::test<7>() + TEST_CASE_METHOD(almasterqueue_data, "almasterqueue: the limit holds and each is sent once in order; a limit is at least one", "[almasterqueue]") { - set_test_name("never more on their way than the limit, every one sent once, in order; at least one"); using Send = almasterqueue_data::Send; ALMasterQueue queue; std::string order; @@ -206,9 +246,10 @@ namespace tut underway.push_back(key); order += (order.empty() ? "" : " ") + key; } - ensure("never more than four", queue.busy() <= 4); + UNSCOPED_INFO("never more than four"); + REQUIRE(queue.busy() <= 4); } - ensure_equals("four started", underway.size(), size_t(4)); + REQUIRE(underway.size() == size_t(4)); while (!underway.empty()) { const std::string done = underway.front(); @@ -218,18 +259,30 @@ namespace tut underway.push_back(key); order += " " + key; } - ensure("never more than four", queue.busy() <= 4 && queue.busy() == underway.size()); + { + INFO("never more than four"); + REQUIRE(queue.busy() <= 4); + REQUIRE(queue.busy() == underway.size()); + } } - ensure_equals("all, once, in the order asked", order, expected); + UNSCOPED_INFO("all, once, in the order asked"); + CHECK(order == expected); ALMasterQueue none(0); - ensure_equals("a limit of none is one", none.limit(), size_t(1)); - ensure("which starts", none.ask("a", Send::Derived)); - ensure("one only", !none.ask("b", Send::Derived)); + UNSCOPED_INFO("a limit of none is one"); + CHECK(none.limit() == size_t(1)); + UNSCOPED_INFO("which starts"); + CHECK(none.ask("a", Send::Derived)); + UNSCOPED_INFO("one only"); + CHECK_FALSE(none.ask("b", Send::Derived)); none.clear(); - ensure("cleared", !none.underway("a") && !none.waiting("b") && none.busy() == 0); - ensure("and starts afresh", none.ask("b", Send::Derived)); - ensure_equals("with nothing behind it", almasterqueue_data::started(none.finished("b")), std::string()); + CHECK_FALSE(none.underway("a")); + CHECK_FALSE(none.waiting("b")); + CHECK(none.busy() == 0); + UNSCOPED_INFO("and starts afresh"); + CHECK(none.ask("b", Send::Derived)); + UNSCOPED_INFO("with nothing behind it"); + CHECK(almasterqueue_data::started(none.finished("b")) == std::string()); } } diff --git a/indra/alscript/tests/almasterwatch_test.cpp b/indra/alscript/tests/almasterwatch_test.cpp index 5533b46c28..84f65708c1 100644 --- a/indra/alscript/tests/almasterwatch_test.cpp +++ b/indra/alscript/tests/almasterwatch_test.cpp @@ -30,7 +30,7 @@ #include "llfile.h" #include "lluuid.h" -#include "../test/lltut.h" +#include #include #include @@ -40,7 +40,7 @@ #include #include -namespace tut +namespace { namespace fs = std::filesystem; @@ -206,42 +206,39 @@ namespace tut } }; - typedef test_group almasterwatch_group; - typedef almasterwatch_group::object almasterwatch_object; - almasterwatch_group almasterwatch_instance("almasterwatch"); - - template<> template<> - void almasterwatch_object::test<1>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: a master saved outside is released once the burst has been quiet a second", "[almasterwatch]") { - set_test_name("a master saved outside is released once the burst has been quiet a second"); const std::string door = in("door.lsl"); put(door, "default {}\n", 100); ALMasterWatch& watched = make(); watched.watch({ almasterwatch_data::master(door) }); - ensure("watched at once, its look answered here", watched.watching(door)); + UNSCOPED_INFO("watched at once, its look answered here"); + CHECK(watched.watching(door)); looked(); pass(5.0); - ensure("what was there already is no change", released.empty()); + UNSCOPED_INFO("what was there already is no change"); + REQUIRE(released.empty()); put(door, "default { state_entry() {} }\n", 200); watched.lookNow(); - ensure("moving: not heard yet", waiting.empty()); + UNSCOPED_INFO("moving: not heard yet"); + REQUIRE(waiting.empty()); watched.lookNow(); pass(0.9); - ensure("not before the quiet", released.empty()); + UNSCOPED_INFO("not before the quiet"); + REQUIRE(released.empty()); pass(0.2); - ensure_equals("released once", released.size(), size_t(1)); - ensure_equals("the master", almasterwatch_data::joined(released[0].masters), std::string("door.lsl")); - ensure("no include", released[0].includes.empty()); + REQUIRE(released.size() == size_t(1)); + CHECK(almasterwatch_data::joined(released[0].masters) == std::string("door.lsl")); + REQUIRE(released[0].includes.empty()); looked(); pass(5.0); - ensure_equals("and not again", released.size(), size_t(1)); + UNSCOPED_INFO("and not again"); + REQUIRE(released.size() == size_t(1)); } - template<> template<> - void almasterwatch_object::test<2>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: a checkout of many files is released once and together after the last", "[almasterwatch]") { - set_test_name("a checkout of many files is released once, all together, after the last of them"); std::vector files; for (int i = 0; i < 10; ++i) { @@ -263,16 +260,15 @@ namespace tut } looked(); pass(0.9); - ensure("the burst goes on: nothing yet", released.empty()); + UNSCOPED_INFO("the burst goes on: nothing yet"); + REQUIRE(released.empty()); pass(0.2); - ensure_equals("released once", released.size(), size_t(1)); - ensure_equals("all ten together", released[0].masters.size(), size_t(10)); + REQUIRE(released.size() == size_t(1)); + REQUIRE(released[0].masters.size() == size_t(10)); } - template<> template<> - void almasterwatch_object::test<3>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: masters and includes are told apart as they are released", "[almasterwatch]") { - set_test_name("masters and includes are told apart as they are released"); const std::string door = in("door.lsl"); const std::string util = in("util.lsl"); put(door, "a", 100); @@ -283,15 +279,13 @@ namespace tut put(util, "b2", 200); looked(); pass(1.1); - ensure_equals("released once", released.size(), size_t(1)); - ensure_equals("the master", almasterwatch_data::joined(released[0].masters), std::string("door.lsl")); - ensure_equals("the include", almasterwatch_data::joined(released[0].includes), std::string("util.lsl")); + REQUIRE(released.size() == size_t(1)); + CHECK(almasterwatch_data::joined(released[0].masters) == std::string("door.lsl")); + CHECK(almasterwatch_data::joined(released[0].includes) == std::string("util.lsl")); } - template<> template<> - void almasterwatch_object::test<4>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: an emptied file is held longer than the quiet unless filled again meanwhile", "[almasterwatch]") { - set_test_name("an emptied file is held a moment and a half, and one filled again meanwhile no longer than the quiet"); const std::string door = in("door.lsl"); put(door, "default {}\n", 100); ALMasterWatch& watched = make(); @@ -300,31 +294,34 @@ namespace tut put(door, "", 200); looked(); pass(1.1); - ensure("quiet, but emptied: held", released.empty()); + UNSCOPED_INFO("quiet, but emptied: held"); + REQUIRE(released.empty()); pass(0.5); - ensure_equals("taken as empty after the hold", released.size(), size_t(1)); + UNSCOPED_INFO("taken as empty after the hold"); + REQUIRE(released.size() == size_t(1)); // An editor saving in two steps: emptied, then filled. put(door, "default { touch_start(integer n) {} }\n", 300); looked(); pass(1.1); - ensure_equals("filled again: released", released.size(), size_t(2)); + UNSCOPED_INFO("filled again: released"); + REQUIRE(released.size() == size_t(2)); put(door, "", 400); looked(); pass(0.3); put(door, "default { state_entry() {} }\n", 500); looked(); pass(0.9); - ensure("a second after it was filled: not yet", released.size() == 2); + UNSCOPED_INFO("a second after it was filled: not yet"); + REQUIRE(released.size() == 2); pass(0.2); - ensure_equals("then, before the emptied hold would have ended", released.size(), size_t(3)); - ensure_equals("the file, once", released[2].masters.size(), size_t(1)); + UNSCOPED_INFO("then, before the emptied hold would have ended"); + REQUIRE(released.size() == size_t(3)); + REQUIRE(released[2].masters.size() == size_t(1)); } - template<> template<> - void almasterwatch_object::test<5>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: what the studio writes is no change and takes a waiting change with it", "[almasterwatch]") { - set_test_name("what the studio writes itself is no change, and takes with it a change waiting in the burst"); const std::string door = in("door.lsl"); put(door, "a", 100); ALMasterWatch& watched = make(); @@ -334,7 +331,8 @@ namespace tut watched.seen(door); looked(); pass(5.0); - ensure("the studio's own write: nothing", released.empty()); + UNSCOPED_INFO("the studio's own write: nothing"); + REQUIRE(released.empty()); // Saved outside, and heard; then written by the studio before the // burst ends, which sends it itself. @@ -343,22 +341,26 @@ namespace tut put(door, "studio again", 400); watched.seen(door); pass(5.0); - ensure("nothing waiting on it from before", released.empty()); + UNSCOPED_INFO("nothing waiting on it from before"); + REQUIRE(released.empty()); looked(); pass(5.0); - ensure("and nothing after", released.empty()); + UNSCOPED_INFO("and nothing after"); + REQUIRE(released.empty()); } - template<> template<> - void almasterwatch_object::test<6>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: a save after a studio write during the first look is heard", "[almasterwatch]") { - set_test_name("a file the studio writes while its first look is out is watched from that write, and a save after it is heard"); const std::string door = in("door.lsl"); put(door, "first", 100); ALMasterWatch& watched = make(/*look_at_once*/ false); watched.watch({ almasterwatch_data::master(door) }); - ensure("waiting on its first look", watched.waiting(door) && !watched.watching(door)); - ensure_equals("asked once", asked.size(), size_t(1)); + { + INFO("waiting on its first look"); + CHECK(watched.waiting(door)); + CHECK_FALSE(watched.watching(door)); + } + REQUIRE(asked.size() == size_t(1)); // The studio writes it, then somebody else saves it, all before the // look answers: the save is heard, the studio's write is not. @@ -366,16 +368,16 @@ namespace tut watched.seen(door); put(door, "outside", 300); answer(0); - ensure("watched now", watched.watching(door)); + UNSCOPED_INFO("watched now"); + CHECK(watched.watching(door)); looked(); pass(1.1); - ensure_equals("the save after the studio's write heard", released.size(), size_t(1)); + UNSCOPED_INFO("the save after the studio's write heard"); + REQUIRE(released.size() == size_t(1)); } - template<> template<> - void almasterwatch_object::test<7>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: a studio write during the first look is no change however old the look", "[almasterwatch]") { - set_test_name("a file the studio writes while its first look is out is no change, whenever the look was made"); const std::string door = in("door.lsl"); const std::string lamp = in("lamp.lsl"); put(door, "first", 100); @@ -387,35 +389,34 @@ namespace tut put(door, "studio", 200); watched.seen(door); answer(0, before); - ensure("both watched", watched.watching(door) && watched.watching(lamp)); + CHECK(watched.watching(door)); + CHECK(watched.watching(lamp)); looked(); pass(5.0); - ensure("the studio's write is no change", released.empty()); + UNSCOPED_INFO("the studio's write is no change"); + REQUIRE(released.empty()); } - template<> template<> - void almasterwatch_object::test<8>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: a first look answering for a file no longer wanted makes no watch of it", "[almasterwatch]") { - set_test_name("a first look answering for a file no longer wanted makes no watch of it"); const std::string door = in("door.lsl"); put(door, "first", 100); ALMasterWatch& watched = make(/*look_at_once*/ false); watched.watch({ almasterwatch_data::master(door) }); watched.watch({}); - ensure("not waiting now", !watched.waiting(door)); + UNSCOPED_INFO("not waiting now"); + CHECK_FALSE(watched.waiting(door)); answer(0); - ensure("not watched", !watched.watching(door)); - ensure_equals("nothing at all", watched.size(), size_t(0)); + CHECK_FALSE(watched.watching(door)); + REQUIRE(watched.size() == size_t(0)); put(door, "changed", 200); looked(); pass(5.0); - ensure("nothing heard", released.empty()); + REQUIRE(released.empty()); } - template<> template<> - void almasterwatch_object::test<9>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: a file wanted again waits on the newer look and one wanted still on its own", "[almasterwatch]") { - set_test_name("a file let go of and wanted again waits on the newer look; one wanted still keeps the look it waits on"); const std::string door = in("door.lsl"); const std::string lamp = in("lamp.lsl"); put(door, "first", 100); @@ -424,30 +425,40 @@ namespace tut watched.watch({ almasterwatch_data::master(door) }); watched.watch({}); watched.watch({ almasterwatch_data::master(door) }); - ensure_equals("asked again", asked.size(), size_t(2)); + UNSCOPED_INFO("asked again"); + REQUIRE(asked.size() == size_t(2)); answer(0); - ensure("the older look makes no watch", !watched.watching(door) && watched.waiting(door)); + { + INFO("the older look makes no watch"); + CHECK_FALSE(watched.watching(door)); + CHECK(watched.waiting(door)); + } answer(1); - ensure("the newer one does", watched.watching(door)); + UNSCOPED_INFO("the newer one does"); + CHECK(watched.watching(door)); // Wanted still while another is added: the look it waits on answers // it, and only the new file is asked after. watched.watch({ almasterwatch_data::master(door), almasterwatch_data::master(lamp) }); - ensure("door watched already: lamp alone asked", asked.size() == 3 && asked[2].paths.size() == 1); + { + INFO("door watched already: lamp alone asked"); + REQUIRE(asked.size() == 3); + REQUIRE(asked[2].paths.size() == 1); + } const std::string other = in("other.lsl"); watched.watch({ almasterwatch_data::master(door), almasterwatch_data::master(lamp), almasterwatch_data::master(other) }); - ensure_equals("lamp waits on its look still: other alone asked", asked[3].paths.size(), size_t(1)); + UNSCOPED_INFO("lamp waits on its look still: other alone asked"); + REQUIRE(asked[3].paths.size() == size_t(1)); answer(3); - ensure("other watched", watched.watching(other)); - ensure("lamp waits on its own", watched.waiting(lamp)); + CHECK(watched.watching(other)); + UNSCOPED_INFO("lamp waits on its own"); + CHECK(watched.waiting(lamp)); answer(2); - ensure("lamp watched", watched.watching(lamp)); + CHECK(watched.watching(lamp)); } - template<> template<> - void almasterwatch_object::test<10>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: masters are looked at twice a second and includes once; past 256 every 2 s", "[almasterwatch]") { - set_test_name("a master is looked at twice a second, an include once, and everything every two seconds past 256 files"); const std::string door = in("door.lsl"); const std::string util = in("util.lsl"); size_t looks = 0; @@ -459,9 +470,10 @@ namespace tut return true; }); watch->watch({ almasterwatch_data::master(door), almasterwatch_data::include(util) }); - ensure_equals("a master", *watch->periodOf(door), ALMasterWatch::MASTER_PERIOD); - ensure_equals("an include", *watch->periodOf(util), ALMasterWatch::INCLUDE_PERIOD); - ensure_equals("both looked at first", looks, size_t(2)); + CHECK(*watch->periodOf(door) == ALMasterWatch::MASTER_PERIOD); + CHECK(*watch->periodOf(util) == ALMasterWatch::INCLUDE_PERIOD); + UNSCOPED_INFO("both looked at first"); + CHECK(looks == size_t(2)); std::vector many = { almasterwatch_data::master(door), almasterwatch_data::include(util) }; for (size_t i = 0; i < ALMasterWatch::MANY - 1; ++i) @@ -469,22 +481,25 @@ namespace tut many.push_back(almasterwatch_data::master(in("m" + std::to_string(i) + ".lsl"))); } watch->watch(many); - ensure_equals("257 watched", watch->size(), size_t(257)); - ensure_equals("past 256: a master every two seconds", *watch->periodOf(door), ALMasterWatch::MANY_PERIOD); - ensure_equals("and an include", *watch->periodOf(util), ALMasterWatch::MANY_PERIOD); - ensure_equals("only the new ones looked at", looks, size_t(257)); + REQUIRE(watch->size() == size_t(257)); + UNSCOPED_INFO("past 256: a master every two seconds"); + CHECK(*watch->periodOf(door) == ALMasterWatch::MANY_PERIOD); + CHECK(*watch->periodOf(util) == ALMasterWatch::MANY_PERIOD); + UNSCOPED_INFO("only the new ones looked at"); + CHECK(looks == size_t(257)); watch->watch({ almasterwatch_data::master(door), almasterwatch_data::include(util) }); - ensure_equals("fewer again: a master's own", *watch->periodOf(door), ALMasterWatch::MASTER_PERIOD); - ensure_equals("an include's own", *watch->periodOf(util), ALMasterWatch::INCLUDE_PERIOD); - ensure_equals("nothing looked at again", looks, size_t(257)); - ensure("one not watched has no period", !watch->periodOf(in("m0.lsl"))); + UNSCOPED_INFO("fewer again: a master's own"); + CHECK(*watch->periodOf(door) == ALMasterWatch::MASTER_PERIOD); + CHECK(*watch->periodOf(util) == ALMasterWatch::INCLUDE_PERIOD); + UNSCOPED_INFO("nothing looked at again"); + CHECK(looks == size_t(257)); + UNSCOPED_INFO("one not watched has no period"); + REQUIRE_FALSE(watch->periodOf(in("m0.lsl"))); } - template<> template<> - void almasterwatch_object::test<11>() + TEST_CASE_METHOD(almasterwatch_data, "almasterwatch: with no main loop to answer through the first looks are made at once", "[almasterwatch]") { - set_test_name("with no main loop to answer through, the first looks are made at once, here"); const std::string door = in("door.lsl"); put(door, "first", 100); watch = std::make_unique( @@ -492,10 +507,12 @@ namespace tut clock()); watch->setQuiet(0.5); watch->watch({ almasterwatch_data::master(door) }); - ensure("watched at once", watch->watching(door)); + UNSCOPED_INFO("watched at once"); + CHECK(watch->watching(door)); put(door, "second", 200); looked(); pass(0.6); - ensure_equals("released after the quiet set", released.size(), size_t(1)); + UNSCOPED_INFO("released after the quiet set"); + REQUIRE(released.size() == size_t(1)); } } diff --git a/indra/alscript/tests/almessagemap_test.cpp b/indra/alscript/tests/almessagemap_test.cpp index 3b09089a5a..47a5dc8f80 100644 --- a/indra/alscript/tests/almessagemap_test.cpp +++ b/indra/alscript/tests/almessagemap_test.cpp @@ -27,9 +27,9 @@ #include "../core/almessagemap.h" #include "../core/alscriptproblem.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { struct almessagemap_data { @@ -43,169 +43,243 @@ namespace tut return out; } }; - typedef test_group almessagemap_group; - typedef almessagemap_group::object almessagemap_object; - tut::almessagemap_group almessagemap_instance("almessagemap"); - template<> template<> - void almessagemap_object::test<1>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: match gives the words between the literals in the marks' order", "[almessagemap]") { - set_test_name("the words between the literals, in the marks' order"); std::vector args; - ensure("matches", ALMessageMap::match("Passing [1] as argument [2] of `[3]' which is declared as `[4] [5]'.", + REQUIRE(ALMessageMap::match("Passing [1] as argument [2] of `[3]' which is declared as `[4] [5]'.", "Passing string as argument 2 of `llSay' which is declared as `integer channel'.", args)); - ensure_equals("the words", joined(args), std::string("<2>")); - ensure("the first literal must begin it", !ALMessageMap::match("Too many arguments to function `[1]'.", "Warning: Too many arguments to function `f'.", args)); - ensure("the last must end it", !ALMessageMap::match("Too many arguments to function `[1]'.", "Too many arguments to function `f'. Really.", args)); - ensure("a template without marks is the message itself", ALMessageMap::match("Functions cannot change state.", "Functions cannot change state.", args)); - ensure("and no other", !ALMessageMap::match("Functions cannot change state.", "Functions cannot change state", args)); - ensure_equals("no words then", args.size(), size_t(0)); + CHECK(joined(args) == std::string("<2>")); + UNSCOPED_INFO("the first literal must begin it"); + CHECK_FALSE(ALMessageMap::match("Too many arguments to function `[1]'.", "Warning: Too many arguments to function `f'.", args)); + UNSCOPED_INFO("the last must end it"); + CHECK_FALSE(ALMessageMap::match("Too many arguments to function `[1]'.", "Too many arguments to function `f'. Really.", args)); + UNSCOPED_INFO("a template without marks is the message itself"); + CHECK(ALMessageMap::match("Functions cannot change state.", "Functions cannot change state.", args)); + UNSCOPED_INFO("and no other"); + CHECK_FALSE(ALMessageMap::match("Functions cannot change state.", "Functions cannot change state", args)); + UNSCOPED_INFO("no words then"); + REQUIRE(args.size() == size_t(0)); } - template<> template<> - void almessagemap_object::test<2>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: a mark used twice stands for one word", "[almessagemap]") { - set_test_name("a mark used twice stands for one word"); std::vector args; - ensure("the same word twice", ALMessageMap::match("Trying to access `[1].[2]', but `[1]' is a [3]", "Trying to access `v.x', but `v' is a integer", args)); - ensure_equals("named once", joined(args), std::string("")); - ensure("a different word is no match", !ALMessageMap::match("Trying to access `[1].[2]', but `[1]' is a [3]", "Trying to access `v.x', but `w' is a integer", args)); + UNSCOPED_INFO("the same word twice"); + REQUIRE(ALMessageMap::match("Trying to access `[1].[2]', but `[1]' is a [3]", "Trying to access `v.x', but `v' is a integer", args)); + UNSCOPED_INFO("named once"); + CHECK(joined(args) == std::string("")); + UNSCOPED_INFO("a different word is no match"); + CHECK_FALSE(ALMessageMap::match("Trying to access `[1].[2]', but `[1]' is a [3]", "Trying to access `v.x', but `w' is a integer", args)); } - template<> template<> - void almessagemap_object::test<3>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: a literal is found at its first occurrence so a word holding it misleads", "[almessagemap]") { - set_test_name("a literal is found at its first occurrence after the last, which a word containing it can mislead"); std::vector args; // The engines' templates keep their words in quotes, so the // literal after a mark begins with the closing quote and a name // cannot contain it; this pins what a bare literal does. - ensure("a name with the literal in it", ALMessageMap::match("[1] is [2] now", "x is y is z now", args)); - ensure_equals("cut at the first: the rest is the second word", joined(args), std::string("")); - ensure("quoted, a name is whole", ALMessageMap::match("Variable '[1]' is never used; prefix with '_' to silence", "Variable 'is' is never used; prefix with '_' to silence", args)); - ensure_equals("the name", joined(args), std::string("")); + UNSCOPED_INFO("a name with the literal in it"); + REQUIRE(ALMessageMap::match("[1] is [2] now", "x is y is z now", args)); + UNSCOPED_INFO("cut at the first: the rest is the second word"); + CHECK(joined(args) == std::string("")); + UNSCOPED_INFO("quoted, a name is whole"); + CHECK(ALMessageMap::match("Variable '[1]' is never used; prefix with '_' to silence", "Variable 'is' is never used; prefix with '_' to silence", args)); + CHECK(joined(args) == std::string("")); // The last literal is anchored at the end, so a word that holds // it is not cut there. - ensure("the end holds", ALMessageMap::match("Unknown require: [1]", "Unknown require: a: b", args)); - ensure_equals("the whole rest", joined(args), std::string("")); + UNSCOPED_INFO("the end holds"); + CHECK(ALMessageMap::match("Unknown require: [1]", "Unknown require: a: b", args)); + UNSCOPED_INFO("the whole rest"); + CHECK(joined(args) == std::string("")); } - template<> template<> - void almessagemap_object::test<4>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: Tailslide's messages are keyed by code and Luau's lints by name", "[almessagemap]") { - set_test_name("Tailslide's messages by code, Luau's lints by name"); ALMessageMap::Match m; - ensure("10007", ALMessageMap::lsl(10007, "`llSay' is undeclared; did you mean llSay?", m)); - ensure_equals("its key", m.key, std::string("LSLUndeclaredWithSuggestion")); - ensure_equals("its words", joined(m.args), std::string("")); - ensure("a code with another message is not taken", !ALMessageMap::lsl(10007, "`llSay' is undeclared.", m)); - ensure("a code the table lacks", !ALMessageMap::lsl(10020, "syntax error, unexpected ';'", m)); - ensure("a lint by name", ALMessageMap::luauLint("LocalUnused", "Variable 'x' is never used; prefix with '_' to silence", m)); - ensure_equals("its key", m.key, std::string("LuauLintLocalUnused")); - ensure("the name is part of the match", !ALMessageMap::luauLint("FunctionUnused", "Variable 'x' is never used; prefix with '_' to silence", m)); - ensure("the comparison lints have a mark for each operator", ALMessageMap::luauLint("ComparisonPrecedence", "X == Y == Z is equivalent to (X == Y) == Z; add parentheses to silence", m)); - ensure_equals("four of them", m.args.size(), size_t(4)); + REQUIRE(ALMessageMap::lsl(10007, "`llSay' is undeclared; did you mean llSay?", m)); + CHECK(m.key == std::string("LSLUndeclaredWithSuggestion")); + CHECK(joined(m.args) == std::string("")); + UNSCOPED_INFO("a code with another message is not taken"); + CHECK_FALSE(ALMessageMap::lsl(10007, "`llSay' is undeclared.", m)); + UNSCOPED_INFO("a code the table lacks"); + CHECK_FALSE(ALMessageMap::lsl(10020, "syntax error, unexpected ';'", m)); + UNSCOPED_INFO("a lint by name"); + CHECK(ALMessageMap::luauLint("LocalUnused", "Variable 'x' is never used; prefix with '_' to silence", m)); + CHECK(m.key == std::string("LuauLintLocalUnused")); + UNSCOPED_INFO("the name is part of the match"); + CHECK_FALSE(ALMessageMap::luauLint("FunctionUnused", "Variable 'x' is never used; prefix with '_' to silence", m)); + UNSCOPED_INFO("the comparison lints have a mark for each operator"); + CHECK(ALMessageMap::luauLint("ComparisonPrecedence", "X == Y == Z is equivalent to (X == Y) == Z; add parentheses to silence", m)); + REQUIRE(m.args.size() == size_t(4)); } - template<> template<> - void almessagemap_object::test<5>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: a lint whose template has clauses matches its shapes fuller first", "[almessagemap]") { - set_test_name("a lint whose template stands for clauses is matched by the shapes it takes, the fuller first"); ALMessageMap::Match m; - ensure("plain", ALMessageMap::luauLint("DeprecatedApi", "Member 'table.getn' is deprecated", m)); - ensure_equals("plain key", m.key, std::string("LuauLintDeprecatedMember")); - ensure_equals("the qualified name whole", joined(m.args), std::string("")); - ensure("with a replacement", ALMessageMap::luauLint("DeprecatedApi", "Member 'table.getn' is deprecated, use '#' instead", m)); - ensure_equals("its key", m.key, std::string("LuauLintDeprecatedMemberUse")); - ensure_equals("its words", joined(m.args), std::string("<#>")); - ensure("with a replacement and a reason", ALMessageMap::luauLint("DeprecatedApi", "Member 'a.b' is deprecated, use 'c' instead. It was slow", m)); - ensure_equals("the fuller key", m.key, std::string("LuauLintDeprecatedMemberUseReason")); - ensure_equals("three words", joined(m.args), std::string("")); - ensure("a function", ALMessageMap::luauLint("DeprecatedApi", "Function 'f' is deprecated. Gone", m)); - ensure_equals("its key", m.key, std::string("LuauLintDeprecatedFunctionReason")); + UNSCOPED_INFO("plain"); + REQUIRE(ALMessageMap::luauLint("DeprecatedApi", "Member 'table.getn' is deprecated", m)); + CHECK(m.key == std::string("LuauLintDeprecatedMember")); + UNSCOPED_INFO("the qualified name whole"); + CHECK(joined(m.args) == std::string("")); + UNSCOPED_INFO("with a replacement"); + CHECK(ALMessageMap::luauLint("DeprecatedApi", "Member 'table.getn' is deprecated, use '#' instead", m)); + CHECK(m.key == std::string("LuauLintDeprecatedMemberUse")); + CHECK(joined(m.args) == std::string("<#>")); + UNSCOPED_INFO("with a replacement and a reason"); + CHECK(ALMessageMap::luauLint("DeprecatedApi", "Member 'a.b' is deprecated, use 'c' instead. It was slow", m)); + UNSCOPED_INFO("the fuller key"); + CHECK(m.key == std::string("LuauLintDeprecatedMemberUseReason")); + CHECK(joined(m.args) == std::string("")); + UNSCOPED_INFO("a function"); + CHECK(ALMessageMap::luauLint("DeprecatedApi", "Function 'f' is deprecated. Gone", m)); + CHECK(m.key == std::string("LuauLintDeprecatedFunctionReason")); } - template<> template<> - void almessagemap_object::test<6>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: Luau's type errors by shape: the fuller shape before the one it begins with", "[almessagemap]") { - set_test_name("Luau's type errors by shape: the fuller shape before the one it begins with"); ALMessageMap::Match m; - ensure("a mismatch", ALMessageMap::luauError("Expected this to be 'number', but got 'string'", m)); - ensure_equals("its key", m.key, std::string("LuauTypeMismatch")); - ensure_equals("wanted then given, quotes and all", joined(m.args), std::string("<'number'><'string'>")); - ensure("with a reason", ALMessageMap::luauError("Expected this to be 'number', but got 'string'; a table is not a number", m)); - ensure_equals("the reason's key", m.key, std::string("LuauTypeMismatchReason")); - ensure_equals("three words", joined(m.args), std::string("<'number'><'string'>")); - ensure("exactly", ALMessageMap::luauError("Expected this to be exactly 'number', but got 'string'", m)); - ensure_equals("not the plain one with 'exactly' in the word", m.key, std::string("LuauTypeMismatchExactly")); - ensure("unreachable", ALMessageMap::luauError("Expected this to be unreachable, but got 'string'", m)); - ensure_equals("its key", m.key, std::string("LuauTypeMismatchUnreachable")); - ensure("a count", ALMessageMap::luauError("Argument count mismatch. Function 'll.Say' expects 2 arguments, but only 1 is specified", m)); - ensure_equals("only one", m.key, std::string("LuauArgumentCountOnlyOne")); - ensure_equals("the name and the count", joined(m.args), std::string("<2>")); - ensure("at least", ALMessageMap::luauError("Argument count mismatch. Function 'f' expects at least 2 arguments, but only 1 is specified", m)); - ensure_equals("not the plain one with 'at least' in the count", m.key, std::string("LuauArgumentCountAtLeastOnlyOne")); - ensure("a range", ALMessageMap::luauError("Argument count mismatch. Function 'f' expects 1 to 2 arguments, but none are specified", m)); - ensure_equals("range, none", m.key, std::string("LuauArgumentCountRangeNone")); - ensure("one wanted", ALMessageMap::luauError("Argument count mismatch. Function 'f' expects 1 argument, but 3 are specified", m)); - ensure_equals("its key", m.key, std::string("LuauArgumentCountOne")); - ensure("a key not found", ALMessageMap::luauError("Key 'b' not found in table '{ a: number }'. Did you mean 'a'?", m)); - ensure_equals("did you mean", m.key, std::string("LuauMissingPropertyDidYouMean")); - ensure_equals("its words", joined(m.args), std::string("<{ a: number }>")); - ensure("nothing the table knows", !ALMessageMap::luauError("Some words nobody wrote", m)); + UNSCOPED_INFO("a mismatch"); + REQUIRE(ALMessageMap::luauError("Expected this to be 'number', but got 'string'", m)); + CHECK(m.key == std::string("LuauTypeMismatch")); + UNSCOPED_INFO("wanted then given, quotes and all"); + CHECK(joined(m.args) == std::string("<'number'><'string'>")); + UNSCOPED_INFO("with a reason"); + CHECK(ALMessageMap::luauError("Expected this to be 'number', but got 'string'; a table is not a number", m)); + CHECK(m.key == std::string("LuauTypeMismatchReason")); + CHECK(joined(m.args) == std::string("<'number'><'string'>")); + UNSCOPED_INFO("exactly"); + CHECK(ALMessageMap::luauError("Expected this to be exactly 'number', but got 'string'", m)); + UNSCOPED_INFO("not the plain one with 'exactly' in the word"); + CHECK(m.key == std::string("LuauTypeMismatchExactly")); + UNSCOPED_INFO("unreachable"); + CHECK(ALMessageMap::luauError("Expected this to be unreachable, but got 'string'", m)); + CHECK(m.key == std::string("LuauTypeMismatchUnreachable")); + UNSCOPED_INFO("a count"); + CHECK(ALMessageMap::luauError("Argument count mismatch. Function 'll.Say' expects 2 arguments, but only 1 is specified", m)); + CHECK(m.key == std::string("LuauArgumentCountOnlyOne")); + UNSCOPED_INFO("the name and the count"); + CHECK(joined(m.args) == std::string("<2>")); + UNSCOPED_INFO("at least"); + CHECK(ALMessageMap::luauError("Argument count mismatch. Function 'f' expects at least 2 arguments, but only 1 is specified", m)); + UNSCOPED_INFO("not the plain one with 'at least' in the count"); + CHECK(m.key == std::string("LuauArgumentCountAtLeastOnlyOne")); + UNSCOPED_INFO("a range"); + CHECK(ALMessageMap::luauError("Argument count mismatch. Function 'f' expects 1 to 2 arguments, but none are specified", m)); + CHECK(m.key == std::string("LuauArgumentCountRangeNone")); + UNSCOPED_INFO("one wanted"); + CHECK(ALMessageMap::luauError("Argument count mismatch. Function 'f' expects 1 argument, but 3 are specified", m)); + CHECK(m.key == std::string("LuauArgumentCountOne")); + UNSCOPED_INFO("a key not found"); + CHECK(ALMessageMap::luauError("Key 'b' not found in table '{ a: number }'. Did you mean 'a'?", m)); + CHECK(m.key == std::string("LuauMissingPropertyDidYouMean")); + CHECK(joined(m.args) == std::string("<{ a: number }>")); + UNSCOPED_INFO("nothing the table knows"); + CHECK_FALSE(ALMessageMap::luauError("Some words nobody wrote", m)); // The new solver's nonstrict mode. - ensure("a checked call", ALMessageMap::luauError("the function 'string.len' expects to get a string as its 1st argument, but is being given a number", m)); - ensure_equals("its key", m.key, std::string("LuauCheckedCall")); - ensure_equals("and words", joined(m.args), std::string("<1st>")); - ensure("an argument that fails, in a function", - ALMessageMap::luauError("in the function 'f', 'the argument 'x' is used in a way that will error at runtime", m)); - ensure_equals("in a function", m.key + joined(m.args), std::string("LuauFailsAtRuntimeIn")); - ensure("and in none", ALMessageMap::luauError("the argument 'x' is used in a way that will error at runtime", m)); - ensure_equals("in none", m.key + joined(m.args), std::string("LuauFailsAtRuntime")); + UNSCOPED_INFO("a checked call"); + CHECK(ALMessageMap::luauError("the function 'string.len' expects to get a string as its 1st argument, but is being given a number", m)); + CHECK(m.key == std::string("LuauCheckedCall")); + CHECK(joined(m.args) == std::string("<1st>")); + UNSCOPED_INFO("an argument that fails, in a function"); + CHECK(ALMessageMap::luauError("in the function 'f', 'the argument 'x' is used in a way that will error at runtime", m)); + CHECK(m.key + joined(m.args) == std::string("LuauFailsAtRuntimeIn")); + UNSCOPED_INFO("and in none"); + CHECK(ALMessageMap::luauError("the argument 'x' is used in a way that will error at runtime", m)); + CHECK(m.key + joined(m.args) == std::string("LuauFailsAtRuntime")); } - template<> template<> - void almessagemap_object::test<7>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: unmarked templates and brackets are literal and fill puts words in as is", "[almessagemap]") { - set_test_name("a template without marks is its message whole; a bracket that is no mark is part of a literal; and a word is put in as it is"); std::vector args; - ensure("not the start of a longer one", !ALMessageMap::match("Functions cannot change state.", "Functions cannot change state. And more.", args)); - ensure("a bracket before a mark stays in the literal", ALMessageMap::match("Index [a] of [1] is out", "Index [a] of list is out", args)); - ensure_equals("and the word is found after it", joined(args), std::string("")); - ensure("the literal still has to begin the message", !ALMessageMap::match("Index [a] of [1] is out", "a] of list is out", args)); - ensure("a bracket after the last mark too", ALMessageMap::match("[1] is [10] long", "list is [10] long", args) && joined(args) == ""); - ensure_equals("a word with a mark in it is not filled again", ALScriptProblem::fill("[1] and [2]", { "a[2]", "b" }), std::string("a[2] and b")); - ensure_equals("a mark with no word stays", ALScriptProblem::fill("[1] and [3]", { "a" }), std::string("a and [3]")); - ensure_equals("each mark as often as it is used", ALScriptProblem::fill("[1][1]", { "x" }), std::string("xx")); + UNSCOPED_INFO("not the start of a longer one"); + REQUIRE_FALSE(ALMessageMap::match("Functions cannot change state.", "Functions cannot change state. And more.", args)); + UNSCOPED_INFO("a bracket before a mark stays in the literal"); + CHECK(ALMessageMap::match("Index [a] of [1] is out", "Index [a] of list is out", args)); + UNSCOPED_INFO("and the word is found after it"); + CHECK(joined(args) == std::string("")); + UNSCOPED_INFO("the literal still has to begin the message"); + CHECK_FALSE(ALMessageMap::match("Index [a] of [1] is out", "a] of list is out", args)); + { + INFO("a bracket after the last mark too"); + CHECK(ALMessageMap::match("[1] is [10] long", "list is [10] long", args)); + CHECK(joined(args) == ""); + } + UNSCOPED_INFO("a word with a mark in it is not filled again"); + CHECK(ALScriptProblem::fill("[1] and [2]", { "a[2]", "b" }) == std::string("a[2] and b")); + UNSCOPED_INFO("a mark with no word stays"); + CHECK(ALScriptProblem::fill("[1] and [3]", { "a" }) == std::string("a and [3]")); + UNSCOPED_INFO("each mark as often as it is used"); + CHECK(ALScriptProblem::fill("[1][1]", { "x" }) == std::string("xx")); } - template<> template<> - void almessagemap_object::test<8>() + TEST_CASE_METHOD(almessagemap_data, "almessagemap: why a SLua require found nothing is keyed with the module name left blank", "[almessagemap]") { - set_test_name("why a SLua require found nothing, keyed: Luau's navigator's words and the studio's own, the module's name left for [1]"); ALMessageMap::Match m; - ensure("a child", ALMessageMap::luauRequire("could not resolve child component \"util\"", m) && m.key == "PreprocRequireNoChild" && - m.args == std::vector({ "", "util" })); - ensure("ambiguous, the fuller shape", ALMessageMap::luauRequire("could not resolve child component \"util\" (ambiguous)", m) && - m.key == "PreprocRequireNoChildAmbiguous"); - ensure("an alias", ALMessageMap::luauRequire("@lib is not a valid alias", m) && m.key == "PreprocRequireNotAlias" && m.args[1] == "lib"); - ensure("a cycle", ALMessageMap::luauRequire("detected alias cycle (@a -> @b -> @a)", m) && m.key == "PreprocRequireAliasCycle" && - m.args[1] == "@a -> @b -> @a"); - ensure("the studio's own", ALMessageMap::luauRequire("the alias '@sl-std' is reserved: aliases starting @sl- are Second Life's", m) && - m.key == "PreprocRequireReserved" && m.args[1] == "sl-std"); - ensure("a folder with both configurations", ALMessageMap::luauRequire("could not resolve alias \"lib\" (ambiguous configuration file)", m) && - m.key == "PreprocRequireConfigAmbiguous" && m.args[1] == "lib"); - ensure("a file nothing lets in", ALMessageMap::luauRequire("'C:/lib/x.luau' is there, but no include folder or alias lets a require read it", m) && - m.key == "PreprocRequireNotAdmitted" && m.args[1] == "C:/lib/x.luau"); - ensure("an alias past its configuration's folder", - ALMessageMap::luauRequire("the configuration in 'C:/lib' names 'D:/out', which is outside it and outside the include folders", m) && - m.key == "PreprocRequireAliasRefused" && m.args == std::vector({ "", "C:/lib", "D:/out" })); - ensure("a configuration that could not be read", - ALMessageMap::luauRequire("the configuration 'C:/lib/.luaurc' could not be read: expected value", m) && - m.key == "PreprocRequireConfigBroken" && m.args == std::vector({ "", "C:/lib/.luaurc", "expected value" })); - ensure("a world alias naming the disk", - ALMessageMap::luauRequire("the alias stands for '/x', a path on disk, which a configuration in the world may not name", m) && - m.key == "PreprocRequireAliasNamesDisk" && m.args[1] == "/x"); - ensure("a reason of no known shape", !ALMessageMap::luauRequire("something else went wrong", m)); - ensure("its keys known as such", ALMessageMap::requireReason("PreprocRequireNoChild") && !ALMessageMap::requireReason("PreprocRequireTooDeep")); + { + INFO("a child"); + REQUIRE(ALMessageMap::luauRequire("could not resolve child component \"util\"", m)); + CHECK(m.key == "PreprocRequireNoChild"); + CHECK(m.args == std::vector({ "", "util" })); + } + { + INFO("ambiguous, the fuller shape"); + CHECK(ALMessageMap::luauRequire("could not resolve child component \"util\" (ambiguous)", m)); + CHECK(m.key == "PreprocRequireNoChildAmbiguous"); + } + { + INFO("an alias"); + CHECK(ALMessageMap::luauRequire("@lib is not a valid alias", m)); + CHECK(m.key == "PreprocRequireNotAlias"); + CHECK(m.args[1] == "lib"); + } + { + INFO("a cycle"); + CHECK(ALMessageMap::luauRequire("detected alias cycle (@a -> @b -> @a)", m)); + CHECK(m.key == "PreprocRequireAliasCycle"); + CHECK(m.args[1] == "@a -> @b -> @a"); + } + { + INFO("the studio's own"); + CHECK(ALMessageMap::luauRequire("the alias '@sl-std' is reserved: aliases starting @sl- are Second Life's", m)); + CHECK(m.key == "PreprocRequireReserved"); + CHECK(m.args[1] == "sl-std"); + } + { + INFO("a folder with both configurations"); + CHECK(ALMessageMap::luauRequire("could not resolve alias \"lib\" (ambiguous configuration file)", m)); + CHECK(m.key == "PreprocRequireConfigAmbiguous"); + CHECK(m.args[1] == "lib"); + } + { + INFO("a file nothing lets in"); + CHECK(ALMessageMap::luauRequire("'C:/lib/x.luau' is there, but no include folder or alias lets a require read it", m)); + CHECK(m.key == "PreprocRequireNotAdmitted"); + CHECK(m.args[1] == "C:/lib/x.luau"); + } + { + INFO("an alias past its configuration's folder"); + CHECK(ALMessageMap::luauRequire("the configuration in 'C:/lib' names 'D:/out', which is outside it and outside the include folders", m)); + CHECK(m.key == "PreprocRequireAliasRefused"); + CHECK(m.args == std::vector({ "", "C:/lib", "D:/out" })); + } + { + INFO("a configuration that could not be read"); + CHECK(ALMessageMap::luauRequire("the configuration 'C:/lib/.luaurc' could not be read: expected value", m)); + CHECK(m.key == "PreprocRequireConfigBroken"); + CHECK(m.args == std::vector({ "", "C:/lib/.luaurc", "expected value" })); + } + { + INFO("a world alias naming the disk"); + CHECK(ALMessageMap::luauRequire("the alias stands for '/x', a path on disk, which a configuration in the world may not name", m)); + CHECK(m.key == "PreprocRequireAliasNamesDisk"); + CHECK(m.args[1] == "/x"); + } + UNSCOPED_INFO("a reason of no known shape"); + CHECK_FALSE(ALMessageMap::luauRequire("something else went wrong", m)); + { + INFO("its keys known as such"); + CHECK(ALMessageMap::requireReason("PreprocRequireNoChild")); + CHECK_FALSE(ALMessageMap::requireReason("PreprocRequireTooDeep")); + } } } diff --git a/indra/alscript/tests/almodulelook_test.cpp b/indra/alscript/tests/almodulelook_test.cpp index 7621d1f60f..199e7262eb 100644 --- a/indra/alscript/tests/almodulelook_test.cpp +++ b/indra/alscript/tests/almodulelook_test.cpp @@ -30,7 +30,7 @@ #include "fsyspath.h" #include "llfile.h" -#include "../test/lltut.h" +#include #include #include @@ -79,21 +79,15 @@ namespace } } -namespace tut +namespace { struct almodulelook_data { ALModuleLook look; }; - typedef test_group almodulelook_group; - typedef almodulelook_group::object almodulelook_object; - almodulelook_group almodulelook_instance("almodulelook"); - - template<> template<> - void almodulelook_object::test<1>() + TEST_CASE_METHOD(almodulelook_data, "almodulelook: LSL: open texts first then folder files by path; never the script itself", "[almodulelook]") { - set_test_name("LSL: a folder's files by their path from it, what each declares; an open text first, the script itself never"); Scratch s; const std::string util = s.write("inc/lib/util.lsl", "integer helper() { return 1; }\n#define CHANNEL -42\n"); s.write("inc/notes.txt", "not a module\n"); @@ -103,19 +97,32 @@ namespace tut input.texts.push_back({ "inventory:open", "open.lsl", 3, std::make_shared("integer opened;\n") }); input.texts.push_back({ "object:self", "self.lsl", 1, std::make_shared("integer mine;\n") }); const std::vector found = look.look(input); - ensure_equals("the open one and the file, not the script itself nor another kind", found.size(), size_t(2)); - ensure("the open one first", found[0].path == "inventory:open" && found[0].exports == std::vector{ "opened" }); + UNSCOPED_INFO("the open one and the file, not the script itself nor another kind"); + REQUIRE(found.size() == size_t(2)); + { + INFO("the open one first"); + CHECK(found[0].path == "inventory:open"); + CHECK(found[0].exports == std::vector{ "opened" }); + } const ALModuleLook::Candidate* file = named(found, "util"); - ensure("the file, as it stands", file && file->path == ALIncludeIdentity::ofFile(util)); - ensure("by its path from the folder, then its own name", file->names.size() >= 2 && file->names[0] == "lib/util.lsl" && file->names[1] == "util.lsl"); - ensure("what it declares", file->exports == std::vector({ "CHANNEL", "helper" }) || - file->exports == std::vector({ "helper", "CHANNEL" })); + { + INFO("the file, as it stands"); + REQUIRE(file); + CHECK(file->path == ALIncludeIdentity::ofFile(util)); + } + { + INFO("by its path from the folder, then its own name"); + REQUIRE(file->names.size() >= 2); + CHECK(file->names[0] == "lib/util.lsl"); + CHECK(file->names[1] == "util.lsl"); + } + UNSCOPED_INFO("what it declares"); + CHECK((file->exports == std::vector({ "CHANNEL", "helper" }) || + file->exports == std::vector({ "helper", "CHANNEL" }))); } - template<> template<> - void almodulelook_object::test<2>() + TEST_CASE_METHOD(almodulelook_data, "almodulelook: SLua: a module by stem under prefix and alias; reread when its version moves", "[almodulelook]") { - set_test_name("SLua: a module by its stem, under a folder's prefix and each alias; an open text read again only as its version moves"); Scratch s; s.write("mods/net/init.luau", "return { get = function() end }\n"); s.write("mods/util.luau", "local M = {}\nfunction M.clamp() end\nreturn M\n"); @@ -128,24 +135,38 @@ namespace tut input.texts.push_back({ "inventory:open", "open.luau", 1, text }); std::vector found = look.look(input); const ALModuleLook::Candidate* util = named(found, "util"); - ensure("by its stem, under the prefix", util && util->names[0] == "@lib/util" && util->exports == std::vector{ "clamp" }); + { + INFO("by its stem, under the prefix"); + REQUIRE(util); + CHECK(util->names[0] == "@lib/util"); + CHECK(util->exports == std::vector{ "clamp" }); + } const ALModuleLook::Candidate* folder = named(found, "init"); - ensure("a folder's init by the folder's name first", folder && folder->names[0] == "@lib/net"); + { + INFO("a folder's init by the folder's name first"); + REQUIRE(folder); + CHECK(folder->names[0] == "@lib/net"); + } const ALModuleLook::Candidate* open = named(found, "open"); - ensure("an open one under the alias too", open && std::find(open->names.begin(), open->names.end(), "@lib/open") != open->names.end()); - ensure("what it exports", open->exports == std::vector{ "one" }); + { + INFO("an open one under the alias too"); + REQUIRE(open); + REQUIRE(std::find(open->names.begin(), open->names.end(), "@lib/open") != open->names.end()); + } + UNSCOPED_INFO("what it exports"); + CHECK(open->exports == std::vector{ "one" }); // The same version, another text: kept as it was read. input.texts[0].text = std::make_shared("local M = {}\nfunction M.two() end\nreturn M\n"); - ensure("the same version: as read", named(look.look(input), "open")->exports == std::vector{ "one" }); + UNSCOPED_INFO("the same version: as read"); + CHECK(named(look.look(input), "open")->exports == std::vector{ "one" }); input.texts[0].version = 2; - ensure("a new one: read again", named(look.look(input), "open")->exports == std::vector{ "two" }); + UNSCOPED_INFO("a new one: read again"); + CHECK(named(look.look(input), "open")->exports == std::vector{ "two" }); } - template<> template<> - void almodulelook_object::test<3>() + TEST_CASE_METHOD(almodulelook_data, "almodulelook: SLua: checked exports of the same text win for files and open texts alike", "[almodulelook]") { - set_test_name("SLua: what the analysis found a module exports, where it checked that very text, over what the text alone says; a file and an open text alike"); Scratch s; const std::string made_text = "local function make()\n local t = {}\n t.alpha = 1\n t.beta = function() end\n return t\nend\nreturn make()\n"; const std::string made = s.write("mods/made.luau", made_text); @@ -153,23 +174,25 @@ namespace tut input.lua = true; input.self = "object:self"; input.folders = { { std::string(), s.at("mods") } }; - ensure("the text alone says nothing of a table made by a call", named(look.look(input), "made")->exports.empty()); + UNSCOPED_INFO("the text alone says nothing of a table made by a call"); + REQUIRE(named(look.look(input), "made")->exports.empty()); ALLuauExports::checked(ALIncludeIdentity::ofFile(made), made_text, { "alpha", "beta" }); - ensure("checked: what its type says", named(look.look(input), "made")->exports == std::vector({ "alpha", "beta" })); + UNSCOPED_INFO("checked: what its type says"); + CHECK(named(look.look(input), "made")->exports == std::vector({ "alpha", "beta" })); auto text = std::make_shared(made_text); input.texts.push_back({ "inventory:open", "open.luau", 1, text }); ALLuauExports::checked("inventory:open", made_text, { "gamma" }); - ensure("an open text checked", named(look.look(input), "open")->exports == std::vector{ "gamma" }); + UNSCOPED_INFO("an open text checked"); + CHECK(named(look.look(input), "open")->exports == std::vector{ "gamma" }); input.texts[0].text = std::make_shared("local M = {}\nfunction M.delta() end\nreturn M\n"); input.texts[0].version = 2; - ensure("its text moved on since: read as it says", named(look.look(input), "open")->exports == std::vector{ "delta" }); + UNSCOPED_INFO("its text moved on since: read as it says"); + CHECK(named(look.look(input), "open")->exports == std::vector{ "delta" }); } - template<> template<> - void almodulelook_object::test<4>() + TEST_CASE_METHOD(almodulelook_data, "almodulelook: extensions match in any case and a later check of the same text is taken", "[almodulelook]") { - set_test_name("a file's extension in any case, as a require finds it; what the analysis found of a file asked each look, a later check of the same text taken"); Scratch s; const std::string text = "local M = {}\nfunction M.one() end\nreturn M\n"; const std::string upper = s.write("mods/Upper.LUAU", text); @@ -180,16 +203,27 @@ namespace tut input.folders = { { "@lib/", s.at("mods") } }; std::vector found = look.look(input); const ALModuleLook::Candidate* one = named(found, "Upper"); - ensure("listed, by its stem", one && one->names[0] == "@lib/Upper" && one->exports == std::vector{ "one" }); + { + INFO("listed, by its stem"); + REQUIRE(one); + CHECK(one->names[0] == "@lib/Upper"); + CHECK(one->exports == std::vector{ "one" }); + } ALLuauExports::checked(ALIncludeIdentity::ofFile(upper), text, { "first" }); - ensure("checked", named(look.look(input), "Upper")->exports == std::vector{ "first" }); + UNSCOPED_INFO("checked"); + CHECK(named(look.look(input), "Upper")->exports == std::vector{ "first" }); ALLuauExports::checked(ALIncludeIdentity::ofFile(upper), text, { "second" }); - ensure("checked again, the text the same: taken", named(look.look(input), "Upper")->exports == std::vector{ "second" }); + UNSCOPED_INFO("checked again, the text the same: taken"); + CHECK(named(look.look(input), "Upper")->exports == std::vector{ "second" }); input.lua = false; found = look.look(input); one = named(found, "defs.LSLH"); - ensure("an LSL include's in any case, by its name whole", one && one->exports == std::vector{ "shouted" }); + { + INFO("an LSL include's in any case, by its name whole"); + REQUIRE(one); + CHECK(one->exports == std::vector{ "shouted" }); + } } } diff --git a/indra/alscript/tests/alnotecardformat_test.cpp b/indra/alscript/tests/alnotecardformat_test.cpp index 9342240362..0c237fb14c 100644 --- a/indra/alscript/tests/alnotecardformat_test.cpp +++ b/indra/alscript/tests/alnotecardformat_test.cpp @@ -27,12 +27,12 @@ #include "../core/alnotecardformat.h" -#include "../test/lltut.h" +#include #include #include -namespace tut +namespace { struct alnotecardformat_data { @@ -48,44 +48,42 @@ namespace tut return out; } }; - typedef test_group alnotecardformat_group; - typedef alnotecardformat_group::object alnotecardformat_object; - alnotecardformat_group alnotecardformat_instance("ALNotecardFormat"); - template<> template<> - void alnotecardformat_object::test<1>() + TEST_CASE_METHOD(alnotecardformat_data, "ALNotecardFormat: guess: JSON by brace or bracket; settings by mostly keys; text otherwise", "[ALNotecardFormat]") { - set_test_name("guessed: JSON by its brace or bracket; settings where most lines are keys or sections, two at least; plain text otherwise"); - ensure_equals("an object", ALNotecardFormat::guess(" \n{ \"a\": 1 }\n"), std::string("json")); - ensure_equals("an array", ALNotecardFormat::guess("[1, 2]"), std::string("json")); - ensure_equals("settings", ALNotecardFormat::guess("# the door\n[Door]\nspeed = 2\nsound: creak\n\nlocked=false\n"), std::string("config")); - ensure_equals("one key is not enough", ALNotecardFormat::guess("title: A Story\nOnce upon a time.\nThe end.\n"), std::string("text")); - ensure_equals("prose with a colon or two", ALNotecardFormat::guess("Note: read this.\nWelcome to the store\nAsk for help\nHave fun\n"), + UNSCOPED_INFO("an object"); + CHECK(ALNotecardFormat::guess(" \n{ \"a\": 1 }\n") == std::string("json")); + UNSCOPED_INFO("an array"); + CHECK(ALNotecardFormat::guess("[1, 2]") == std::string("json")); + UNSCOPED_INFO("settings"); + CHECK(ALNotecardFormat::guess("# the door\n[Door]\nspeed = 2\nsound: creak\n\nlocked=false\n") == std::string("config")); + UNSCOPED_INFO("one key is not enough"); + CHECK(ALNotecardFormat::guess("title: A Story\nOnce upon a time.\nThe end.\n") == std::string("text")); + UNSCOPED_INFO("prose with a colon or two"); + CHECK(ALNotecardFormat::guess("Note: read this.\nWelcome to the store\nAsk for help\nHave fun\n") == std::string("text")); - ensure_equals("nothing", ALNotecardFormat::guess(""), std::string("text")); + CHECK(ALNotecardFormat::guess("") == std::string("text")); } - template<> template<> - void alnotecardformat_object::test<2>() + TEST_CASE_METHOD(alnotecardformat_data, "ALNotecardFormat: linesPast finds lines longer than llGetNotecardLine returns by bytes", "[ALNotecardFormat]") { - set_test_name("the lines, counted from 0, longer than llGetNotecardLine returns: by bytes"); const std::string one_line_too_long(ALNotecardFormat::READ_LINE_BYTES + 1, 'x'); const std::string exactly(ALNotecardFormat::READ_LINE_BYTES, 'y'); const std::vector past = ALNotecardFormat::linesPast("short\n" + one_line_too_long + "\n" + exactly + "\n" + one_line_too_long); - ensure("the second and the fourth", past == std::vector({ 1, 3 })); + UNSCOPED_INFO("the second and the fourth"); + CHECK(past == std::vector({ 1, 3 })); // 513 characters of two bytes each: 1026 bytes. std::string wide; for (int i = 0; i < 513; ++i) { wide += "\xC3\xA9"; } - ensure("by bytes, not characters", ALNotecardFormat::linesPast(wide) == std::vector({ 0 })); + UNSCOPED_INFO("by bytes, not characters"); + CHECK(ALNotecardFormat::linesPast(wide) == std::vector({ 0 })); } - template<> template<> - void alnotecardformat_object::test<3>() + TEST_CASE_METHOD(alnotecardformat_data, "ALNotecardFormat: a JSON outline nests keys and places array objects and spans each value", "[ALNotecardFormat]") { - set_test_name("a JSON outline: every key nested under the one it is in, an array's objects by their places, each spanning its value"); const std::string text = "{\n" " \"name\": \"Door\",\n" " \"speed\": 2.5,\n" @@ -94,61 +92,98 @@ namespace tut " \"on\": true\n" "}\n"; const std::vector entries = ALNotecardFormat::outline(text); - ensure_equals("the keys", said(entries), + REQUIRE(said(entries) == std::string("name=\"Door\" speed=2.5 sounds={...} .open=\"creak\" .shut=\"bang\" stops=[...] .[2]={...} ..at=3 on=true")); const ALScriptOutlineEntry& sounds = entries[2]; - ensure("sounds named where it is written", sounds.nameSpan.line == 3 && sounds.nameSpan.column == 2 && sounds.nameSpan.endColumn == 10); - ensure("and spanning its object", sounds.span.line == 3 && sounds.span.endLine == 3 && sounds.span.endColumn == 47); - ensure("a scalar spans its value", entries[1].span.line == 2 && entries[1].span.endColumn == 14); + { + INFO("sounds named where it is written"); + CHECK(sounds.nameSpan.line == 3); + CHECK(sounds.nameSpan.column == 2); + CHECK(sounds.nameSpan.endColumn == 10); + } + { + INFO("and spanning its object"); + CHECK(sounds.span.line == 3); + CHECK(sounds.span.endLine == 3); + CHECK(sounds.span.endColumn == 47); + } + { + INFO("a scalar spans its value"); + CHECK(entries[1].span.line == 2); + CHECK(entries[1].span.endColumn == 14); + } } - template<> template<> - void alnotecardformat_object::test<4>() + TEST_CASE_METHOD(alnotecardformat_data, "ALNotecardFormat: a JSON outline of a top array or broken text or too many entries", "[ALNotecardFormat]") { - set_test_name("a JSON outline of an array at the top, of text that is not JSON all through, and of more entries than asked for"); - ensure_equals("an array's objects", said(ALNotecardFormat::outline("[{\"a\": 1}, {\"b\": [ {\"c\": null} ]}]")), + UNSCOPED_INFO("an array's objects"); + CHECK(said(ALNotecardFormat::outline("[{\"a\": 1}, {\"b\": [ {\"c\": null} ]}]")) == std::string("[0]={...} .a=1 [1]={...} .b=[...] ..[0]={...} ...c=null")); - ensure_equals("broken off: as far as it goes", said(ALNotecardFormat::outline("{\"a\": {\"b\": 1, \"c\"")), std::string("a={...} .b=1")); - ensure_equals("not JSON at all: nothing", said(ALNotecardFormat::outline("just words: here")), std::string()); - ensure_equals("no more than asked", ALNotecardFormat::outline("{\"a\":1,\"b\":2,\"c\":3}", 2).size(), size_t(2)); + UNSCOPED_INFO("broken off: as far as it goes"); + CHECK(said(ALNotecardFormat::outline("{\"a\": {\"b\": 1, \"c\"")) == std::string("a={...} .b=1")); + UNSCOPED_INFO("not JSON at all: nothing"); + CHECK(said(ALNotecardFormat::outline("just words: here")) == std::string()); + UNSCOPED_INFO("no more than asked"); + REQUIRE(ALNotecardFormat::outline("{\"a\":1,\"b\":2,\"c\":3}", 2).size() == size_t(2)); } - template<> template<> - void alnotecardformat_object::test<5>() + TEST_CASE_METHOD(alnotecardformat_data, "ALNotecardFormat: namedAt finds the notecard named first in llGetNotecardLine and its kin", "[ALNotecardFormat]") { - set_test_name("a notecard a script names where it reads one: the string first in llGetNotecardLine and its kin, LSL's or SLua's, in either quote"); const std::string lsl = " key q = llGetNotecardLine(\"config\", line);"; std::optional named = ALNotecardFormat::namedAt(lsl, 34); - ensure("inside the string", named && named->name == "config" && named->begin == 30 && named->end == 38); - ensure("on its quote", ALNotecardFormat::namedAt(lsl, 30).has_value()); - ensure("not on the call's name", !ALNotecardFormat::namedAt(lsl, 16)); - ensure("SLua's, single quotes, blanks about the bracket", ALNotecardFormat::namedAt("ll.GetNumberOfNotecardLines ( 'dialog' )", 32)->name == "dialog"); - ensure("an escape taken out", ALNotecardFormat::namedAt("llGetNotecardLineSync(\"say \\\"hi\\\"\", 0)", 25)->name == "say \"hi\""); - ensure("a string another call is given, not", !ALNotecardFormat::namedAt("llSay(0, \"config\");", 12)); - ensure("the second argument, not", !ALNotecardFormat::namedAt("llGetNotecardLine(name, \"x\")", 25)); + { + INFO("inside the string"); + REQUIRE(named); + CHECK(named->name == "config"); + CHECK(named->begin == 30); + CHECK(named->end == 38); + } + UNSCOPED_INFO("on its quote"); + REQUIRE(ALNotecardFormat::namedAt(lsl, 30).has_value()); + UNSCOPED_INFO("not on the call's name"); + REQUIRE_FALSE(ALNotecardFormat::namedAt(lsl, 16)); + UNSCOPED_INFO("SLua's, single quotes, blanks about the bracket"); + CHECK(ALNotecardFormat::namedAt("ll.GetNumberOfNotecardLines ( 'dialog' )", 32)->name == "dialog"); + UNSCOPED_INFO("an escape taken out"); + CHECK(ALNotecardFormat::namedAt("llGetNotecardLineSync(\"say \\\"hi\\\"\", 0)", 25)->name == "say \"hi\""); + UNSCOPED_INFO("a string another call is given, not"); + REQUIRE_FALSE(ALNotecardFormat::namedAt("llSay(0, \"config\");", 12)); + UNSCOPED_INFO("the second argument, not"); + REQUIRE_FALSE(ALNotecardFormat::namedAt("llGetNotecardLine(name, \"x\")", 25)); const std::string script = "default {\n" " state_entry() { llGetNotecardLine(\"config\", 0); llSay(0, \"config\"); }\n" " touch_start(integer n) { llGetNumberOfNotecardLines(\"config\"); llGetNotecardLine(\"other\", 0); }\n" "}\n"; const std::vector read = ALNotecardFormat::readersOf(script, "config"); - ensure_equals("two readers of it", read.size(), size_t(2)); - ensure("the first", read[0].line == 1 && read[0].column == 36 && read[0].endColumn == 44); - ensure("the second", read[1].line == 2 && read[1].column == 54); - ensure("none of another", ALNotecardFormat::readersOf(script, "missing").empty()); + REQUIRE(read.size() == size_t(2)); + CHECK(read[0].line == 1); + CHECK(read[0].column == 36); + CHECK(read[0].endColumn == 44); + CHECK(read[1].line == 2); + CHECK(read[1].column == 54); + UNSCOPED_INFO("none of another"); + REQUIRE(ALNotecardFormat::readersOf(script, "missing").empty()); } - template<> template<> - void alnotecardformat_object::test<6>() + TEST_CASE_METHOD(alnotecardformat_data, "ALNotecardFormat: readersOf counts a lone CR as a line break as LF is and CRLF as one", "[ALNotecardFormat]") { - set_test_name("a notecard's readers by the lines a document makes of the script: a lone CR a break as LF is, CRLF one"); const std::string script = "default {\r" " state_entry() { llGetNotecardLine(\"config\", 0); }\r\n" " touch_start(integer n) { llGetNumberOfNotecardLines(\"config\"); }\n" "}\r"; const std::vector read = ALNotecardFormat::readersOf(script, "config"); - ensure_equals("both", read.size(), size_t(2)); - ensure("the first on the second line", read[0].line == 1 && read[0].column == 36 && read[0].endColumn == 44); - ensure("the second on the third", read[1].line == 2 && read[1].column == 54); + REQUIRE(read.size() == size_t(2)); + { + INFO("the first on the second line"); + CHECK(read[0].line == 1); + CHECK(read[0].column == 36); + CHECK(read[0].endColumn == 44); + } + { + INFO("the second on the third"); + CHECK(read[1].line == 2); + CHECK(read[1].column == 54); + } } } diff --git a/indra/alscript/tests/alpreprocessor_test.cpp b/indra/alscript/tests/alpreprocessor_test.cpp index b19d3d871e..ea92613d1f 100644 --- a/indra/alscript/tests/alpreprocessor_test.cpp +++ b/indra/alscript/tests/alpreprocessor_test.cpp @@ -28,7 +28,7 @@ #include "../lsl/allslservice.h" #include "../preprocessor/alpreprocessor.h" -#include "../test/lltut.h" +#include #include "Luau/Compiler.h" #include "lua.h" @@ -40,6 +40,7 @@ #include #include #include +#include #include #include @@ -70,7 +71,7 @@ namespace } } -namespace tut +namespace { struct alpreprocessor_data { @@ -197,15 +198,16 @@ namespace tut // The grid's builtins, which the optimizer and const folding read. A // test that needs them asks for them itself: Tailslide keeps them for // the process once loaded, so whichever asks first loads them. - static void ensureGridBuiltins() + static void requireGridBuiltins() { ALLSLService service; std::string error; - ensure("builtins: " + error, service.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error)); + INFO("builtins: " << error); + REQUIRE(service.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error)); } // The same but the notes of a require nothing reads, which a script - // made to try something else may leave about (test 50 tries them). + // made to try something else may leave about (the last case here tries them). static std::string faults(const ALPreprocessor::Result& r) { ALPreprocessor::Result kept; @@ -234,40 +236,34 @@ namespace tut } }; - typedef test_group alpreprocessor_group; - typedef alpreprocessor_group::object alpreprocessor_object; - alpreprocessor_group alpreprocessor_instance("alpreprocessor"); - - template<> template<> - void alpreprocessor_object::test<1>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: object-like and function-like macros expand in code and nowhere else", "[alpreprocessor]") { - set_test_name("object-like and function-like macros expand in code and nowhere else"); const std::string source = "#define GREETING \"hello\"\n" "#define TWICE(x) x + x\n" "default { state_entry() { llSay(0, GREETING); integer y = TWICE(2); string s = \"GREETING\"; /* GREETING */ } }"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", r.text, + CHECK(messages(r) == std::string()); + CHECK(r.text == std::string("\n\ndefault { state_entry() { llSay(0, \"hello\"); integer y = 2 + 2; string s = \"GREETING\"; /* GREETING */ } }")); - ensure("not disabled", !r.disabled); + CHECK_FALSE(r.disabled); // A function-like macro's name on its own is only a name, and its // arguments may run over lines. r = ALPreprocessor::run("#define M(a, b) a+b\nM + 1\nM(1,\n 2)\n", options()); - ensure_equals("name alone", r.text, std::string("\nM + 1\n1+2\n")); - ensure_equals("no problems", messages(r), std::string()); + UNSCOPED_INFO("name alone"); + CHECK(r.text == std::string("\nM + 1\n1+2\n")); + CHECK(messages(r) == std::string()); // `#define` needs a name; an unknown directive is an error; a // redefinition is a warning; `#undef` takes it away. r = ALPreprocessor::run("#define\n#foo\n#define A 1\n#define A 2\nA\n#undef A\nA\n", options()); - ensure_equals("errors", messages(r), std::string("E 0: macro name missing in #define\nE 1: ill formed preprocessor directive '#foo'\nW 3: macro 'A' redefined\n")); - ensure_equals("undef", squeeze(r.text), std::string("2\nA\n")); + CHECK(messages(r) == std::string("E 0: macro name missing in #define\nE 1: ill formed preprocessor directive '#foo'\nW 3: macro 'A' redefined\n")); + UNSCOPED_INFO("undef"); + CHECK(squeeze(r.text) == std::string("2\nA\n")); } - template<> template<> - void alpreprocessor_object::test<2>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: variadic macros with stringizing and pasting", "[alpreprocessor]") { - set_test_name("variadics, stringizing and pasting"); const std::string source = "#define STR(x) #x\n" "#define CAT(a, b) a ## b\n" "#define LOG(...) llOwnerSay(#__VA_ARGS__)\n" @@ -280,8 +276,8 @@ namespace tut "STR(__LINE__)\n" "TAIL(1) TAIL(1, 2, 3)\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", squeeze(r.text), + CHECK(messages(r) == std::string()); + CHECK(squeeze(r.text) == std::string("\"hello world\"\n" "\"\\\"q\\\\n\\\"\"\n" "llSay(0, \"x\")\n" @@ -291,22 +287,24 @@ namespace tut "1 1 2, 3\n")); r = ALPreprocessor::run("#define P(a) ## a\n#define Q(a) # b\n#define R(a, a) a\nP(1)\n", options()); - ensure_equals("refused", messages(r), + UNSCOPED_INFO("refused"); + CHECK(messages(r) == std::string("E 0: '##' cannot be at either end of a macro body\n" "E 1: '#' is not followed by a macro parameter\n" "E 2: duplicate macro parameter 'a'\n")); - ensure_equals("nothing defined", squeeze(r.text), std::string("P(1)\n")); + UNSCOPED_INFO("nothing defined"); + CHECK(squeeze(r.text) == std::string("P(1)\n")); r = ALPreprocessor::run("#define M(a, b) a b\nM(1)\nM(1, 2, 3)\nM(1\n", options()); - ensure_equals("arity", messages(r), + UNSCOPED_INFO("arity"); + CHECK(messages(r) == std::string("E 1: too few arguments for macro 'M'\nE 2: too many arguments for macro 'M'\nE 3: unterminated argument list invoking macro 'M'\n")); - ensure_equals("left as written", squeeze(r.text), std::string("M(1)\nM(1, 2, 3)\nM(1\n")); + UNSCOPED_INFO("left as written"); + CHECK(squeeze(r.text) == std::string("M(1)\nM(1, 2, 3)\nM(1\n")); } - template<> template<> - void alpreprocessor_object::test<3>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: rescanning stops where the standard says", "[alpreprocessor]") { - set_test_name("rescanning stops where the standard says"); const std::string source = "#define f(a) a*g\n" "#define g(a) f(a)\n" "#define foo foo\n" @@ -318,14 +316,12 @@ namespace tut "AB\n" "I(I)(3)\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", squeeze(r.text), std::string("2*9*g\nfoo\na b AB\nI(3)\n")); + CHECK(messages(r) == std::string()); + CHECK(squeeze(r.text) == std::string("2*9*g\nfoo\na b AB\nI(3)\n")); } - template<> template<> - void alpreprocessor_object::test<4>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: conditionals over C integer expressions keep their lines", "[alpreprocessor]") { - set_test_name("conditionals over C integer expressions keep their lines"); const std::string source = "#define A 1\n" "#define B 0\n" "#if A && !B\n" @@ -364,8 +360,8 @@ namespace tut "#endif\n" "end\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", squeeze(r.text), std::string("yes\nyes2\nyes3\nyes4\nyes5\nyes6\nend\n")); + CHECK(messages(r) == std::string()); + CHECK(squeeze(r.text) == std::string("yes\nyes2\nyes3\nyes4\nyes5\nyes6\nend\n")); std::istringstream in(r.text); std::string line; S32 n = 0; @@ -373,13 +369,16 @@ namespace tut { ++n; } - ensure_equals("one line each", n, 37); - ensure_equals("yes4 on its line", r.text.find("yes4"), r.text.find("yes4")); + UNSCOPED_INFO("one line each"); + CHECK(n == 37); + UNSCOPED_INFO("yes4 on its line"); + CHECK(r.text.find("yes4") == r.text.find("yes4")); ALSourceMap::Loc loc = r.map.toSource(19, 0); - ensure_equals("yes4 from line 19", loc.line, 19); + UNSCOPED_INFO("yes4 from line 19"); + CHECK(loc.line == 19); r = ALPreprocessor::run("#if 1\n#else\n#else\n#endif\n#endif\n#elif 1\n#if 1/0\n#endif\n#if 1 +\n#endif\n#if (1\n#endif\n#if 1\n", options()); - ensure_equals("errors", messages(r), + CHECK(messages(r) == std::string("E 2: #else after #else\n" "E 4: #endif without #if\n" "E 5: #elif without #if\n" @@ -389,10 +388,8 @@ namespace tut "E 12: #if without #endif at the end of the file\n")); } - template<> template<> - void alpreprocessor_object::test<5>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: includes come from the resolver once if asked and name their own file", "[alpreprocessor]") { - set_test_name("includes come from the resolver, once if they ask, and say which file they are"); add("a.lsl", "integer a = __LINE__; string f = __SHORTFILE__; string id = __ASSETID__;\n", "aaaa"); add("once.lsl", "#pragma once\nonce\n"); add("deep.lsl", "#include \"deep.lsl\"\n"); @@ -409,49 +406,56 @@ namespace tut "#include NAME\n" "FROM_INC\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string("E 5: could not find include file 'missing.lsl'\n")); - ensure_equals("text", r.text, + CHECK(messages(r) == std::string("E 5: could not find include file 'missing.lsl'\n")); + CHECK(r.text == std::string("\ninteger a = 1; string f = \"a.lsl\"; string id = \"aaaa\";\n" "string top = \"main.lsl\"; string tid = \"NOT_IN_WORLD\";\n" "\n\nonce\n" "\n\n\n\n\n\n7\n")); - ensure_equals("includes", r.includes.size(), 3u); - ensure_equals("first include", r.includes[0], std::string("a.lsl")); - ensure_equals("pending", r.pending.size(), 1u); - ensure_equals("pending name", r.pending[0], std::string("pending.lsl")); - ensure_equals("files", r.map.files().size(), 4u); - ensure_equals("main file", r.map.files()[0].name, std::string("main.lsl")); - ensure_equals("an include by its identity", r.map.fileOf("once.lsl"), 2); + REQUIRE(r.includes.size() == 3u); + CHECK(r.includes[0] == std::string("a.lsl")); + REQUIRE(r.pending.size() == 1u); + CHECK(r.pending[0] == std::string("pending.lsl")); + REQUIRE(r.map.files().size() == 4u); + CHECK(r.map.files()[0].name == std::string("main.lsl")); + UNSCOPED_INFO("an include by its identity"); + CHECK(r.map.fileOf("once.lsl") == 2); r = ALPreprocessor::run("#include \"deep.lsl\"\n", options()); - ensure("too deep", messages(r).find("nested too deeply") != std::string::npos); - ensure("in the include", r.problems.front().file == "deep.lsl"); + REQUIRE(messages(r).find("nested too deeply") != std::string::npos); + UNSCOPED_INFO("in the include"); + CHECK(r.problems.front().file == "deep.lsl"); r = ALPreprocessor::run("#include \"err.lsl\"\nafter\n", options()); - ensure_equals("error in an include", messages(r), std::string("E err.lsl:0: bad\n")); - ensure_equals("goes on", squeeze(r.text), std::string("after\n")); + UNSCOPED_INFO("error in an include"); + CHECK(messages(r) == std::string("E err.lsl:0: bad\n")); + UNSCOPED_INFO("goes on"); + CHECK(squeeze(r.text) == std::string("after\n")); ALPreprocessor::Options none = options(); none.resolve = nullptr; r = ALPreprocessor::run("#include \"a.lsl\"\n", none); - ensure_equals("no resolver", messages(r), std::string("E 0: could not find include file 'a.lsl'\n")); + UNSCOPED_INFO("no resolver"); + CHECK(messages(r) == std::string("E 0: could not find include file 'a.lsl'\n")); } - template<> template<> - void alpreprocessor_object::test<6>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: Firestorm quirks: multiline strings; off switch; #line; predefines", "[alpreprocessor]") { - set_test_name("Firestorm's LSL quirks: strings over lines, the off switch, #line, and the predefined macros"); ALPreprocessor::Result r = ALPreprocessor::run("string s = \"a\nb\";\nx", options()); - ensure_equals("a newline in a string", r.text, std::string("string s = \"a\\nb\"\n;\nx")); + UNSCOPED_INFO("a newline in a string"); + CHECK(r.text == std::string("string s = \"a\\nb\"\n;\nx")); const std::string off = "//fspreprocessor off\n#define X 1\nX\n"; r = ALPreprocessor::run(off, options()); - ensure("disabled", r.disabled); - ensure_equals("as it was", r.text, off); - ensure_equals("maps to itself", r.map.toSource(2, 0).line, 2); + CHECK(r.disabled); + UNSCOPED_INFO("as it was"); + CHECK(r.text == off); + UNSCOPED_INFO("maps to itself"); + CHECK(r.map.toSource(2, 0).line == 2); r = ALPreprocessor::run("#line 5 \"x\"\ny\n", options()); - ensure_equals("line passed through", r.text, std::string("//#line 5 \"x\"\ny\n")); + UNSCOPED_INFO("line passed through"); + CHECK(r.text == std::string("//#line 5 \"x\"\ny\n")); ALPreprocessor::Options o = options(); o.agentId = "1234"; @@ -460,29 +464,41 @@ namespace tut r = ALPreprocessor::run("__LINE__ __FILE__ __SHORTFILE__ __ASSETID__\n__AGENTKEY__ __AGENTID__ __AGENTIDRAW__ __AGENTNAME__ __UNIXTIME__\n" "integer(1.5) list(1, 2) key(x)\n__DATE__ __TIME__\n", o); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("line one", r.text.substr(0, r.text.find('\n')), std::string("1 \"main.lsl\" \"main.lsl\" \"asset-1\"")); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("line one"); + CHECK(r.text.substr(0, r.text.find('\n')) == std::string("1 \"main.lsl\" \"main.lsl\" \"asset-1\"")); std::istringstream in(r.text); std::string line; std::getline(in, line); std::getline(in, line); - ensure_equals("line two", line, std::string("\"1234\" \"1234\" 1234 \"Some Body\" 1234567890")); + UNSCOPED_INFO("line two"); + CHECK(line == std::string("\"1234\" \"1234\" 1234 \"Some Body\" 1234567890")); std::getline(in, line); - ensure_equals("line three", line, std::string("((integer)(1.5)) ((list)(1, 2)) ((key)(x))")); + UNSCOPED_INFO("line three"); + CHECK(line == std::string("((integer)(1.5)) ((list)(1, 2)) ((key)(x))")); std::getline(in, line); - ensure_equals("date and time", line.size(), 13u + 1u + 10u); - ensure("date quoted", line[0] == '"' && line[12] == '"'); - ensure("time quoted", line[14] == '"' && line[23] == '"'); - ensure_equals("time colons", line.substr(17, 1) + line.substr(20, 1), std::string("::")); + UNSCOPED_INFO("date and time"); + REQUIRE(line.size() == 13u + 1u + 10u); + { + INFO("date quoted"); + CHECK(line[0] == '"'); + CHECK(line[12] == '"'); + } + { + INFO("time quoted"); + CHECK(line[14] == '"'); + CHECK(line[23] == '"'); + } + UNSCOPED_INFO("time colons"); + CHECK(line.substr(17, 1) + line.substr(20, 1) == std::string("::")); r = ALPreprocessor::run("#define BS 1 \\\n + 2\nBS\n", options()); - ensure_equals("continuation", squeeze(r.text), std::string("1 + 2\n")); + UNSCOPED_INFO("continuation"); + CHECK(squeeze(r.text) == std::string("1 + 2\n")); } - template<> template<> - void alpreprocessor_object::test<7>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: switch becomes the jump table Firestorm emits", "[alpreprocessor]") { - set_test_name("switch becomes the jump table Firestorm emits"); const std::string source = "#define USE_SWITCHES\n" "switch (x) {\n" "case 1: llSay(0, \"one\"); break;\n" @@ -490,9 +506,9 @@ namespace tut "default: llSay(0, \"other\");\n" "}\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string()); - ensure("used", r.usedSwitches); - ensure_equals("text", r.text, + CHECK(messages(r) == std::string()); + CHECK(r.usedSwitches); + CHECK(r.text == std::string("\n{if((x) == (1))jump _sw1_1;\n" "if((x) == (2))jump _sw1_2;\n" "jump _sw1_default;\n" @@ -505,63 +521,69 @@ namespace tut ALPreprocessor::Options o = options(); o.switches = true; r = ALPreprocessor::run("switch (a) { case 1: switch (b) { case 2: break; } break; }\n", o); - ensure_equals("nested", r.text, + UNSCOPED_INFO("nested"); + CHECK(r.text == std::string("{if((a) == (1))jump _sw2_1;\njump _sw2_end;\n" " @_sw2_1; {if((b) == (2))jump _sw1_1;\njump _sw1_end;\n" " @_sw1_1; jump _sw1_end; \n@_sw1_end;\n} jump _sw2_end; \n@_sw2_end;\n}\n")); - ensure_equals("not used when not asked", ALPreprocessor::run("switch (a) { }\n", options()).text, std::string("switch (a) { }\n")); + UNSCOPED_INFO("not used when not asked"); + CHECK(ALPreprocessor::run("switch (a) { }\n", options()).text == std::string("switch (a) { }\n")); r = ALPreprocessor::run("switch (a) { case 1 }\n", o); - ensure_equals("a case with no end", messages(r), std::string("E 0: cannot find ':' or '{' after case\n")); + UNSCOPED_INFO("a case with no end"); + CHECK(messages(r) == std::string("E 0: cannot find ':' or '{' after case\n")); // A value worked out again for each case, said where that is more // than a cost: a call, or one that changes something. const std::string warned = "W 0: the switch's value is worked out again for each case: put it in a local first\n"; - ensure_equals("a call", messages(ALPreprocessor::run("switch ((integer)llFrand(3)) { case 1: break; }\n", o)), warned); - ensure_equals("an increment", messages(ALPreprocessor::run("switch (i++) { case 1: break; }\n", o)), warned); - ensure_equals("an assignment", messages(ALPreprocessor::run("switch (i = 2) { case 1: break; }\n", o)), warned); - ensure_equals("not a name", messages(ALPreprocessor::run("switch (i) { case 1: break; }\n", o)), std::string()); - ensure_equals("nor arithmetic or a field", messages(ALPreprocessor::run("switch ((i + 1) * v.x) { case 1: break; }\n", o)), std::string()); - ensure_equals("nor a comparison", messages(ALPreprocessor::run("switch (i == 2) { case 1: break; }\n", o)), std::string()); + UNSCOPED_INFO("a call"); + CHECK(messages(ALPreprocessor::run("switch ((integer)llFrand(3)) { case 1: break; }\n", o)) == warned); + UNSCOPED_INFO("an increment"); + CHECK(messages(ALPreprocessor::run("switch (i++) { case 1: break; }\n", o)) == warned); + UNSCOPED_INFO("an assignment"); + CHECK(messages(ALPreprocessor::run("switch (i = 2) { case 1: break; }\n", o)) == warned); + UNSCOPED_INFO("not a name"); + CHECK(messages(ALPreprocessor::run("switch (i) { case 1: break; }\n", o)) == std::string()); + UNSCOPED_INFO("nor arithmetic or a field"); + CHECK(messages(ALPreprocessor::run("switch ((i + 1) * v.x) { case 1: break; }\n", o)) == std::string()); + UNSCOPED_INFO("nor a comparison"); + CHECK(messages(ALPreprocessor::run("switch (i == 2) { case 1: break; }\n", o)) == std::string()); } - template<> template<> - void alpreprocessor_object::test<8>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: lazy lists read and write through the helper", "[alpreprocessor]") { - set_test_name("lazy lists read and write through the helper"); const std::string source = "#define USE_LAZY_LISTS\n" "list l; integer i = (integer)l[2]; l[0] = 5; string s = (string)(l[(integer)l[1]]); rotation r = (rotation)l[0];\n" "x = (integer)(l[0]; f((float)l[0], l[1] = 2);\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string()); - ensure("used", r.usedLazyLists); + CHECK(messages(r) == std::string()); + CHECK(r.usedLazyLists); const std::string helper = "list lazy_list_set(list L, integer i, list v)\n{\n while (llGetListLength(L) < i)\n L = L + 0;\n return llListReplaceList(L, v, i, i);\n}\n\n"; - ensure_equals("text", r.text, + CHECK(r.text == helper + "\nlist l; integer i = llList2Integer(l, 2); l = lazy_list_set(l,0,[5]); string s = llList2String(l, llList2Integer(l, 1)); rotation r = llList2Rot(l, 0);\n" "x = (integer)(l[0]; f(llList2Float(l, 0), l = lazy_list_set(l,1,[2]));\n"); ALPreprocessor::Options o = options(); o.lazyLists = true; r = ALPreprocessor::run("integer a = b == c;\n", o); - ensure("nothing to do", !r.usedLazyLists); - ensure_equals("untouched", r.text, std::string("integer a = b == c;\n")); + UNSCOPED_INFO("nothing to do"); + CHECK_FALSE(r.usedLazyLists); + UNSCOPED_INFO("untouched"); + CHECK(r.text == std::string("integer a = b == c;\n")); } - template<> template<> - void alpreprocessor_object::test<9>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: compression drops comments and every blank that is not needed", "[alpreprocessor]") { - set_test_name("compression drops comments and every blank that is not needed"); ALPreprocessor::Options o = options(); o.compress = true; ALPreprocessor::Result r = ALPreprocessor::run("// comment\ndefault { state_entry ( ) { integer a = - -1 ; /* x */ llSay( 0 , \"a b\" ) ; a ++ ; float f = 1 . 5; } }\n", o); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", r.text, std::string("default{state_entry(){integer a=- -1;llSay(0,\"a b\");a++;float f=1 . 5;}}\n")); + CHECK(messages(r) == std::string()); + CHECK(r.text == std::string("default{state_entry(){integer a=- -1;llSay(0,\"a b\");a++;float f=1 . 5;}}\n")); r = ALPreprocessor::run("a\n\n\nb / /* c */ d\n", o); - ensure_equals("lines kept, not multiplied", r.text, std::string("a\nb/d\n")); + UNSCOPED_INFO("lines kept, not multiplied"); + CHECK(r.text == std::string("a\nb/d\n")); } - template<> template<> - void alpreprocessor_object::test<10>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua: strings and comments and # kept; require gathered into modules", "[alpreprocessor]") { - set_test_name("SLua: its own strings and comments, # in a line, and require gathered into modules"); const std::string source = "--#define N 3\n" "local s = [[N]] -- N\n" "local t = \"N\" .. #N_list .. 'N' .. `N{N}`\n" @@ -569,21 +591,22 @@ namespace tut "print(N)\n" "--#line 3\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options(true)); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", r.text, + CHECK(messages(r) == std::string()); + CHECK(r.text == std::string("\nlocal s = [[N]] -- N\nlocal t = \"N\" .. #N_list .. 'N' .. `N{N}`\n--[==[ N\nN ]==]\nprint(3)\n--#line 3\n")); add("a", "local b = require(\"b\")\nreturn { b = b }\n"); add("b", "return 42\n"); add("c", "return require(\"c\")\n"); r = ALPreprocessor::run("local a = require(\"a\")\nlocal b = require('b')\nlocal x = require(name)\n", options(true)); - ensure_equals("problems", faults(r), std::string()); + CHECK(faults(r) == std::string()); // Numbered as met: a, then what a requires. const std::string bundled = bundleOf({ "local b = require(2)\nreturn { b = b }\n", "return 42\n" }, "local a = require(1)\nlocal b = require(2)\nlocal x = require(name)\n"); - ensure_equals("modules", r.text, bundled); - ensure_equals("included", r.includes.size(), 2u); - ensure_equals("b mapped to its file", r.map.files()[r.map.toSource(7, 0).file].name, std::string("b")); + CHECK(r.text == bundled); + REQUIRE(r.includes.size() == 2u); + UNSCOPED_INFO("b mapped to its file"); + CHECK(r.map.files()[r.map.toSource(7, 0).file].name == std::string("b")); // The modules' own lines, which the studio shows nothing of: not // the table's, which are no one's, nor the script's. std::string others; @@ -591,8 +614,10 @@ namespace tut { others += std::to_string(first) + "-" + std::to_string(last) + " "; } - ensure_equals("the modules' lines", others, std::string("3-4 7-7 ")); - ensure("not apart unless asked", !r.apart.valid); + UNSCOPED_INFO("the modules' lines"); + CHECK(others == std::string("3-4 7-7 ")); + UNSCOPED_INFO("not apart unless asked"); + CHECK_FALSE(r.apart.valid); // Apart, for the analyzers: the script with its requires as calls, // and each module as the run made it, each mapped to its file; the // text the bundle still. @@ -600,108 +625,106 @@ namespace tut apart.apart = true; const ALPreprocessor::Result split = ALPreprocessor::run("local a = require(\"a\")\nlocal b = require('b')\nlocal x = require(name)\n", apart); - ensure_equals("the bundle as ever", split.text, bundled); - ensure("apart", split.apart.valid && split.apart.modules.size() == 2); - ensure_equals("the script, its requires calls", split.apart.script.text, + UNSCOPED_INFO("the bundle as ever"); + CHECK(split.text == bundled); + CHECK(split.apart.valid); + REQUIRE(split.apart.modules.size() == 2); + UNSCOPED_INFO("the script, its requires calls"); + CHECK(split.apart.script.text == std::string("local a = require(\"a\")\nlocal b = require('b')\nlocal x = require(name)\n")); - ensure_equals("the first reached", split.apart.modules[0].key, std::string("a")); - ensure_equals("as made, its own require a call", split.apart.modules[0].text, std::string("local b = require(\"b\")\nreturn { b = b }\n")); - ensure_equals("then what it reached", split.apart.modules[1].key, std::string("b")); - ensure_equals("b", split.apart.modules[1].text, std::string("return 42\n")); + UNSCOPED_INFO("the first reached"); + CHECK(split.apart.modules[0].key == std::string("a")); + UNSCOPED_INFO("as made, its own require a call"); + CHECK(split.apart.modules[0].text == std::string("local b = require(\"b\")\nreturn { b = b }\n")); + UNSCOPED_INFO("then what it reached"); + CHECK(split.apart.modules[1].key == std::string("b")); + CHECK(split.apart.modules[1].text == std::string("return 42\n")); const ALSourceMap::Loc in_a = split.apart.modules[0].map.toSource(1, 0); - ensure("mapped to its file", in_a.found() && split.apart.modules[0].map.files()[in_a.file].name == "a" && in_a.line == 1); - ensure("the script to its own", split.apart.script.map.toSource(2, 6).file == 0); + { + INFO("mapped to its file"); + REQUIRE(in_a.found()); + CHECK(split.apart.modules[0].map.files()[in_a.file].name == "a"); + CHECK(in_a.line == 1); + } + UNSCOPED_INFO("the script to its own"); + CHECK(split.apart.script.map.toSource(2, 6).file == 0); r = ALPreprocessor::run("local c = require(\"c\")\n", options(true)); - ensure_equals("a cycle", messages(r), std::string("E c:0: 'c' requires itself\n")); + UNSCOPED_INFO("a cycle"); + CHECK(messages(r) == std::string("E c:0: 'c' requires itself\n")); r = ALPreprocessor::run("local d = require(\"d\")\n", options(true)); - ensure_equals("missing", messages(r), std::string("E 0: could not find module 'd'\n")); - ensure_equals("left alone", r.text, std::string("local d = require(\"d\")\n")); + UNSCOPED_INFO("missing"); + CHECK(messages(r) == std::string("E 0: could not find module 'd'\n")); + UNSCOPED_INFO("left alone"); + CHECK(r.text == std::string("local d = require(\"d\")\n")); r = ALPreprocessor::run("--fspreprocessor off\n--#define X\n", options(true)); - ensure("off in Lua", r.disabled); + UNSCOPED_INFO("off in Lua"); + CHECK(r.disabled); } - template<> template<> - void alpreprocessor_object::test<11>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: the source map goes both ways", "[alpreprocessor]") { - set_test_name("the source map goes both ways"); add("inc", "x;\n"); ALPreprocessor::Result r = ALPreprocessor::run("#define X 10\ninteger a = X + 1;\n#include \"inc\"\ny;\n", options()); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", r.text, std::string("\ninteger a = 10 + 1;\n\nx;\ny;\n")); + CHECK(messages(r) == std::string()); + CHECK(r.text == std::string("\ninteger a = 10 + 1;\n\nx;\ny;\n")); ALSourceMap::Loc loc = r.map.toSource(1, 0); - ensure("found", loc.found()); - ensure_equals("integer", loc.line * 1000 + loc.column, 1000); + REQUIRE(loc.found()); + UNSCOPED_INFO("integer"); + CHECK(loc.line * 1000 + loc.column == 1000); loc = r.map.toSource(1, 5); - ensure_equals("within integer", loc.column, 5); + UNSCOPED_INFO("within integer"); + CHECK(loc.column == 5); loc = r.map.toSource(1, 12); - ensure_equals("the 10 is the X", loc.column, 12); + UNSCOPED_INFO("the 10 is the X"); + CHECK(loc.column == 12); loc = r.map.toSource(1, 13); - ensure_equals("still the X", loc.column, 12); + UNSCOPED_INFO("still the X"); + CHECK(loc.column == 12); loc = r.map.toSource(1, 15); - ensure_equals("the + past the X", loc.column, 14); + UNSCOPED_INFO("the + past the X"); + CHECK(loc.column == 14); loc = r.map.toSource(1, 40); - ensure_equals("past the end", loc.column, 18); + UNSCOPED_INFO("past the end"); + CHECK(loc.column == 18); loc = r.map.toSource(0, 0); - ensure("a directive line has no origin", !loc.found()); + UNSCOPED_INFO("a directive line has no origin"); + REQUIRE_FALSE(loc.found()); loc = r.map.toSource(3, 0); - ensure_equals("from the include", loc.file, 1); - ensure_equals("its line", loc.line, 0); + UNSCOPED_INFO("from the include"); + CHECK(loc.file == 1); + UNSCOPED_INFO("its line"); + CHECK(loc.line == 0); loc = r.map.toSource(4, 0); - ensure_equals("after the include", loc.file * 1000 + loc.line, 3); + UNSCOPED_INFO("after the include"); + CHECK(loc.file * 1000 + loc.line == 3); loc = r.map.toExpanded(0, 1, 12); - ensure("forward found", loc.found()); - ensure_equals("X went to line 1", loc.line, 1); - ensure_equals("X went to column 12", loc.column, 12); + UNSCOPED_INFO("forward found"); + REQUIRE(loc.found()); + UNSCOPED_INFO("X went to line 1"); + CHECK(loc.line == 1); + UNSCOPED_INFO("X went to column 12"); + CHECK(loc.column == 12); loc = r.map.toExpanded(0, 1, 3); - ensure_equals("within integer forward", loc.column, 3); + UNSCOPED_INFO("within integer forward"); + CHECK(loc.column == 3); loc = r.map.toExpanded(0, 0, 3); - ensure("a directive made nothing", !loc.found()); + UNSCOPED_INFO("a directive made nothing"); + REQUIRE_FALSE(loc.found()); loc = r.map.toExpanded(0, 3, 0); - ensure_equals("y went past the include", loc.line, 4); + UNSCOPED_INFO("y went past the include"); + CHECK(loc.line == 4); loc = r.map.toExpanded(1, 0, 1); - ensure_equals("the include's x", loc.line, 3); + UNSCOPED_INFO("the include's x"); + CHECK(loc.line == 3); loc = r.map.toExpanded(0, 1, 100); - ensure_equals("past the end forward", loc.column, 19); + UNSCOPED_INFO("past the end forward"); + CHECK(loc.column == 19); } - template<> template<> - void alpreprocessor_object::test<13>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: every golden file comes out as its expected text", "[alpreprocessor]") { - set_test_name("the optimizer runs over the expanded text, and its notes come back to the source"); - ensureGridBuiltins(); - add("consts.lsl", "#define CHANNEL 7\ninteger unused = 1;\n"); - ALPreprocessor::Options o = options(); - o.optimize = true; - o.compress = true; - const std::string source = "#include \"consts.lsl\"\n#define TWICE(x) ((x) * 2)\ndefault\n{\n state_entry()\n {\n llSay(CHANNEL, (string)TWICE(21));\n }\n}\n"; - ALPreprocessor::Result r = ALPreprocessor::run(source, o); - ensure("optimized", r.optimized); - ensure_equals("text", r.text, std::string("default\n{\nstate_entry()\n{\nllSay(7,\"42\");\n}\n}\n")); - // The unused global was in the include, and the note says so. - bool noted = false; - for (const ALScriptProblem& p : r.problems) - { - if (p.message.find("removed the unused global unused") != std::string::npos) - { - noted = true; - ensure_equals("in the include", p.file, std::string("consts.lsl")); - ensure_equals("on its line there", p.line, 1); - } - } - ensure("noted", noted); - // The llSay is on output line 4, which came from source line 6. - const ALSourceMap::Loc loc = r.map.toSource(4, 0); - ensure("found", loc.found()); - ensure_equals("main file", loc.file, 0); - ensure_equals("line", loc.line, 6); - } - - template<> template<> - void alpreprocessor_object::test<12>() - { - set_test_name("every golden file comes out as its expected text"); namespace fs = std::filesystem; const fs::path dir = fsyspath(AL_ALSCRIPT_TEST_DIR) / "preprocessor"; S32 checked = 0; @@ -715,7 +738,8 @@ namespace tut const std::string stem = entry.path().stem().string(); const fs::path expected = dir / (stem + "_expected" + entry.path().extension().string()); ALPreprocessor::Result r = runGolden(entry.path(), "\n"); - ensure_equals("problems in " + name, messages(r), std::string()); + CAPTURE(name); + REQUIRE(messages(r) == std::string()); // With AL_PREPROCESSOR_WRITE_EXPECTED set, the run writes the // expected files instead of checking them: for a new golden // file, or a change that was meant, each to be read over @@ -726,91 +750,147 @@ namespace tut out << r.text; continue; } - ensure("expected file for " + name, fs::exists(expected)); - ensure_equals("output of " + name, r.text, readFile(expected)); + REQUIRE(fs::exists(expected)); + REQUIRE(r.text == readFile(expected)); ++checked; } - ensure("some golden files", checked > 0 || std::getenv("AL_PREPROCESSOR_WRITE_EXPECTED")); + UNSCOPED_INFO("some golden files"); + REQUIRE((checked > 0 || std::getenv("AL_PREPROCESSOR_WRITE_EXPECTED"))); } - template<> template<> - void alpreprocessor_object::test<14>() + + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: the optimizer runs on expanded text and its notes map to the source", "[alpreprocessor]") + { + requireGridBuiltins(); + add("consts.lsl", "#define CHANNEL 7\ninteger unused = 1;\n"); + ALPreprocessor::Options o = options(); + o.optimize = true; + o.compress = true; + const std::string source = "#include \"consts.lsl\"\n#define TWICE(x) ((x) * 2)\ndefault\n{\n state_entry()\n {\n llSay(CHANNEL, (string)TWICE(21));\n }\n}\n"; + ALPreprocessor::Result r = ALPreprocessor::run(source, o); + CHECK(r.optimized); + CHECK(r.text == std::string("default\n{\nstate_entry()\n{\nllSay(7,\"42\");\n}\n}\n")); + // The unused global was in the include, and the note says so. + bool noted = false; + for (const ALScriptProblem& p : r.problems) + { + if (p.message.find("removed the unused global unused") != std::string::npos) + { + noted = true; + UNSCOPED_INFO("in the include"); + REQUIRE(p.file == std::string("consts.lsl")); + UNSCOPED_INFO("on its line there"); + REQUIRE(p.line == 1); + } + } + CHECK(noted); + // The llSay is on output line 4, which came from source line 6. + const ALSourceMap::Loc loc = r.map.toSource(4, 0); + REQUIRE(loc.found()); + UNSCOPED_INFO("main file"); + CHECK(loc.file == 0); + CHECK(loc.line == 6); + } + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: break and continue become jumps; &= |= ^= <<= >>= become assignments", "[alpreprocessor]") { - set_test_name("break and continue become jumps to labels after the loop and at the body's end, and &= |= ^= <<= >>= the assignments they are"); ALPreprocessor::Options o = options(); o.extensions = true; ALPreprocessor::Result r = ALPreprocessor::run("while (a) { if (b) break; if (c) continue; d++; }\n", o); - ensure_equals("problems", messages(r), std::string()); - ensure("used", r.usedExtensions); - ensure_equals("a while", r.text, std::string("while (a) { if (b) jump _brk1; if (c) jump _cnt1; d++; @_cnt1;}@_brk1;\n")); + CHECK(messages(r) == std::string()); + CHECK(r.usedExtensions); + UNSCOPED_INFO("a while"); + CHECK(r.text == std::string("while (a) { if (b) jump _brk1; if (c) jump _cnt1; d++; @_cnt1;}@_brk1;\n")); r = ALPreprocessor::run("for (i = 0; i < 3; i++) if (i == 1) continue; else x++;\n", o); - ensure_equals("a for with one statement for a body gets braces", r.text, std::string("for (i = 0; i < 3; i++) { if (i == 1) jump _cnt1; else x++;@_cnt1;}\n")); + UNSCOPED_INFO("a for with one statement for a body gets braces"); + CHECK(r.text == std::string("for (i = 0; i < 3; i++) { if (i == 1) jump _cnt1; else x++;@_cnt1;}\n")); r = ALPreprocessor::run("do { if (x) break 2; } while (y);\n", o); - ensure_equals("break 2 with one loop is an error", messages(r), std::string("E 0: break outside a loop\n")); + UNSCOPED_INFO("break 2 with one loop is an error"); + CHECK(messages(r) == std::string("E 0: break outside a loop\n")); r = ALPreprocessor::run("while (a) { do { if (x) break 2; if (y) break; } while (b); c++; }\n", o); - ensure_equals("break 2 goes past the outer loop, break past the inner", r.text, + UNSCOPED_INFO("break 2 goes past the outer loop, break past the inner"); + CHECK(r.text == std::string("while (a) { do { if (x) jump _brk1; if (y) jump _brk2; } while (b);@_brk2; c++; }@_brk1;\n")); r = ALPreprocessor::run("continue;\n", o); - ensure_equals("outside a loop", messages(r), std::string("E 0: continue outside a loop\n")); + UNSCOPED_INFO("outside a loop"); + CHECK(messages(r) == std::string("E 0: continue outside a loop\n")); o.switches = true; r = ALPreprocessor::run("while (a) { switch (b) { case 1: break; } while (c) { break; } }\n", o); - ensure_equals("a break in a switch is the switch's, one in a loop inside it the loop's", r.text, + UNSCOPED_INFO("a break in a switch is the switch's, one in a loop inside it the loop's"); + CHECK(r.text == std::string("while (a) { {if((b) == (1))jump _sw1_1;\njump _sw1_end;\n @_sw1_1; jump _sw1_end; \n@_sw1_end;\n} while (c) { jump _brk2; }@_brk2; }\n")); r = ALPreprocessor::run("x &= 6; v.y |= 1 << n; z ^= (a | b); f(k <<= 2, 3); m >>= 1 + p;\n", o); - ensure_equals("the assignments", r.text, std::string("x = x & (6); v.y = v.y | (1 << n); z = z ^ ((a | b)); f(k = k << (2), 3); m = m >> (1 + p);\n")); - ensure_equals("not used when not asked", ALPreprocessor::run("while (a) break;\n", options()).text, std::string("while (a) break;\n")); + UNSCOPED_INFO("the assignments"); + CHECK(r.text == std::string("x = x & (6); v.y = v.y | (1 << n); z = z ^ ((a | b)); f(k = k << (2), 3); m = m >> (1 + p);\n")); + UNSCOPED_INFO("not used when not asked"); + CHECK(ALPreprocessor::run("while (a) break;\n", options()).text == std::string("while (a) break;\n")); r = ALPreprocessor::run("#define USE_EXTENSIONS\nwhile (a) break;\n", options()); - ensure_equals("USE_EXTENSIONS turns it on", r.text, std::string("\nwhile (a) jump _brk1;@_brk1;\n")); + UNSCOPED_INFO("USE_EXTENSIONS turns it on"); + CHECK(r.text == std::string("\nwhile (a) jump _brk1;@_brk1;\n")); r = ALPreprocessor::run("inline f() { }\ninline integer g(integer x) { return x; }\ninline = 3;\n", o); - ensure_equals("inline before a function is taken off and the name kept", r.text, std::string("f() { }\ninteger g(integer x) { return x; }\ninline = 3;\n")); - ensure("the names", r.inlined.size() == 2 && r.inlined[0] == "f" && r.inlined[1] == "g"); + UNSCOPED_INFO("inline before a function is taken off and the name kept"); + CHECK(r.text == std::string("f() { }\ninteger g(integer x) { return x; }\ninline = 3;\n")); + REQUIRE(r.inlined.size() == 2); + CHECK(r.inlined[0] == "f"); + CHECK(r.inlined[1] == "g"); r = ALPreprocessor::run("integer break; while (a) { break = 1; x = continue; }\n", o); - ensure_equals("the words as names are errors, not rewrites", messages(r), + UNSCOPED_INFO("the words as names are errors, not rewrites"); + CHECK(messages(r) == std::string("E 0: 'break' is a name here, which break and continue reserve\n" "E 0: 'break' is a name here, which break and continue reserve\n" "E 0: 'continue' is a name here, which break and continue reserve\n")); - ensure_equals("and left as they were", r.text, std::string("integer break; while (a) { break = 1; x = continue; }\n")); + UNSCOPED_INFO("and left as they were"); + CHECK(r.text == std::string("integer break; while (a) { break = 1; x = continue; }\n")); } - template<> template<> - void alpreprocessor_object::test<15>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a label the script has where a loop's would go is the loop's", "[alpreprocessor]") { - set_test_name("a label the script has where a loop's would go is the loop's"); ALPreprocessor::Options o = options(); o.extensions = true; ALPreprocessor::Result r = ALPreprocessor::run("while (a) { if (b) break; c++; }\n@out;\n", o); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("a label after the loop is the break's", r.text, std::string("while (a) { if (b) jump out; c++; }\n@out;\n")); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("a label after the loop is the break's"); + CHECK(r.text == std::string("while (a) { if (b) jump out; c++; }\n@out;\n")); r = ALPreprocessor::run("while (a) { if (b) continue; c++; @next; }\n", o); - ensure_equals("a label ending the body is the continue's", r.text, std::string("while (a) { if (b) jump next; c++; @next; }\n")); + UNSCOPED_INFO("a label ending the body is the continue's"); + CHECK(r.text == std::string("while (a) { if (b) jump next; c++; @next; }\n")); r = ALPreprocessor::run("do { if (b) break 2; } while (a); @x;\n", o); - ensure_equals("break 2 with one loop is still an error", messages(r), std::string("E 0: break outside a loop\n")); + UNSCOPED_INFO("break 2 with one loop is still an error"); + CHECK(messages(r) == std::string("E 0: break outside a loop\n")); r = ALPreprocessor::run("for (;;) { while (a) { if (b) break 2; } @in; }\n@after;\n", o); - ensure_equals("each loop's own: break 2 to the outer's following label, the inner's ending label unused", r.text, + UNSCOPED_INFO("each loop's own: break 2 to the outer's following label, the inner's ending label unused"); + CHECK(r.text == std::string("for (;;) { while (a) { if (b) jump after; } @in; }\n@after;\n")); r = ALPreprocessor::run("while (a) { break; } x = 1; @late;\n", o); - ensure_equals("a label that is not right after the loop is not the loop's", r.text, std::string("while (a) { jump _brk1; }@_brk1; x = 1; @late;\n")); + UNSCOPED_INFO("a label that is not right after the loop is not the loop's"); + CHECK(r.text == std::string("while (a) { jump _brk1; }@_brk1; x = 1; @late;\n")); } - template<> template<> - void alpreprocessor_object::test<16>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a preprocessor problem carries a key and its words for translation", "[alpreprocessor]") { - set_test_name("a problem of the preprocessor's own carries a key and its words, for a translation to be built the same way"); ALPreprocessor::Options o = options(); o.extensions = true; ALPreprocessor::Result r = ALPreprocessor::run("continue;\n#define f(a, a) a\n", o); // The directives go first, the extensions after: the macro's // problem is said before the loop's. - ensure_equals("two problems", r.problems.size(), size_t(2)); - ensure_equals("the words", r.problems[1].message, std::string("continue outside a loop")); - ensure_equals("the key", r.problems[1].key, std::string("PreprocOutsideLoop")); - ensure("the word it was built with", r.problems[1].args.size() == 1 && r.problems[1].args[0] == "continue"); - ensure_equals("the macro's key", r.problems[0].key, std::string("PreprocDuplicateParameter")); - ensure("and its word", r.problems[0].args.size() == 1 && r.problems[0].args[0] == "a"); - ensure_equals("a translation is the text with the words put in", ALScriptProblem::fill("[1] hors d'une boucle ([1], [2])", { "continue", "x" }), std::string("continue hors d'une boucle (continue, x)")); - ensure_equals("a word with a mark in it is not read again", ALScriptProblem::fill("[1]", { "[2]" }), std::string("[2]")); + REQUIRE(r.problems.size() == size_t(2)); + CHECK(r.problems[1].message == std::string("continue outside a loop")); + CHECK(r.problems[1].key == std::string("PreprocOutsideLoop")); + { + INFO("the word it was built with"); + REQUIRE(r.problems[1].args.size() == 1); + CHECK(r.problems[1].args[0] == "continue"); + } + UNSCOPED_INFO("the macro's key"); + CHECK(r.problems[0].key == std::string("PreprocDuplicateParameter")); + { + INFO("and its word"); + REQUIRE(r.problems[0].args.size() == 1); + CHECK(r.problems[0].args[0] == "a"); + } + UNSCOPED_INFO("a translation is the text with the words put in"); + CHECK(ALScriptProblem::fill("[1] hors d'une boucle ([1], [2])", { "continue", "x" }) == std::string("continue hors d'une boucle (continue, x)")); + UNSCOPED_INFO("a word with a mark in it is not read again"); + CHECK(ALScriptProblem::fill("[1]", { "[2]" }) == std::string("[2]")); } - template<> template<> - void alpreprocessor_object::test<17>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a run is bounded: doubling macros stopped; deep expressions refused", "[alpreprocessor]") { - set_test_name("a run is bounded: macros that double are stopped, a deep expression is refused, and a deep include is"); // Hide sets stop a macro expanding as itself, not one that // doubles: this is 2^24 tokens and would otherwise eat the // machine, on the thread that opened the script. @@ -823,14 +903,22 @@ namespace tut ALPreprocessor::Options o = options(); o.tokenBudget = 100000; ALPreprocessor::Result r = ALPreprocessor::run(bomb, o); - ensure("the run said it ran away", r.overran); - ensure("and gave the source back as it was", r.text == bomb); - ensure("with the reason", !r.problems.empty() && r.problems.back().key == std::string("PreprocTooMuch")); - ensure("an error", r.problems.back().severity == ALScriptProblem::Severity::Error); + UNSCOPED_INFO("the run said it ran away"); + CHECK(r.overran); + UNSCOPED_INFO("and gave the source back as it was"); + CHECK(r.text == bomb); + { + INFO("with the reason"); + REQUIRE_FALSE(r.problems.empty()); + CHECK(r.problems.back().key == std::string("PreprocTooMuch")); + } + CHECK(r.problems.back().severity == ALScriptProblem::Severity::Error); // A run within the budget is untouched by it. ALPreprocessor::Result fine = ALPreprocessor::run("#define A 1\ninteger n = A;\n", o); - ensure("nothing said", !fine.overran); - ensure("and the macro put in", fine.text.find("integer n = 1;") != std::string::npos); + UNSCOPED_INFO("nothing said"); + CHECK_FALSE(fine.overran); + UNSCOPED_INFO("and the macro put in"); + REQUIRE(fine.text.find("integer n = 1;") != std::string::npos); // An expression nested past the depth is refused rather than // taking the C++ stack down with it. std::string deep = "#if "; @@ -847,18 +935,17 @@ namespace tut ALPreprocessor::Options d = options(); d.expressionDepth = 16; ALPreprocessor::Result nested = ALPreprocessor::run(deep, d); - ensure("said", !nested.problems.empty()); - ensure_equals("with its key", nested.problems.front().key, std::string("PreprocExpressionTooDeep")); + REQUIRE_FALSE(nested.problems.empty()); + CHECK(nested.problems.front().key == std::string("PreprocExpressionTooDeep")); // And one within the depth is answered. ALPreprocessor::Result shallow = ALPreprocessor::run("#if ((((1))))\ninteger n;\n#endif\n", d); - ensure("nothing said", shallow.problems.empty()); - ensure("the group taken", shallow.text.find("integer n;") != std::string::npos); + REQUIRE(shallow.problems.empty()); + UNSCOPED_INFO("the group taken"); + REQUIRE(shallow.text.find("integer n;") != std::string::npos); } - template<> template<> - void alpreprocessor_object::test<18>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: an include that includes itself stops at the depth and is listed once", "[alpreprocessor]") { - set_test_name("an include that includes itself is stopped by the depth, and each include is listed once"); add("a.lsl", "#include \"b.lsl\"\n"); add("b.lsl", "#include \"a.lsl\"\n"); ALPreprocessor::Options o = options(); @@ -869,20 +956,20 @@ namespace tut { deep = deep || p.key == "PreprocIncludeTooDeep"; } - ensure("said so", deep); - ensure_equals("each listed once", r.includes.size(), size_t(2)); + CHECK(deep); + UNSCOPED_INFO("each listed once"); + REQUIRE(r.includes.size() == size_t(2)); // With `#pragma once` it settles rather than going deep at all. files.clear(); add("c.lsl", "#pragma once\ninteger c;\n"); ALPreprocessor::Result twice = ALPreprocessor::run("#include \"c.lsl\"\n#include \"c.lsl\"\n", o); - ensure("nothing said", twice.problems.empty()); - ensure_equals("listed once", twice.includes.size(), size_t(1)); - ensure_equals("and put in once", twice.text.find("integer c;"), twice.text.rfind("integer c;")); + REQUIRE(twice.problems.empty()); + REQUIRE(twice.includes.size() == size_t(1)); + UNSCOPED_INFO("and put in once"); + CHECK(twice.text.find("integer c;") == twice.text.rfind("integer c;")); } - template<> template<> - void alpreprocessor_object::test<19>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: finish after an expansion alone gives what one whole run gives", "[alpreprocessor]") { - set_test_name("what a run does after the expansion it will do on its own, and to the same end"); // A caller that expands in rounds, waiting on includes from the // world, leaves the compression off until its last round and // calls `finish` itself. What it gets must be what one run @@ -896,28 +983,29 @@ namespace tut ALPreprocessor::Options expanding = options(); expanding.compress = false; ALPreprocessor::Result in_two = ALPreprocessor::run(source, expanding); - ensure("the expansion alone leaves it as it was", in_two.text.find("// comment") != std::string::npos); + UNSCOPED_INFO("the expansion alone leaves it as it was"); + REQUIRE(in_two.text.find("// comment") != std::string::npos); ALPreprocessor::finish(in_two, whole); - ensure_equals("the same text", in_two.text, at_once.text); - ensure_equals("and the same way back", in_two.map.toSource(0, 0).line, at_once.map.toSource(0, 0).line); + CHECK(in_two.text == at_once.text); + UNSCOPED_INFO("and the same way back"); + CHECK(in_two.map.toSource(0, 0).line == at_once.map.toSource(0, 0).line); } - template<> template<> - void alpreprocessor_object::test<20>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: the scripter's defines reach every script and #undef has the last word", "[alpreprocessor]") { - set_test_name("a scripter's own macros are defined for every script, and a script's #undef has the last word"); ALPreprocessor::Options opts = options(); opts.defines = { "DEBUG", "LEVEL=3", " SPACED = 7 ", "2BAD", "" }; ALPreprocessor::Result r = ALPreprocessor::run("#if DEBUG\nx = LEVEL;\n#endif\ny = SPACED;\n#undef LEVEL\nz = LEVEL;\n", opts); - ensure_equals("no problems", messages(r), std::string()); - ensure("DEBUG is 1, so the block is in: " + r.text, r.text.find("x = 3;") != std::string::npos); - ensure("a value around blanks: " + r.text, r.text.find("y = 7;") != std::string::npos); - ensure("undefined by the script: " + r.text, r.text.find("z = LEVEL;") != std::string::npos); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("DEBUG is 1, so the block is in: " << r.text); + REQUIRE(r.text.find("x = 3;") != std::string::npos); + UNSCOPED_INFO("a value around blanks: " << r.text); + REQUIRE(r.text.find("y = 7;") != std::string::npos); + UNSCOPED_INFO("undefined by the script: " << r.text); + REQUIRE(r.text.find("z = LEVEL;") != std::string::npos); } - template<> template<> - void alpreprocessor_object::test<21>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a run is bounded through chained ?: and nested calls and self includes", "[alpreprocessor]") { - set_test_name("a run is bounded however the script nests: chained ?:, macros in macros' arguments, files that include themselves"); // A chain of ?: is a level each, as a bracket is: said, and not // followed down the machine's stack. std::string chain = "#if "; @@ -932,7 +1020,8 @@ namespace tut } chain += "\nyes\n#endif\n"; ALPreprocessor::Result r = ALPreprocessor::run(chain, options()); - ensure("the chain is too deep: " + messages(r).substr(0, 200), messages(r).find("nests too deeply") != std::string::npos); + UNSCOPED_INFO("the chain is too deep: " << messages(r).substr(0, 200)); + REQUIRE(messages(r).find("nests too deeply") != std::string::npos); // A macro invoked in its own argument, deeper than the bound. std::string nested = "#define F(x) x\n"; @@ -947,13 +1036,19 @@ namespace tut } nested += ";\n"; r = ALPreprocessor::run(nested, options()); - ensure("overran", r.overran); - ensure("and said why: " + messages(r).substr(0, 200), messages(r).find("more deeply than this preprocessor follows") != std::string::npos); - ensure_equals("the text is the source as it was", r.text, nested); + CHECK(r.overran); + UNSCOPED_INFO("and said why: " << messages(r).substr(0, 200)); + REQUIRE(messages(r).find("more deeply than this preprocessor follows") != std::string::npos); + UNSCOPED_INFO("the text is the source as it was"); + CHECK(r.text == nested); // Within the bound, as deep as a person writes, it expands. r = ALPreprocessor::run("#define F(x) x\nF(F(F(F(F(1)))));\n", options()); - ensure("five deep is nothing: " + messages(r), !r.overran && r.text.find("1;") != std::string::npos); + { + INFO("five deep is nothing: " << messages(r)); + CHECK_FALSE(r.overran); + REQUIRE(r.text.find("1;") != std::string::npos); + } // A file that includes itself twice: each level doubles, and the // files it opens are counted against the budget like any tokens. @@ -961,17 +1056,17 @@ namespace tut ALPreprocessor::Options opts = options(); opts.tokenBudget = 200000; r = ALPreprocessor::run("#include \"twice.lsl\"\n", opts); - ensure("overran, rather than opening two to the thirty-second files", r.overran); + UNSCOPED_INFO("overran, rather than opening two to the thirty-second files"); + CHECK(r.overran); } - template<> template<> - void alpreprocessor_object::test<22>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a name in a string literal is escaped; no module path reaches a save", "[alpreprocessor]") { - set_test_name("a name put in a string literal is escaped; a module's path is put nowhere in what a save sends"); ALPreprocessor::Options opts = options(); opts.fileName = "Say \"hi\" \\ there"; ALPreprocessor::Result r = ALPreprocessor::run("llSay(0, __FILE__);\nllSay(0, __SHORTFILE__);\n", opts); - ensure("the quote and the backslash escaped: " + r.text, r.text.find("\"Say \\\"hi\\\" \\\\ there\"") != std::string::npos); + UNSCOPED_INFO("the quote and the backslash escaped: " << r.text); + REQUIRE(r.text.find("\"Say \\\"hi\\\" \\\\ there\"") != std::string::npos); // A module found on a Windows disk: required by its number, and // the folders it is in -- the scripter's own -- nowhere in the text. @@ -981,15 +1076,18 @@ namespace tut module.path = "disk:C:\\Users\\me\\lib\\util.luau"; files["./util"] = module; r = ALPreprocessor::run("local util = require(\"./util\")\n", options(true)); - ensure_equals("nothing wrong", faults(r), std::string()); - ensure("required by its number: " + r.text, r.text.find("local util = require(1)") != std::string::npos); - ensure("no path: " + r.text, r.text.find("disk:") == std::string::npos && r.text.find("Users") == std::string::npos); + CHECK(faults(r) == std::string()); + UNSCOPED_INFO("required by its number: " << r.text); + REQUIRE(r.text.find("local util = require(1)") != std::string::npos); + { + INFO("no path: " << r.text); + REQUIRE(r.text.find("disk:") == std::string::npos); + REQUIRE(r.text.find("Users") == std::string::npos); + } } - template<> template<> - void alpreprocessor_object::test<23>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: an interpolated SLua string with a brace or backtick ends where it ends", "[alpreprocessor]") { - set_test_name("an interpolated SLua string whose expression holds a string with a brace or a backtick ends where it ends"); const std::vector tokens = ALPreprocessor::tokenize("local s = `a{ \"{\" .. `{'}'}` }b` .. X\n", true); std::vector kinds; for (const ALPreprocessor::Token& t : tokens) @@ -999,18 +1097,22 @@ namespace tut kinds.push_back(t.text); } } - ensure("the string whole, then what follows it: " + std::to_string(kinds.size()), - kinds.size() >= 6 && kinds[3] == "`a{ \"{\" .. `{'}'}` }b`" && kinds[4] == ".." && kinds[5] == "X"); + { + INFO("the string whole, then what follows it"); + REQUIRE(kinds.size() >= 6); + CHECK(kinds[3] == "`a{ \"{\" .. `{'}'}` }b`"); + CHECK(kinds[4] == ".."); + CHECK(kinds[5] == "X"); + } // And the macro after it is expanded. ALPreprocessor::Result r = ALPreprocessor::run("--#define X 42\nlocal s = `{\"{\"}` .. X\n", options(true)); - ensure("the macro after the string expanded: " + r.text, r.text.find(".. 42") != std::string::npos); + UNSCOPED_INFO("the macro after the string expanded: " << r.text); + REQUIRE(r.text.find(".. 42") != std::string::npos); } - template<> template<> - void alpreprocessor_object::test<24>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: switch and loop transforms bounded; a long else-if chain no deeper", "[alpreprocessor]") { - set_test_name("the switch and loop transforms are bounded however the script nests, and a long run of else-ifs is no deeper for its length"); ALPreprocessor::Options opts = options(); opts.switches = true; opts.extensions = true; @@ -1028,9 +1130,11 @@ namespace tut } blocks += "\n} }\n"; ALPreprocessor::Result r = ALPreprocessor::run(blocks, opts); - ensure("overran", r.overran); - ensure("and said why: " + messages(r).substr(0, 200), messages(r).find("blocks nest more deeply") != std::string::npos); - ensure_equals("the text is the source as it was", r.text, blocks); + CHECK(r.overran); + UNSCOPED_INFO("and said why: " << messages(r).substr(0, 200)); + REQUIRE(messages(r).find("blocks nest more deeply") != std::string::npos); + UNSCOPED_INFO("the text is the source as it was"); + CHECK(r.text == blocks); // Switches in switches, likewise. std::string switches = "default { state_entry() { integer x;\n"; @@ -1044,8 +1148,10 @@ namespace tut } switches += "\n} }\n"; r = ALPreprocessor::run(switches, opts); - ensure("switches overran", r.overran); - ensure("and said why: " + messages(r).substr(0, 200), messages(r).find("blocks nest more deeply") != std::string::npos); + UNSCOPED_INFO("switches overran"); + CHECK(r.overran); + UNSCOPED_INFO("and said why: " << messages(r).substr(0, 200)); + REQUIRE(messages(r).find("blocks nest more deeply") != std::string::npos); // As deep as anybody writes, done. std::string loops = "default { state_entry() { integer i;\n"; @@ -1060,8 +1166,13 @@ namespace tut } loops += "\n} }\n"; r = ALPreprocessor::run(loops, opts); - ensure("thirty deep is nothing: " + messages(r).substr(0, 200), !r.overran && !r.hasErrors()); - ensure("and the loops were lowered: " + r.text.substr(0, 200), r.text.find("jump _brk") != std::string::npos); + { + INFO("thirty deep is nothing: " << messages(r).substr(0, 200)); + CHECK_FALSE(r.overran); + CHECK_FALSE(r.hasErrors()); + } + UNSCOPED_INFO("and the loops were lowered: " << r.text.substr(0, 200)); + REQUIRE(r.text.find("jump _brk") != std::string::npos); // A loop governing a long chain of else-ifs, on a thread with what // the platform gives one -- half a megabyte on a Mac: the chain is @@ -1074,42 +1185,65 @@ namespace tut chain += "} }\n"; std::thread worker([&]() { r = ALPreprocessor::run(chain, opts); }); worker.join(); - ensure("the chain is no deeper for its length: " + messages(r).substr(0, 200), !r.overran && !r.hasErrors()); - ensure("and all of it is there", r.text.find("else if (i == 19999) i += 1;") != std::string::npos); + { + INFO("the chain is no deeper for its length: " << messages(r).substr(0, 200)); + CHECK_FALSE(r.overran); + CHECK_FALSE(r.hasErrors()); + } + UNSCOPED_INFO("and all of it is there"); + REQUIRE(r.text.find("else if (i == 19999) i += 1;") != std::string::npos); } - template<> template<> - void alpreprocessor_object::test<25>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: each line of a multiline comment or long string maps back to its own", "[alpreprocessor]") { - set_test_name("every line of a token over several lines maps back to its own: a block comment, a long string"); const std::string source = "#define X 1\n/* one\n two\n three */ integer x = X;\n"; ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("nothing wrong", messages(r), std::string()); - ensure("the comment on the lines it was on: " + r.text, r.text.find("\n/* one\n two\n three */ integer x = 1;") != std::string::npos); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("the comment on the lines it was on: " << r.text); + REQUIRE(r.text.find("\n/* one\n two\n three */ integer x = 1;") != std::string::npos); ALSourceMap::Loc loc = r.map.toSource(2, 4); - ensure("the comment's middle line maps", loc.found()); - ensure_equals("to its own line", loc.line, 2); - ensure_equals("and column", loc.column, 4); + UNSCOPED_INFO("the comment's middle line maps"); + REQUIRE(loc.found()); + UNSCOPED_INFO("to its own line"); + CHECK(loc.line == 2); + UNSCOPED_INFO("and column"); + CHECK(loc.column == 4); loc = r.map.toSource(3, 4); - ensure_equals("its last line, before what follows it on the line", loc.line, 3); - ensure_equals("at its own column", loc.column, 4); + UNSCOPED_INFO("its last line, before what follows it on the line"); + CHECK(loc.line == 3); + UNSCOPED_INFO("at its own column"); + CHECK(loc.column == 4); loc = r.map.toSource(3, 12); - ensure_equals("and what follows it where it is", loc.column, 12); + UNSCOPED_INFO("and what follows it where it is"); + CHECK(loc.column == 12); loc = r.map.toExpanded(0, 2, 4); - ensure("and back", loc.found() && loc.line == 2 && loc.column == 4); + { + INFO("and back"); + REQUIRE(loc.found()); + CHECK(loc.line == 2); + CHECK(loc.column == 4); + } const std::string lua = "--#define X 2\nlocal s = [[one\ntwo\nthree]] .. X\n"; r = ALPreprocessor::run(lua, options(true)); - ensure_equals("nothing wrong in SLua", messages(r), std::string()); + CHECK(messages(r) == std::string()); loc = r.map.toSource(2, 1); - ensure("a long string's middle line maps to its own", loc.found() && loc.line == 2 && loc.column == 1); + { + INFO("a long string's middle line maps to its own"); + REQUIRE(loc.found()); + CHECK(loc.line == 2); + CHECK(loc.column == 1); + } loc = r.map.toSource(3, 11); - ensure("and the macro after it to where it was invoked", loc.found() && loc.line == 3 && loc.column == 11); + { + INFO("and the macro after it to where it was invoked"); + REQUIRE(loc.found()); + CHECK(loc.line == 3); + CHECK(loc.column == 11); + } } - template<> template<> - void alpreprocessor_object::test<26>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: transformAt reads a line's transform; a keyword as a name is none", "[alpreprocessor]") { - set_test_name("a line is read as the transform it is written for, and a name that is one of the words is no transform"); using T = ALPreprocessor::Transform; const auto of = [](std::vector lines, S32 at, std::string* said = nullptr) { std::string word; @@ -1121,49 +1255,72 @@ namespace tut return t; }; std::string word; - ensure("switch", of({ " switch (x)" }, 0, &word) == T::Switch); - ensure_equals("its word", word, std::string("switch")); - ensure("switch not called", of({ "switch = 2;" }, 0) == T::None); - ensure("a case", of({ " case 1:" }, 0, &word) == T::Switch); - ensure_equals("the case's word", word, std::string("case")); - ensure("case assigned", of({ "case = 1;" }, 0) == T::None); - ensure("case called", of({ "case(1);" }, 0) == T::None); - ensure("case with no colon", of({ "case 1" }, 0) == T::None); - ensure("break", of({ "break;" }, 0) == T::Extensions); - ensure("break out of two", of({ "break 2;" }, 0) == T::Extensions); - ensure("continue", of({ "\tcontinue;" }, 0) == T::Extensions); - ensure("break assigned", of({ "break = 3;" }, 0) == T::None); - ensure("an inline function", of({ "inline f(integer a)" }, 0, &word) == T::Extensions); - ensure_equals("inline's word", word, std::string("inline")); - ensure("an inline function with its type", of({ "inline integer f(integer a)" }, 0) == T::Extensions); - ensure("inline after the parameters", of({ "integer f(integer a) inline {" }, 0, &word) == T::Extensions); - ensure_equals("its word", word, std::string("inline")); - ensure("inline after them, the brace on the next line", of({ "f() inline" }, 0) == T::Extensions); - ensure("a name that ends in inline is not", of({ "integer f(integer a) noinline {" }, 0) == T::None); - ensure("inline assigned", of({ "inline = 2;" }, 0) == T::None); - ensure("const before a type", of({ "const integer X = 1;" }, 0, &word) == T::Extensions); - ensure_equals("const's word", word, std::string("const")); - ensure("a const parameter", of({ "f(integer a, const vector v)" }, 0) == T::Extensions); - ensure("a variable named const", of({ "integer const = 1;" }, 0) == T::None); - ensure("a word that starts with const", of({ "constant = 1;" }, 0) == T::None); - ensure("after a brace and a statement's end", of({ "} ; switch (x)" }, 0) == T::Switch); - ensure("a plain statement", of({ "integer x = 1;" }, 0) == T::None); - ensure("a blank line", of({ " " }, 0) == T::None); + CHECK(of({ " switch (x)" }, 0, &word) == T::Switch); + CHECK(word == std::string("switch")); + UNSCOPED_INFO("switch not called"); + CHECK(of({ "switch = 2;" }, 0) == T::None); + CHECK(of({ " case 1:" }, 0, &word) == T::Switch); + CHECK(word == std::string("case")); + UNSCOPED_INFO("case assigned"); + CHECK(of({ "case = 1;" }, 0) == T::None); + UNSCOPED_INFO("case called"); + CHECK(of({ "case(1);" }, 0) == T::None); + UNSCOPED_INFO("case with no colon"); + CHECK(of({ "case 1" }, 0) == T::None); + CHECK(of({ "break;" }, 0) == T::Extensions); + UNSCOPED_INFO("break out of two"); + CHECK(of({ "break 2;" }, 0) == T::Extensions); + CHECK(of({ "\tcontinue;" }, 0) == T::Extensions); + UNSCOPED_INFO("break assigned"); + CHECK(of({ "break = 3;" }, 0) == T::None); + UNSCOPED_INFO("an inline function"); + CHECK(of({ "inline f(integer a)" }, 0, &word) == T::Extensions); + CHECK(word == std::string("inline")); + UNSCOPED_INFO("an inline function with its type"); + CHECK(of({ "inline integer f(integer a)" }, 0) == T::Extensions); + UNSCOPED_INFO("inline after the parameters"); + CHECK(of({ "integer f(integer a) inline {" }, 0, &word) == T::Extensions); + CHECK(word == std::string("inline")); + UNSCOPED_INFO("inline after them, the brace on the next line"); + CHECK(of({ "f() inline" }, 0) == T::Extensions); + UNSCOPED_INFO("a name that ends in inline is not"); + CHECK(of({ "integer f(integer a) noinline {" }, 0) == T::None); + UNSCOPED_INFO("inline assigned"); + CHECK(of({ "inline = 2;" }, 0) == T::None); + UNSCOPED_INFO("const before a type"); + CHECK(of({ "const integer X = 1;" }, 0, &word) == T::Extensions); + CHECK(word == std::string("const")); + UNSCOPED_INFO("a const parameter"); + CHECK(of({ "f(integer a, const vector v)" }, 0) == T::Extensions); + UNSCOPED_INFO("a variable named const"); + CHECK(of({ "integer const = 1;" }, 0) == T::None); + UNSCOPED_INFO("a word that starts with const"); + CHECK(of({ "constant = 1;" }, 0) == T::None); + UNSCOPED_INFO("after a brace and a statement's end"); + CHECK(of({ "} ; switch (x)" }, 0) == T::Switch); + UNSCOPED_INFO("a plain statement"); + CHECK(of({ "integer x = 1;" }, 0) == T::None); + UNSCOPED_INFO("a blank line"); + CHECK(of({ " " }, 0) == T::None); // A brace of its own is the switch's where the line before opens one. - ensure("the switch's brace", of({ "switch (x)", "", " {" }, 2) == T::Switch); - ensure("a brace after anything else", of({ "if (x)", "{" }, 1) == T::None); - ensure("a brace after a break is not the switch's", of({ "break;", "{" }, 1) == T::None); - ensure("a brace with nothing before it", of({ "", "{" }, 1) == T::None); - ensure("a line past the end", of({ "switch (x)" }, 1) == T::None); + UNSCOPED_INFO("the switch's brace"); + CHECK(of({ "switch (x)", "", " {" }, 2) == T::Switch); + UNSCOPED_INFO("a brace after anything else"); + CHECK(of({ "if (x)", "{" }, 1) == T::None); + UNSCOPED_INFO("a brace after a break is not the switch's"); + CHECK(of({ "break;", "{" }, 1) == T::None); + UNSCOPED_INFO("a brace with nothing before it"); + CHECK(of({ "", "{" }, 1) == T::None); + UNSCOPED_INFO("a line past the end"); + CHECK(of({ "switch (x)" }, 1) == T::None); } - template<> template<> - void alpreprocessor_object::test<27>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: verbatimSpan maps copied source back whole but not a macro's output", "[alpreprocessor]") { - set_test_name("a stretch of the output copied from the source maps back whole, and one a macro made does not"); const ALPreprocessor::Result r = ALPreprocessor::run("#define GREET llOwnerSay(\"hi\")\nGREET;\n llSay(0, \"x\");\n", options()); - ensure("no problems: " + messages(r), r.problems.empty()); + UNSCOPED_INFO("no problems: " << messages(r)); + REQUIRE(r.problems.empty()); // Where each call landed in the output. S32 greet_line = -1, greet_column = -1, say_line = -1, say_column = -1; std::istringstream in(r.text); @@ -1181,24 +1338,38 @@ namespace tut say_column = static_cast(at); } } - ensure("both in the output: " + r.text, greet_line >= 0 && say_line >= 0); + { + INFO("both in the output: " << r.text); + CHECK(greet_line >= 0); + CHECK(say_line >= 0); + } ALSourceMap::Loc begin, end; - ensure("llSay is the source's own", r.map.verbatimSpan(say_line, say_column, say_column + 5, begin, end)); - ensure_equals("from its line", begin.line, 2); - ensure_equals("its column", begin.column, 4); - ensure_equals("to its end", end.column, 9); - ensure_equals("in the script", begin.file, 0); - ensure("an insertion after it too", r.map.verbatimSpan(say_line, say_column + 5, say_column + 5, begin, end) && begin.column == 9); - ensure("a stretch of several tokens, the blanks between them the source's", r.map.verbatimSpan(say_line, say_column, say_column + 13, begin, end)); - ensure_equals("to the call's end", end.column, 17); - ensure("what the macro made is not", !r.map.verbatimSpan(greet_line, greet_column, greet_column + 10, begin, end)); + UNSCOPED_INFO("llSay is the source's own"); + REQUIRE(r.map.verbatimSpan(say_line, say_column, say_column + 5, begin, end)); + UNSCOPED_INFO("from its line"); + CHECK(begin.line == 2); + UNSCOPED_INFO("its column"); + CHECK(begin.column == 4); + UNSCOPED_INFO("to its end"); + CHECK(end.column == 9); + UNSCOPED_INFO("in the script"); + CHECK(begin.file == 0); + { + INFO("an insertion after it too"); + CHECK(r.map.verbatimSpan(say_line, say_column + 5, say_column + 5, begin, end)); + CHECK(begin.column == 9); + } + UNSCOPED_INFO("a stretch of several tokens, the blanks between them the source's"); + CHECK(r.map.verbatimSpan(say_line, say_column, say_column + 13, begin, end)); + UNSCOPED_INFO("to the call's end"); + CHECK(end.column == 17); + UNSCOPED_INFO("what the macro made is not"); + CHECK_FALSE(r.map.verbatimSpan(greet_line, greet_column, greet_column + 10, begin, end)); } - template<> template<> - void alpreprocessor_object::test<28>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a weighed optimizer run says what it saved in all and per note", "[alpreprocessor]") { - set_test_name("the optimizer's run, weighed, says what it saved in code: the whole, and each note what its lines came to less"); - ensureGridBuiltins(); + requireGridBuiltins(); const std::string source = "integer unused(integer n)\n" "{\n" " return n * 3;\n" @@ -1215,8 +1386,12 @@ namespace tut o.optimizer.target = ALLSLOptimizer::Target::LSO; o.weigh = true; const ALPreprocessor::Result r = ALPreprocessor::run(source, o); - ensure("optimized", r.optimized); - ensure("weighed, and lighter: " + std::to_string(r.codeBefore) + " to " + std::to_string(r.codeAfter), r.codeAfter > 0 && r.codeAfter < r.codeBefore); + CHECK(r.optimized); + { + INFO("weighed, and lighter"); + CHECK(r.codeAfter > 0); + CHECK(r.codeAfter < r.codeBefore); + } const ALScriptProblem* removed = nullptr; const ALScriptProblem* folded = nullptr; const ALScriptProblem* sizes = nullptr; @@ -1226,14 +1401,31 @@ namespace tut folded = p.line == 8 && p.source == ALScriptProblem::Source::Optimizer && p.key != "OptimizerRemovedFunction" ? &p : folded; sizes = p.key == "OptimizerSizesWeighed" ? &p : sizes; } - ensure("the function's removal noted", removed && removed->savedBytes.has_value()); - ensure("its lines' code all saved: " + std::to_string(removed->savedBytes.value_or(0)), *removed->savedBytes > 0); - ensure("the fold noted, with what its line saved", folded && folded->savedBytes && *folded->savedBytes > 0); - ensure("the whole said", sizes != nullptr); - ensure_equals("in code", sizes->args[2], std::to_string(r.codeBefore)); - ensure_equals("on its target", sizes->args[4], std::string("LSL (LSO)")); + { + INFO("the function's removal noted"); + REQUIRE(removed); + REQUIRE(removed->savedBytes.has_value()); + } + UNSCOPED_INFO("its lines' code all saved"); + CHECK(*removed->savedBytes > 0); + { + INFO("the fold noted, with what its line saved"); + REQUIRE(folded); + REQUIRE(folded->savedBytes); + CHECK(*folded->savedBytes > 0); + } + UNSCOPED_INFO("the whole said"); + REQUIRE(sizes != nullptr); + UNSCOPED_INFO("in code"); + CHECK(sizes->args[2] == std::to_string(r.codeBefore)); + UNSCOPED_INFO("on its target"); + CHECK(sizes->args[4] == std::string("LSL (LSO)")); const S64 lost = S64(r.codeBefore) - S64(r.codeAfter); - ensure("no note says more than the whole lost", *removed->savedBytes <= lost && *folded->savedBytes <= lost); + { + INFO("no note says more than the whole lost"); + REQUIRE(*removed->savedBytes <= lost); + CHECK(*folded->savedBytes <= lost); + } // Not weighed unless asked: no bytes, and the words as they were. o.weigh = false; @@ -1241,17 +1433,20 @@ namespace tut bool said = false; for (const ALScriptProblem& p : bare.problems) { - ensure("nothing weighed", !p.savedBytes); + UNSCOPED_INFO("nothing weighed"); + REQUIRE_FALSE(p.savedBytes); said = said || p.key == "OptimizerSizes"; } - ensure("the size in source only", said && bare.codeBefore == 0); + { + INFO("the size in source only"); + CHECK(said); + CHECK(bare.codeBefore == 0); + } } - template<> template<> - void alpreprocessor_object::test<29>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a script that does not compile: the optimizer stands aside with a note", "[alpreprocessor]") { - set_test_name("a script that does not compile is not the preprocessor's error: the optimizer stands aside with a note where the first error is"); - ensureGridBuiltins(); + requireGridBuiltins(); // Nexii's library, whose ObjectLinksetSittingAvatars has a parameter // without its type, included as a save would include it. llifstream in(std::string(AL_ALSCRIPT_TEST_DIR) + "/preprocessor/include/linkset.lsl", std::ios::binary); @@ -1263,35 +1458,37 @@ namespace tut o.optimize = true; o.weigh = true; ALPreprocessor::Result r = ALPreprocessor::run(source, o); - ensure("no error of the preprocessor's: " + messages(r), !r.hasErrors()); - ensure_equals("one note, on the library's line", messages(r), + UNSCOPED_INFO("no error of the preprocessor's: " << messages(r)); + CHECK_FALSE(r.hasErrors()); + UNSCOPED_INFO("one note, on the library's line"); + CHECK(messages(r) == std::string("N linkset.lsl:110: not optimized: the script does not compile as it stands, and compiling it says why\n")); - ensure("keyed", r.problems.size() == 1 && r.problems[0].key == "OptimizerUncompiled" && r.problems[0].column == 33); - ensure("not optimized", !r.optimized); + REQUIRE(r.problems.size() == 1); + CHECK(r.problems[0].key == "OptimizerUncompiled"); + CHECK(r.problems[0].column == 33); + CHECK_FALSE(r.optimized); ALPreprocessor::Options plain = options(); - ensure_equals("the text as expanded, for the compiler to read", r.text, ALPreprocessor::run(source, plain).text); + UNSCOPED_INFO("the text as expanded, for the compiler to read"); + CHECK(r.text == ALPreprocessor::run(source, plain).text); } - template<> template<> - void alpreprocessor_object::test<30>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua: only the global require finds a module and not a field or method", "[alpreprocessor]") { - set_test_name("SLua: a field or a method named require is the table's, and only the global finds a module"); add("x", "return 1\n"); const ALPreprocessor::Result r = ALPreprocessor::run("local a = t.require(\"x\")\n" "local b = t : require(\"x\")\n" "local c = t .. require(\"x\")\n", options(true)); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("only the concatenated call is a module's", r.text, + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("only the concatenated call is a module's"); + CHECK(r.text == bundleOf({ "return 1\n" }, "local a = t.require(\"x\")\n" "local b = t : require(\"x\")\n" "local c = t .. require(1)\n")); } - template<> template<> - void alpreprocessor_object::test<31>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua: requiresIn finds just the calls a run puts a module in place of", "[alpreprocessor]") { - set_test_name("SLua: the calls a run puts a module in place of are found where they stand, and only those"); const std::string source = "local a = require(\"a\")\n" // 0 "local b = require 'b'\n" // 1: no parentheses "local c = require( -- the next\n" @@ -1304,13 +1501,29 @@ namespace tut "local h = require([[h]])\n" "local i = x .. require(\"i\")\n"; // 10 const std::vector found = ALPreprocessor::requiresIn(source); - ensure_equals("three", found.size(), size_t(3)); - ensure_equals("a", found[0].name, std::string("a")); - ensure("a's stretch", found[0].line == 0 && found[0].column == 10 && found[0].endLine == 0 && found[0].endColumn == 22); - ensure_equals("c", found[1].name, std::string("c")); - ensure("c's stretch, across lines", found[1].line == 2 && found[1].column == 10 && found[1].endLine == 4 && found[1].endColumn == 1); - ensure_equals("i", found[2].name, std::string("i")); - ensure("i's line", found[2].line == 10 && found[2].column == 15); + REQUIRE(found.size() == size_t(3)); + CHECK(found[0].name == std::string("a")); + { + INFO("a's stretch"); + CHECK(found[0].line == 0); + CHECK(found[0].column == 10); + CHECK(found[0].endLine == 0); + CHECK(found[0].endColumn == 22); + } + CHECK(found[1].name == std::string("c")); + { + INFO("c's stretch, across lines"); + CHECK(found[1].line == 2); + CHECK(found[1].column == 10); + CHECK(found[1].endLine == 4); + CHECK(found[1].endColumn == 1); + } + CHECK(found[2].name == std::string("i")); + { + INFO("i's line"); + CHECK(found[2].line == 10); + CHECK(found[2].column == 15); + } // Every one of them is one a run puts a module in place of, and // no other call is. @@ -1318,7 +1531,7 @@ namespace tut add("c", "return 3\n"); add("i", "return 9\n"); const ALPreprocessor::Result r = ALPreprocessor::run(source, options(true)); - ensure_equals("problems", faults(r), std::string()); + CHECK(faults(r) == std::string()); // Each module's function is put at the top once, and asked for by // its number where it was called for. size_t numbered = 0; @@ -1326,13 +1539,12 @@ namespace tut { numbered += at + 8 < r.text.size() && std::isdigit(static_cast(r.text[at + 8])); } - ensure_equals("as many requires as calls found", numbered, found.size()); + UNSCOPED_INFO("as many requires as calls found"); + REQUIRE(numbered == found.size()); } - template<> template<> - void alpreprocessor_object::test<32>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a run says what each include or module resolved to as written", "[alpreprocessor]") { - set_test_name("a run says what each include and module was found as, by the name the file asking wrote"); add("lib/util.lsl", "#include \"inner.lsl\"\nint util() { return 1; }\n"); files["lib/util.lsl"].path = "disk:/scripts/lib/util.lsl"; add("inner.lsl", "int inner() { return 2; }\n"); @@ -1341,33 +1553,54 @@ namespace tut const ALPreprocessor::Result r = ALPreprocessor::run("#include \"lib/util.lsl\"\n#include \"lib/util.lsl\"\ndefault {}\n", options()); // Included twice, with no #pragma once: its own include asked for // twice as well. - ensure_equals("four found", r.resolved.size(), size_t(4)); - ensure("the script's, by its name, as the identity found", - r.resolved[0].from.empty() && r.resolved[0].name == "lib/util.lsl" && r.resolved[0].path == "disk:/scripts/lib/util.lsl" && - !r.resolved[0].require); - ensure("the include's own, from it", r.resolved[1].from == "disk:/scripts/lib/util.lsl" && r.resolved[1].name == "inner.lsl" && - r.resolved[1].path == "inner.lsl"); - ensure("the same asked twice, twice", r.resolved[2].name == "lib/util.lsl" && r.resolved[2].from.empty() && - r.resolved[3].name == "inner.lsl"); + REQUIRE(r.resolved.size() == size_t(4)); + { + INFO("the script's, by its name, as the identity found"); + REQUIRE(r.resolved[0].from.empty()); + CHECK(r.resolved[0].name == "lib/util.lsl"); + CHECK(r.resolved[0].path == "disk:/scripts/lib/util.lsl"); + CHECK_FALSE(r.resolved[0].require); + } + { + INFO("the include's own, from it"); + CHECK(r.resolved[1].from == "disk:/scripts/lib/util.lsl"); + CHECK(r.resolved[1].name == "inner.lsl"); + CHECK(r.resolved[1].path == "inner.lsl"); + } + { + INFO("the same asked twice, twice"); + CHECK(r.resolved[2].name == "lib/util.lsl"); + REQUIRE(r.resolved[2].from.empty()); + CHECK(r.resolved[3].name == "inner.lsl"); + } add("mod", "return 1\n"); files["mod"].path = "disk:/scripts/mod.luau"; const ALPreprocessor::Result lua = ALPreprocessor::run("local m = require(\"mod\")\nlocal n = require(\"none\")\n", options(true)); - ensure_equals("a module found, one not", lua.resolved.size(), size_t(1)); - ensure("as a require", lua.resolved[0].require && lua.resolved[0].name == "mod" && lua.resolved[0].path == "disk:/scripts/mod.luau"); + UNSCOPED_INFO("a module found, one not"); + REQUIRE(lua.resolved.size() == size_t(1)); + { + INFO("as a require"); + CHECK(lua.resolved[0].require); + CHECK(lua.resolved[0].name == "mod"); + CHECK(lua.resolved[0].path == "disk:/scripts/mod.luau"); + } } - template<> template<> - void alpreprocessor_object::test<33>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: repeated parameters and self pasting or stringizing stop within budget", "[alpreprocessor]") { - set_test_name("a parameter used over and over is spent as it goes in, and a token pasted or stringized to itself stops before it outgrows a script"); // A parameter used sixteen times, through arguments that each // expand the last: sixteen to the seventh tokens by the end, which // were made whole before they were counted. const std::string many = "#define D(x) x x x x x x x x x x x x x x x x\nD(D(D(D(D(D(D(1)))))))\n"; ALPreprocessor::Result r = ALPreprocessor::run(many, options()); - ensure("ran away: " + messages(r), r.overran && r.problems.back().key == std::string("PreprocTooMuch")); - ensure("and gave the source back", r.text == many); + { + INFO("ran away: " << messages(r)); + CHECK(r.overran); + CHECK(r.problems.back().key == std::string("PreprocTooMuch")); + } + UNSCOPED_INFO("and gave the source back"); + CHECK(r.text == many); // A token pasted to itself through an indirection doubles its text // and not the tokens: two to the fortieth bytes by the end. @@ -1377,7 +1610,11 @@ namespace tut call = "D(" + call + ")"; } r = ALPreprocessor::run("#define CAT(a) a##a\n#define D(a) CAT(a)\n" + call + ";\n", options()); - ensure("pasting stopped: " + messages(r), r.overran && r.problems.back().key == std::string("PreprocTokenTooLong")); + { + INFO("pasting stopped: " << messages(r)); + CHECK(r.overran); + CHECK(r.problems.back().key == std::string("PreprocTokenTooLong")); + } // A string stringized over and over, each level escaping the last. call = "\"a\""; @@ -1386,30 +1623,52 @@ namespace tut call = "X(" + call + ")"; } r = ALPreprocessor::run("#define S(x) #x\n#define X(x) S(x)\n" + call + ";\n", options()); - ensure("stringizing stopped: " + messages(r), r.overran && r.problems.back().key == std::string("PreprocTokenTooLong")); + { + INFO("stringizing stopped: " << messages(r)); + CHECK(r.overran); + CHECK(r.problems.back().key == std::string("PreprocTokenTooLong")); + } // The bytes stop what the tokens would not, and what fits is // untouched. ALPreprocessor::Options small = options(); small.byteBudget = 1000; r = ALPreprocessor::run("#define L \"" + std::string(600, 'a') + "\"\nL L\n", small); - ensure("the bytes: " + messages(r), r.overran && r.problems.back().key == std::string("PreprocTooMuch")); + { + INFO("the bytes: " << messages(r)); + CHECK(r.overran); + CHECK(r.problems.back().key == std::string("PreprocTooMuch")); + } r = ALPreprocessor::run("#define L \"" + std::string(100, 'a') + "\"\nL L\n", small); - ensure("within them: " + messages(r), !r.overran && r.text.find(std::string(100, 'a')) != std::string::npos); + { + INFO("within them: " << messages(r)); + CHECK_FALSE(r.overran); + REQUIRE(r.text.find(std::string(100, 'a')) != std::string::npos); + } // What a job answers when a run throws: as a run that ran away. const ALPreprocessor::Result failed = ALPreprocessor::failed("integer x;\n", options(), "std::bad_alloc"); - ensure("overran, with the source", failed.overran && failed.text == "integer x;\n"); - ensure("and why", failed.problems.size() == 1 && failed.problems[0].key == std::string("PreprocFailed") && - failed.problems[0].message.find("std::bad_alloc") != std::string::npos && - failed.problems[0].severity == ALScriptProblem::Severity::Error); - ensure("mapped to itself", failed.map.toSource(0, 3).found() && failed.map.toSource(0, 3).line == 0); + { + INFO("overran, with the source"); + CHECK(failed.overran); + CHECK(failed.text == "integer x;\n"); + } + { + INFO("and why"); + REQUIRE(failed.problems.size() == 1); + CHECK(failed.problems[0].key == std::string("PreprocFailed")); + REQUIRE(failed.problems[0].message.find("std::bad_alloc") != std::string::npos); + CHECK(failed.problems[0].severity == ALScriptProblem::Severity::Error); + } + { + INFO("mapped to itself"); + REQUIRE(failed.map.toSource(0, 3).found()); + CHECK(failed.map.toSource(0, 3).line == 0); + } } - template<> template<> - void alpreprocessor_object::test<34>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: nested lazy list reads stop at the block depth or each become a call", "[alpreprocessor]") { - set_test_name("lazy lists' reads nested by macros stop at the depth blocks may nest, and within it each is made a call"); // A read inside a read's brackets, a thousand deep, made by macros // each expanding the last -- rescanned, so the expansion itself is // flat, and only the rewrite descends. @@ -1422,81 +1681,114 @@ namespace tut ALPreprocessor::Options o = options(); o.lazyLists = true; ALPreprocessor::Result r = ALPreprocessor::run(deep, o); - ensure("stopped at the depth: " + messages(r), r.overran && r.problems.back().key == std::string("PreprocNestsTooDeep")); + { + INFO("stopped at the depth: " << messages(r)); + CHECK(r.overran); + CHECK(r.problems.back().key == std::string("PreprocNestsTooDeep")); + } // Within it, each read is a call, the inner in the outer's place. r = ALPreprocessor::run("#define N0 0\n#define N1 (integer)l[N0]\n#define N2 (integer)l[N1]\n#define N3 (integer)l[N2]\nlist l;\ninteger x = N3;\n", o); - ensure("nested reads each a call: " + r.text, - !r.overran && r.text.find("integer x = llList2Integer(l, llList2Integer(l, llList2Integer(l, 0)));") != std::string::npos); + { + INFO("nested reads each a call: " << r.text); + CHECK_FALSE(r.overran); + REQUIRE(r.text.find("integer x = llList2Integer(l, llList2Integer(l, llList2Integer(l, 0)));") != std::string::npos); + } } - template<> template<> - void alpreprocessor_object::test<35>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua: a comment header stays above the unmapped module table", "[alpreprocessor]") { - set_test_name("SLua: a header of plain comments and hot ones stays ahead of the module table, which maps to no line of the script"); add("m", "return 1\n"); const ALPreprocessor::Result r = ALPreprocessor::run("-- @file vehicle/hovertext.luau\n--!strict\n--!nolint LocalUnused\nlocal m = require(\"m\")\n", options(true)); - ensure_equals("problems", faults(r), std::string()); - ensure_equals("the header first, whole", r.text.substr(0, r.text.find("do\n local modules")), + CHECK(faults(r) == std::string()); + UNSCOPED_INFO("the header first, whole"); + CHECK(r.text.substr(0, r.text.find("do\n local modules")) == std::string("-- @file vehicle/hovertext.luau\n--!strict\n--!nolint LocalUnused\n")); - ensure("each header line its own", r.map.toSource(1, 0).found() && r.map.toSource(1, 0).file == 0 && r.map.toSource(1, 0).line == 1); - ensure("the table's lines and the require's map nowhere", !r.map.toSource(3, 0).found() && !r.map.toSource(9, 0).found()); - ensure("nor its module's wrapping", !r.map.toSource(5, 0).found()); - ensure("the module's own line maps to it", r.map.toSource(6, 0).found() && r.map.files()[r.map.toSource(6, 0).file].name == "m"); - ensure("the script's code after it maps back", r.map.toSource(19, 0).file == 0 && r.map.toSource(19, 0).line == 3); + { + INFO("each header line its own"); + REQUIRE(r.map.toSource(1, 0).found()); + CHECK(r.map.toSource(1, 0).file == 0); + CHECK(r.map.toSource(1, 0).line == 1); + } + { + INFO("the table's lines and the require's map nowhere"); + REQUIRE_FALSE(r.map.toSource(3, 0).found()); + REQUIRE_FALSE(r.map.toSource(9, 0).found()); + } + UNSCOPED_INFO("nor its module's wrapping"); + REQUIRE_FALSE(r.map.toSource(5, 0).found()); + { + INFO("the module's own line maps to it"); + REQUIRE(r.map.toSource(6, 0).found()); + CHECK(r.map.files()[r.map.toSource(6, 0).file].name == "m"); + } + { + INFO("the script's code after it maps back"); + CHECK(r.map.toSource(19, 0).file == 0); + CHECK(r.map.toSource(19, 0).line == 3); + } } - template<> template<> - void alpreprocessor_object::test<36>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a superseded run stops and gives the source back; a wanted one runs on", "[alpreprocessor]") { - set_test_name("a run no longer wanted stops where it stands and says so, the source as it was; one still wanted is not touched"); std::atomic later{ false }; ALPreprocessor::Options wanted = options(false); wanted.superseded = &later; wanted.switches = true; const std::string source = "#define TWICE(x) ((x) * 2)\ndefault { state_entry() { integer n = TWICE(3); switch (n) { case 6: break; } } }\n"; const ALPreprocessor::Result made = ALPreprocessor::run(source, wanted); - ensure("still wanted: made", !made.superseded && !made.overran && made.text.find("((3) * 2)") != std::string::npos); + { + INFO("still wanted: made"); + CHECK_FALSE(made.superseded); + CHECK_FALSE(made.overran); + REQUIRE(made.text.find("((3) * 2)") != std::string::npos); + } later = true; const ALPreprocessor::Result stopped = ALPreprocessor::run(source, wanted); - ensure("not wanted: stopped", stopped.superseded && stopped.overran); - ensure_equals("the source as it was", stopped.text, source); - ensure("nothing said of it", std::none_of(stopped.problems.begin(), stopped.problems.end(), + { + INFO("not wanted: stopped"); + CHECK(stopped.superseded); + CHECK(stopped.overran); + } + UNSCOPED_INFO("the source as it was"); + CHECK(stopped.text == source); + UNSCOPED_INFO("nothing said of it"); + CHECK(std::none_of(stopped.problems.begin(), stopped.problems.end(), [](const ALScriptProblem& p) { return p.severity == ALScriptProblem::Severity::Error; })); } - template<> template<> - void alpreprocessor_object::test<37>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: include guards: once until #undef; no guard with #else or code after", "[alpreprocessor]") { - set_test_name("a header behind a classic include guard: expanded once however often included, listed once in the map; again once undefined; an #else or code after it no guard"); add("guarded.lsl", "// the helpers\n#ifndef GUARDED\n#define GUARDED\ninteger helper() { return 1; }\n#endif\n"); const ALPreprocessor::Result r = ALPreprocessor::run( "#include \"guarded.lsl\"\n#include \"guarded.lsl\"\n#include \"guarded.lsl\"\ndefault { state_entry() { helper(); } }\n", options()); - ensure_equals("problems", messages(r), std::string()); + CHECK(messages(r) == std::string()); size_t count = 0; for (size_t at = r.text.find("integer helper()"); at != std::string::npos; at = r.text.find("integer helper()", at + 1)) { ++count; } - ensure_equals("once", count, size_t(1)); - ensure_equals("listed once", r.map.files().size(), size_t(2)); + CHECK(count == size_t(1)); + UNSCOPED_INFO("listed once"); + REQUIRE(r.map.files().size() == size_t(2)); const ALPreprocessor::Result undone = ALPreprocessor::run( "#include \"guarded.lsl\"\n#undef GUARDED\n#include \"guarded.lsl\"\ndefault { state_entry() { } }\n", options()); - ensure("undefined: again", undone.text.find("integer helper()") != undone.text.rfind("integer helper()")); + UNSCOPED_INFO("undefined: again"); + CHECK(undone.text.find("integer helper()") != undone.text.rfind("integer helper()")); add("elsed.lsl", "#ifndef ELSED\n#define ELSED\ninteger a;\n#else\ninteger b;\n#endif\n"); const ALPreprocessor::Result elsed = ALPreprocessor::run("#include \"elsed.lsl\"\n#include \"elsed.lsl\"\n", options()); - ensure("an #else: no guard, the other branch made", elsed.text.find("integer b;") != std::string::npos); + UNSCOPED_INFO("an #else: no guard, the other branch made"); + REQUIRE(elsed.text.find("integer b;") != std::string::npos); add("after.lsl", "#ifndef AFTER\n#define AFTER\n#endif\ninteger after;\n"); const ALPreprocessor::Result after = ALPreprocessor::run("#include \"after.lsl\"\n#include \"after.lsl\"\n", options()); - ensure("code after the #endif: no guard, made twice", after.text.find("integer after;") != after.text.rfind("integer after;")); + UNSCOPED_INFO("code after the #endif: no guard, made twice"); + CHECK(after.text.find("integer after;") != after.text.rfind("integer after;")); } - template<> template<> - void alpreprocessor_object::test<38>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: macro arguments: none for no parameters; lines join; blanks kept", "[alpreprocessor]") { - set_test_name("a macro's arguments: none for one with no parameters, a line an argument runs over a space, blanks with no ( after them given back as they were"); const std::string source = "#define Z() zero\n" "#define I(x) [x]\n" "#define TWO(a, b) a+b\n" @@ -1505,14 +1797,12 @@ namespace tut "TWO(1,\n2) TWO( (1, 2) ,\t3 )\n" "I\n(3)\n"; const ALPreprocessor::Result r = ALPreprocessor::run(source, options()); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("text", squeeze(r.text), std::string("zero zero Z\n[1] I x I\n1+2 (1, 2)+3\n[3]\n")); + CHECK(messages(r) == std::string()); + CHECK(squeeze(r.text) == std::string("zero zero Z\n[1] I x I\n1+2 (1, 2)+3\n[3]\n")); } - template<> template<> - void alpreprocessor_object::test<39>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a function marked inline in any form is listed and the mark taken off", "[alpreprocessor]") { - set_test_name("a function marked inline in any of the ways a mark is written, at the top of the script, and the marks taken off with the extensions off"); - ensureGridBuiltins(); + requireGridBuiltins(); ALPreprocessor::Options o = options(); o.extensions = true; ALPreprocessor::Result r = ALPreprocessor::run("integer f(integer a) inline { return a; }\n" @@ -1520,24 +1810,35 @@ namespace tut "h() //pragma inline\n{\n}\n" "k() /* pragma inline */ { }\n", o); - ensure_equals("problems", messages(r), std::string()); - ensure("the four marked, in order", r.inlined == std::vector{ "f", "g", "h", "k" }); - ensure("the word after the parameters taken off: " + r.text, r.text.find("integer f(integer a) { return a; }") != std::string::npos); - ensure("a comment left as it is: " + r.text, r.text.find("g(string s) /*pragma inline*/ { llOwnerSay(s); }") != std::string::npos && - r.text.find("h() //pragma inline\n{") != std::string::npos); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("the four marked, in order"); + CHECK(r.inlined == std::vector{ "f", "g", "h", "k" }); + UNSCOPED_INFO("the word after the parameters taken off: " << r.text); + REQUIRE(r.text.find("integer f(integer a) { return a; }") != std::string::npos); + { + INFO("a comment left as it is: " << r.text); + REQUIRE(r.text.find("g(string s) /*pragma inline*/ { llOwnerSay(s); }") != std::string::npos); + REQUIRE(r.text.find("h() //pragma inline\n{") != std::string::npos); + } r = ALPreprocessor::run("integer inline = 1;\nf() { integer x = inline; }\ng() /* not pragma inline */ { }\ninteger v = (1) /*pragma inline*/;\n", o); - ensure("a variable named inline, another comment, a parenthesis that closes no parameters: none marked", r.inlined.empty()); - ensure_equals("and nothing taken off", r.text, std::string("integer inline = 1;\nf() { integer x = inline; }\ng() /* not pragma inline */ { }\ninteger v = (1) /*pragma inline*/;\n")); + UNSCOPED_INFO("a variable named inline, another comment, a parenthesis that closes no parameters: none marked"); + REQUIRE(r.inlined.empty()); + UNSCOPED_INFO("and nothing taken off"); + CHECK(r.text == std::string("integer inline = 1;\nf() { integer x = inline; }\ng() /* not pragma inline */ { }\ninteger v = (1) /*pragma inline*/;\n")); r = ALPreprocessor::run("default { state_entry() inline { } }\n", o); - ensure("an event is not a function to put in place", r.inlined.empty()); + UNSCOPED_INFO("an event is not a function to put in place"); + REQUIRE(r.inlined.empty()); // With the extensions off the marks come off all the same. const ALPreprocessor::Result plain = ALPreprocessor::run("inline f() { }\ninteger g(integer a) inline { return a; }\n", options()); - ensure_equals("taken off", plain.text, std::string("f() { }\ninteger g(integer a) { return a; }\n")); - ensure("and the names kept", plain.inlined == std::vector{ "f", "g" }); - ensure("the extensions not said to be used", !plain.usedExtensions); + UNSCOPED_INFO("taken off"); + CHECK(plain.text == std::string("f() { }\ninteger g(integer a) { return a; }\n")); + UNSCOPED_INFO("and the names kept"); + CHECK(plain.inlined == std::vector{ "f", "g" }); + UNSCOPED_INFO("the extensions not said to be used"); + CHECK_FALSE(plain.usedExtensions); // And the optimizer puts the marked function in place. ALPreprocessor::Options optimizing = options(); @@ -1545,15 +1846,17 @@ namespace tut const ALPreprocessor::Result put = ALPreprocessor::run("say(string s) inline\n{\n llOwnerSay(s);\n llOwnerSay(s + \"!\");\n}\n" "default\n{\n state_entry()\n {\n say(\"a\");\n say(\"b\");\n }\n}\n", optimizing); - ensure("both calls put in place: " + put.text, put.text.find("say(") == std::string::npos && put.text.find("llOwnerSay(\"a\")") != std::string::npos && - put.text.find("llOwnerSay(\"b\" + \"!\")") != std::string::npos); + { + INFO("both calls put in place: " << put.text); + REQUIRE(put.text.find("say(") == std::string::npos); + REQUIRE(put.text.find("llOwnerSay(\"a\")") != std::string::npos); + REQUIRE(put.text.find("llOwnerSay(\"b\" + \"!\")") != std::string::npos); + } } - template<> template<> - void alpreprocessor_object::test<40>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: const before a type is taken off and each name said where it is", "[alpreprocessor]") { - set_test_name("const before a type is taken off, and each name it declares said where it is, a function's as a function"); - ensureGridBuiltins(); + requireGridBuiltins(); const auto at = [](const ALPreprocessor::Result& r, size_t i, const std::string& name, S32 line, S32 column, bool function) { return i < r.consts.size() && r.consts[i].name == name && r.consts[i].line == line && r.consts[i].column == column && r.consts[i].function == function; @@ -1563,48 +1866,61 @@ namespace tut "const integer sq(const integer x) { return x * x; }\n" "default { state_entry() { const vector v = <1, 2, 3>; llOwnerSay((string)v); } }\n", options()); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("taken off", r.text, + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("taken off"); + CHECK(r.text == std::string("integer X = 5;\n" "float Y = 10.0;\n" "integer sq(integer x) { return x * x; }\n" "default { state_entry() { vector v = <1, 2, 3>; llOwnerSay((string)v); } }\n")); - ensure_equals("five", r.consts.size(), size_t(5)); - ensure("a global", at(r, 0, "X", 0, 8, false)); - ensure("the spaces up to the type gone too", at(r, 1, "Y", 1, 6, false)); - ensure("a function", at(r, 2, "sq", 2, 8, true)); - ensure("its parameter", at(r, 3, "x", 2, 19, false)); - ensure("a local", at(r, 4, "v", 3, 33, false)); - ensure("the extensions not said to be used", !r.usedExtensions); + REQUIRE(r.consts.size() == size_t(5)); + UNSCOPED_INFO("a global"); + CHECK(at(r, 0, "X", 0, 8, false)); + UNSCOPED_INFO("the spaces up to the type gone too"); + CHECK(at(r, 1, "Y", 1, 6, false)); + UNSCOPED_INFO("a function"); + CHECK(at(r, 2, "sq", 2, 8, true)); + UNSCOPED_INFO("its parameter"); + CHECK(at(r, 3, "x", 2, 19, false)); + UNSCOPED_INFO("a local"); + CHECK(at(r, 4, "v", 3, 33, false)); + UNSCOPED_INFO("the extensions not said to be used"); + CHECK_FALSE(r.usedExtensions); // A name, an assignment, a word with no type after it: no const. const std::string plain = "integer const = 1;\nf() { const = 2; integer constant = const; }\n"; r = ALPreprocessor::run(plain, options()); - ensure_equals("nothing taken off", r.text, plain); - ensure("nothing said", r.consts.empty()); + UNSCOPED_INFO("nothing taken off"); + CHECK(r.text == plain); + UNSCOPED_INFO("nothing said"); + REQUIRE(r.consts.empty()); // Made by a macro, and with the extensions on. ALPreprocessor::Options o = options(); o.extensions = true; r = ALPreprocessor::run("#define FIXED const integer\nFIXED Z = 3;\n", o); - ensure_equals("problems", messages(r), std::string()); - ensure_equals("a macro's", squeeze(r.text), std::string("integer Z = 3;\n")); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("a macro's"); + CHECK(squeeze(r.text) == std::string("integer Z = 3;\n")); const size_t z = r.text.find('Z'); const size_t newline = r.text.rfind('\n', z); const S32 line = static_cast(std::count(r.text.begin(), r.text.begin() + z, '\n')); const S32 column = static_cast(newline == std::string::npos ? z : z - newline - 1); - ensure("where the name is in the text", at(r, 0, "Z", line, column, false) && r.consts.size() == 1); + { + INFO("where the name is in the text"); + CHECK(at(r, 0, "Z", line, column, false)); + REQUIRE(r.consts.size() == 1); + } } - template<> template<> - void alpreprocessor_object::test<41>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua: directives are --# first on a line and # is always Luau's length", "[alpreprocessor]") { - set_test_name("SLua: a directive is --# and its name first on a line, # is Luau's length wherever it stands, and a macro's # is length too"); // A line that starts with # goes on an expression: never a // directive, and nothing said of it. ALPreprocessor::Result r = ALPreprocessor::run("local n =\n #items\n#define N 3\nprint(N)\n", options(true)); - ensure_equals("nothing wrong", messages(r), std::string()); - ensure_equals("# lines are Luau's", r.text, std::string("local n =\n #items\n#define N 3\nprint(N)\n")); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("# lines are Luau's"); + CHECK(r.text == std::string("local n =\n #items\n#define N 3\nprint(N)\n")); // --# with a directive's name at once, indented or not; the // conditionals, the macros, #line. @@ -1622,8 +1938,9 @@ namespace tut "--#endif\n" "--#line 40\n", options(true)); - ensure_equals("nothing wrong", messages(r), std::string()); - ensure_equals("directives read, # as length", r.text, + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("directives read, # as length"); + CHECK(r.text == std::string("\n\n\n\n\n\n\n\n\n\nprint(#xs, xs[#xs], ##xs, 2)\n\n--#line 40\n")); // Comments that only look like directives stay comments: selene's @@ -1637,48 +1954,67 @@ namespace tut "local z = 1 --#define Z 3\n" "print(X, Y, Z)\n"; r = ALPreprocessor::run(comments, options(true)); - ensure_equals("nothing wrong", messages(r), std::string()); - ensure_equals("all as written", r.text, comments); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("all as written"); + CHECK(r.text == comments); // An include, as LSL has one. add("lib.luau", "--#define GREETING \"hi\"\nlocal lib = {}\n"); r = ALPreprocessor::run("--#include \"lib.luau\"\nprint(GREETING)\n", options(true)); - ensure_equals("nothing wrong", messages(r), std::string()); - ensure("the include's text in, and its macro", r.text.find("local lib = {}") != std::string::npos && - r.text.find("print(\"hi\")") != std::string::npos); + CHECK(messages(r) == std::string()); + { + INFO("the include's text in, and its macro"); + REQUIRE(r.text.find("local lib = {}") != std::string::npos); + REQUIRE(r.text.find("print(\"hi\")") != std::string::npos); + } // Its errors are the directive's. r = ALPreprocessor::run("--#endif\n--#if\n--#endif\n", options(true)); - ensure("an #endif without #if, and an #if without an expression: " + messages(r), - messages(r).find("E 0:") != std::string::npos && messages(r).find("E 1:") != std::string::npos); + { + INFO("an #endif without #if, and an #if without an expression: " << messages(r)); + REQUIRE(messages(r).find("E 0:") != std::string::npos); + REQUIRE(messages(r).find("E 1:") != std::string::npos); + } // Read as a script's lines are, by what finds includes and names. - ensure_equals("--#include's name", ALPreprocessor::directiveName(" --#include \"x\"", true), size_t(5)); - ensure("not with a blank", ALPreprocessor::directiveName("--# include \"x\"", true) == std::string_view::npos); - ensure("not # in SLua", ALPreprocessor::directiveName("#include \"x\"", true) == std::string_view::npos); - ensure_equals("# in LSL, blanks about it", ALPreprocessor::directiveName(" # include \"x\"", false), size_t(3)); - ensure("not --# in LSL", ALPreprocessor::directiveName("--#include \"x\"", false) == std::string_view::npos); + UNSCOPED_INFO("--#include's name"); + CHECK(ALPreprocessor::directiveName(" --#include \"x\"", true) == size_t(5)); + UNSCOPED_INFO("not with a blank"); + REQUIRE(ALPreprocessor::directiveName("--# include \"x\"", true) == std::string_view::npos); + UNSCOPED_INFO("not # in SLua"); + REQUIRE(ALPreprocessor::directiveName("#include \"x\"", true) == std::string_view::npos); + UNSCOPED_INFO("# in LSL, blanks about it"); + CHECK(ALPreprocessor::directiveName(" # include \"x\"", false) == size_t(3)); + UNSCOPED_INFO("not --# in LSL"); + REQUIRE(ALPreprocessor::directiveName("--#include \"x\"", false) == std::string_view::npos); // And by the lines a verbatim reader sees: --# is a mark of its own. const std::vector tokens = ALPreprocessor::tokenize("--#define A 1\n#a\n", true); - ensure("the mark, then the directive's words", tokens.size() > 2 && tokens[0].text == "--#" && tokens[1].text == "define"); + { + INFO("the mark, then the directive's words"); + REQUIRE(tokens.size() > 2); + CHECK(tokens[0].text == "--#"); + CHECK(tokens[1].text == "define"); + } } - template<> template<> - void alpreprocessor_object::test<42>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua: a --#define continues over a backslash before any line ending", "[alpreprocessor]") { - set_test_name("SLua: a --#define goes on over a backslash at its line's end, before LF, CRLF or a lone CR, and past blanks with a warning; an include's CRLF lines read as LF"); const std::string lf = "--#define SAY(x) \\\n" " ll.OwnerSay(x) \\\n" " print(x)\n" "SAY(\"hi\")\n"; ALPreprocessor::Result r = ALPreprocessor::run(lf, options(true)); - ensure_equals("nothing wrong", messages(r), std::string()); + CHECK(messages(r) == std::string()); const std::string joined = r.text; const size_t used = joined.find("ll.OwnerSay(\"hi\")"); - ensure("the body on the line that uses it: " + joined, - used != std::string::npos && joined.find("print(\"hi\")") > used && joined.find('\n', used) > joined.find("print(\"hi\")") && - joined.find('\\') == std::string::npos); + { + INFO("the body on the line that uses it: " << joined); + REQUIRE(used != std::string::npos); + CHECK(joined.find("print(\"hi\")") > used); + CHECK(joined.find('\n', used) > joined.find("print(\"hi\")")); + REQUIRE(joined.find('\\') == std::string::npos); + } const auto ended = [](const std::string& text, const char* end) { std::string out; for (char c : text) @@ -1688,30 +2024,35 @@ namespace tut return out; }; r = ALPreprocessor::run(ended(lf, "\r\n"), options(true)); - ensure_equals("CRLF said nothing", messages(r), std::string()); - ensure_equals("CRLF", r.text, joined); + UNSCOPED_INFO("CRLF said nothing"); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("CRLF"); + CHECK(r.text == joined); r = ALPreprocessor::run(ended(lf, "\r"), options(true)); - ensure_equals("a lone CR", r.text, joined); + UNSCOPED_INFO("a lone CR"); + CHECK(r.text == joined); r = ALPreprocessor::run("--#define SAY(x) \\ \n" " ll.OwnerSay(x) \\\t\n" " print(x)\n" "SAY(\"hi\")\n", options(true)); - ensure_equals("blanks after the backslash", r.text, joined); - ensure_equals("said of each", messages(r), + UNSCOPED_INFO("blanks after the backslash"); + CHECK(r.text == joined); + UNSCOPED_INFO("said of each"); + CHECK(messages(r) == std::string("W 0: backslash and newline separated by space\nW 1: backslash and newline separated by space\n")); add("say.luau", "--#define SAY(x) \\\r\n print(x)\r\n"); r = ALPreprocessor::run("--#include \"say.luau\"\nSAY(1)\n", options(true)); - ensure_equals("an include with CRLF said nothing", messages(r), std::string()); - ensure("and its macro goes on over its lines: " + r.text, r.text.find("print(1)") != std::string::npos); + UNSCOPED_INFO("an include with CRLF said nothing"); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("and its macro goes on over its lines: " << r.text); + REQUIRE(r.text.find("print(1)") != std::string::npos); } - template<> template<> - void alpreprocessor_object::test<43>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: every golden file with CRLF or lone CR line ends comes out as expected", "[alpreprocessor]") { - set_test_name("every golden file and its includes, their lines ended in CRLF or in a lone CR as a Windows checkout or an old Mac would, come out as their expected text, saying nothing"); namespace fs = std::filesystem; const fs::path dir = fsyspath(AL_ALSCRIPT_TEST_DIR) / "preprocessor"; S32 checked = 0; @@ -1728,30 +2069,40 @@ namespace tut { const std::string said = ending[1] ? " with CRLF" : " with a lone CR"; ALPreprocessor::Result r = runGolden(entry.path(), ending); - ensure_equals("problems in " + name + said, messages(r), std::string()); - ensure_equals("output of " + name + said, r.text, want); + INFO(name << said); + REQUIRE(messages(r) == std::string()); + REQUIRE(r.text == want); } ++checked; } - ensure("some golden files", checked > 0); + UNSCOPED_INFO("some golden files"); + CHECK(checked > 0); } - template<> template<> - void alpreprocessor_object::test<44>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: usesDirectives and wanted: why a script is preprocessed", "[alpreprocessor]") { - set_test_name("a script holds a directive where a line is one, outside an LSL block comment; why one is preprocessed: enveloped, then its directives, then the setting"); typedef ALPreprocessor P; - ensure("#define", P::usesDirectives("#define X 1\ndefault { state_entry() { } }\n", false)); - ensure("one further down, indented", P::usesDirectives("integer x;\n #include \"lib.lsl\"\n", false)); - ensure("none: # only in a string", !P::usesDirectives("default { state_entry() { llSay(0, \"#define\"); } }\n", false)); - ensure("none: a line comment", !P::usesDirectives("// #define X 1\ndefault { }\n", false)); - ensure("none: in a block comment", !P::usesDirectives("/* notes\n#define X 1\n*/\ndefault { }\n", false)); - ensure("after a block comment closes", P::usesDirectives("/* notes */\n#define X 1\n", false)); - ensure("not opened by one in a string", P::usesDirectives("string s = \"/*\";\n#define X 1\n", false)); - ensure("SLua's --#", P::usesDirectives("--#define X 1\nprint(X)\n", true)); - ensure("none: Luau's length operator first on a line", !P::usesDirectives("local n =\n #items\n", true)); - ensure("none: an SLua comment", !P::usesDirectives("-- # a heading\nprint(1)\n", true)); - ensure("none: LSL's # in SLua's place", !P::usesDirectives("#define X 1\n", true)); + CHECK(P::usesDirectives("#define X 1\ndefault { state_entry() { } }\n", false)); + UNSCOPED_INFO("one further down, indented"); + CHECK(P::usesDirectives("integer x;\n #include \"lib.lsl\"\n", false)); + UNSCOPED_INFO("none: # only in a string"); + CHECK_FALSE(P::usesDirectives("default { state_entry() { llSay(0, \"#define\"); } }\n", false)); + UNSCOPED_INFO("none: a line comment"); + CHECK_FALSE(P::usesDirectives("// #define X 1\ndefault { }\n", false)); + UNSCOPED_INFO("none: in a block comment"); + CHECK_FALSE(P::usesDirectives("/* notes\n#define X 1\n*/\ndefault { }\n", false)); + UNSCOPED_INFO("after a block comment closes"); + CHECK(P::usesDirectives("/* notes */\n#define X 1\n", false)); + UNSCOPED_INFO("not opened by one in a string"); + CHECK(P::usesDirectives("string s = \"/*\";\n#define X 1\n", false)); + UNSCOPED_INFO("SLua's --#"); + CHECK(P::usesDirectives("--#define X 1\nprint(X)\n", true)); + UNSCOPED_INFO("none: Luau's length operator first on a line"); + CHECK_FALSE(P::usesDirectives("local n =\n #items\n", true)); + UNSCOPED_INFO("none: an SLua comment"); + CHECK_FALSE(P::usesDirectives("-- # a heading\nprint(1)\n", true)); + UNSCOPED_INFO("none: LSL's # in SLua's place"); + CHECK_FALSE(P::usesDirectives("#define X 1\n", true)); bool asked = false; const auto directives = [&asked](bool answer) { @@ -1760,18 +2111,26 @@ namespace tut return answer; }; }; - ensure("enveloped first, its text not read", P::wanted(true, directives(true), true) == P::Wanted::Enveloped && !asked); - ensure("then its directives, the setting on or off", - P::wanted(false, directives(true), true) == P::Wanted::Directives && P::wanted(false, directives(true), false) == P::Wanted::Directives); - ensure("then the setting", P::wanted(false, directives(false), true) == P::Wanted::Setting); - ensure("else not", P::wanted(false, directives(false), false) == P::Wanted::No); - ensure("by text", P::wanted("#define X 1\n", false, false, false) == P::Wanted::Directives); + { + INFO("enveloped first, its text not read"); + CHECK(P::wanted(true, directives(true), true) == P::Wanted::Enveloped); + CHECK_FALSE(asked); + } + { + INFO("then its directives, the setting on or off"); + CHECK(P::wanted(false, directives(true), true) == P::Wanted::Directives); + CHECK(P::wanted(false, directives(true), false) == P::Wanted::Directives); + } + UNSCOPED_INFO("then the setting"); + CHECK(P::wanted(false, directives(false), true) == P::Wanted::Setting); + UNSCOPED_INFO("else not"); + CHECK(P::wanted(false, directives(false), false) == P::Wanted::No); + UNSCOPED_INFO("by text"); + CHECK(P::wanted("#define X 1\n", false, false, false) == P::Wanted::Directives); } - template<> template<> - void alpreprocessor_object::test<45>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a require only the old search found is said with a fix each way", "[alpreprocessor]") { - set_test_name("a require found nowhere that the search before found: said where, with a fix for each way to say it now, the alias's not preferred"); ALPreprocessor::Options o = options(true); o.resolve = [](const ALPreprocessor::Ask& ask, ALPreprocessor::Include& out) { if (ask.name == "util") @@ -1782,26 +2141,45 @@ namespace tut return ALPreprocessor::Found::No; }; const ALPreprocessor::Result r = ALPreprocessor::run("local util = require('util')\nlocal other = require(\"other\")\n", o); - ensure_equals("both said", r.problems.size(), size_t(2)); + REQUIRE(r.problems.size() == size_t(2)); const ALScriptProblem& moved = r.problems[0]; - ensure_equals("where the search found it", moved.key, std::string("PreprocModuleSearched")); - ensure("its words", moved.args == std::vector({ "util", "/inc/util.luau" }) && moved.message.find("/inc/util.luau") != std::string::npos); - ensure_equals("a fix for each way", moved.fixes.size(), size_t(2)); + UNSCOPED_INFO("where the search found it"); + CHECK(moved.key == std::string("PreprocModuleSearched")); + { + INFO("its words"); + CHECK(moved.args == std::vector({ "util", "/inc/util.luau" })); + REQUIRE(moved.message.find("/inc/util.luau") != std::string::npos); + } + UNSCOPED_INFO("a fix for each way"); + REQUIRE(moved.fixes.size() == size_t(2)); const ALScriptFix& plain = moved.fixes[0]; - ensure("the require's string put in, its quote kept", plain.edits.size() == 1 && plain.edits[0].text == "'@shared/util'" && - plain.edits[0].line == 0 && plain.edits[0].column == 21 && plain.edits[0].endColumn == 27); - ensure("preferred", plain.preferred && plain.key == "ScriptFixRequireAs"); + { + INFO("the require's string put in, its quote kept"); + REQUIRE(plain.edits.size() == 1); + CHECK(plain.edits[0].text == "'@shared/util'"); + CHECK(plain.edits[0].line == 0); + CHECK(plain.edits[0].column == 21); + CHECK(plain.edits[0].endColumn == 27); + } + CHECK(plain.preferred); + CHECK(plain.key == "ScriptFixRequireAs"); const ALScriptFix& alias = moved.fixes[1]; - ensure("the alias's named in it, and not preferred", alias.key == "ScriptFixRequireAlias" && alias.args.size() == 3 && alias.args[1] == "inc" && - !alias.preferred && !alias.safe); - ensure_equals("one the search never found: as before", r.problems[1].key, std::string("PreprocModuleNotFound")); - ensure("with no fix", r.problems[1].fixes.empty()); + { + INFO("the alias's named in it, and not preferred"); + CHECK(alias.key == "ScriptFixRequireAlias"); + REQUIRE(alias.args.size() == 3); + CHECK(alias.args[1] == "inc"); + CHECK_FALSE(alias.preferred); + CHECK_FALSE(alias.safe); + } + UNSCOPED_INFO("one the search never found: as before"); + CHECK(r.problems[1].key == std::string("PreprocModuleNotFound")); + UNSCOPED_INFO("with no fix"); + REQUIRE(r.problems[1].fixes.empty()); } - template<> template<> - void alpreprocessor_object::test<46>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a require of a file beside a same-named folder init warns naming both", "[alpreprocessor]") { - set_test_name("a require of a file beside a folder's init of its name: taken, and a warning naming both"); ALPreprocessor::Options o = options(true); o.resolve = [](const ALPreprocessor::Ask& ask, ALPreprocessor::Include& out) { out.text = "return 1\n"; @@ -1811,28 +2189,41 @@ namespace tut return ALPreprocessor::Found::Yes; }; const ALPreprocessor::Result r = ALPreprocessor::run("local both = require('./both')\nprint(both)\n", o); - ensure_equals("one said", r.problems.size(), size_t(1)); + REQUIRE(r.problems.size() == size_t(1)); const ALScriptProblem& said = r.problems[0]; - ensure("a warning", said.severity == ALScriptProblem::Severity::Warning && said.key == "PreprocModuleBesideInit"); - ensure("naming both, each from the folder both are in", said.args == std::vector({ "./both", "both.luau", "both/init.luau" })); - ensure("the module taken", r.text.find("return 1") != std::string::npos); + CHECK(said.severity == ALScriptProblem::Severity::Warning); + CHECK(said.key == "PreprocModuleBesideInit"); + UNSCOPED_INFO("naming both, each from the folder both are in"); + CHECK(said.args == std::vector({ "./both", "both.luau", "both/init.luau" })); + UNSCOPED_INFO("the module taken"); + REQUIRE(r.text.find("return 1") != std::string::npos); } - template<> template<> - void alpreprocessor_object::test<47>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: a missing require says why: keyed where known else in Luau's words", "[alpreprocessor]") { - set_test_name("a require found nowhere said with why: keyed where the reason has a known shape, the module's name first, else in Luau's words"); ALPreprocessor::Options o = options(true); o.resolve = [](const ALPreprocessor::Ask& ask, ALPreprocessor::Include& out) { out.why = ask.name == "./a" ? "could not resolve child component \"a\"" : ask.name == "./b" ? "something odd" : ""; return ALPreprocessor::Found::No; }; const ALPreprocessor::Result r = ALPreprocessor::run("require('./a')\nrequire('./b')\nrequire('./c')\n", o); - ensure_equals("each said", r.problems.size(), size_t(3)); - ensure("keyed", r.problems[0].key == "PreprocRequireNoChild" && r.problems[0].args == std::vector({ "./a", "a" }) && - r.problems[0].message == "could not find module './a': could not resolve child component \"a\""); - ensure("in Luau's words", r.problems[1].key == "PreprocModuleNotFoundWhy" && r.problems[1].args == std::vector({ "./b", "something odd" })); - ensure("no reason: as before", r.problems[2].key == "PreprocModuleNotFound" && r.problems[2].args == std::vector{ "./c" }); + REQUIRE(r.problems.size() == size_t(3)); + { + INFO("keyed"); + CHECK(r.problems[0].key == "PreprocRequireNoChild"); + CHECK(r.problems[0].args == std::vector({ "./a", "a" })); + CHECK(r.problems[0].message == "could not find module './a': could not resolve child component \"a\""); + } + { + INFO("in Luau's words"); + CHECK(r.problems[1].key == "PreprocModuleNotFoundWhy"); + CHECK(r.problems[1].args == std::vector({ "./b", "something odd" })); + } + { + INFO("no reason: as before"); + CHECK(r.problems[2].key == "PreprocModuleNotFound"); + CHECK(r.problems[2].args == std::vector{ "./c" }); + } } // How often each module ran, as a module says it through `counted`, @@ -1840,10 +2231,8 @@ namespace tut // own, and what it sets is not the script's. static std::map gRuns; - template<> template<> - void alpreprocessor_object::test<48>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua's bundled require is Luau's: each module once; nil runs again", "[alpreprocessor]") { - set_test_name("SLua's require, run as a save sends it, is Luau's: a module run once, at its first require, every later one the same value -- but nil, run again; one never required not run; a module's globals its own"); add("count", "counted(\"count\")\nreturn {}\n"); add("lazy", "counted(\"lazy\")\nreturn {}\n"); add("never", "counted(\"never\")\nreturn {}\n"); @@ -1863,7 +2252,7 @@ namespace tut "shared = own.inner == a\n" "ownSees = own.sees\n", options(true)); - ensure_equals("problems", messages(r), std::string()); + CHECK(messages(r) == std::string()); // Run in SLua's VM, its globals read back. gRuns.clear(); @@ -1883,9 +2272,11 @@ namespace tut // module of the scripter's making. lua_setmemcat(L, LUA_FIRST_USER_MEMCAT); const std::string bytecode = Luau::compile(r.text); - ensure("loads: " + r.text, luau_load(L, "=bundle", bytecode.data(), bytecode.size(), 0) == 0); + UNSCOPED_INFO("loads: " << r.text); + REQUIRE(luau_load(L, "=bundle", bytecode.data(), bytecode.size(), 0) == 0); const int status = lua_pcall(L, 0, 0, 0); - ensure(std::string("runs: ") + (status != 0 && lua_isstring(L, -1) ? lua_tostring(L, -1) : "") + "\n" + r.text, status == 0); + UNSCOPED_INFO("runs: " << (status != 0 && lua_isstring(L, -1) ? lua_tostring(L, -1) : "") << "\n" << r.text); + CHECK(status == 0); const auto truth = [L](const char* name) { lua_getglobal(L, name); const bool value = lua_toboolean(L, -1); @@ -1898,20 +2289,31 @@ namespace tut lua_pop(L, 1); return value; }; - ensure_equals("required three times, run once", gRuns["count"], 1); - ensure("and the same value each time", truth("same") && truth("shared")); - ensure_equals("not run before its first require", number("lazyBefore"), 0.0); - ensure_equals("then once, however often required", gRuns["lazy"], 1); - ensure_equals("never required, never run", gRuns["never"], 0); - ensure_equals("nil kept by nothing: run again, as Luau runs it", gRuns["nothing"], 2); - ensure("a module's global its own", truth("ownSees") && !truth("leaked")); + UNSCOPED_INFO("required three times, run once"); + CHECK(gRuns["count"] == 1); + { + INFO("and the same value each time"); + CHECK(truth("same")); + CHECK(truth("shared")); + } + UNSCOPED_INFO("not run before its first require"); + CHECK(number("lazyBefore") == 0.0); + UNSCOPED_INFO("then once, however often required"); + CHECK(gRuns["lazy"] == 1); + UNSCOPED_INFO("never required, never run"); + CHECK(gRuns["never"] == 0); + UNSCOPED_INFO("nil kept by nothing: run again, as Luau runs it"); + CHECK(gRuns["nothing"] == 2); + { + INFO("a module's global its own"); + CHECK(truth("ownSees")); + CHECK_FALSE(truth("leaked")); + } lua_close(L); } - template<> template<> - void alpreprocessor_object::test<49>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: @line comments where asked agree with the map; none if nothing moved", "[alpreprocessor]") { - set_test_name("@line comments, where asked: a reader counting lines from each finds every line of the text where the map does, and where nothing moved there are none"); // Read as the plugin reads them: the line after a comment is the // line it says of the file it names, and each line after the next. const auto readsTrue = [](const ALPreprocessor::Result& r, const std::string& script, std::string& why) { @@ -1955,16 +2357,24 @@ namespace tut lua.lineComments = true; const std::string script = "--!strict\nlocal util = require(\"util\")\n\nlocal deep = require(\"deep\")\nprint(util.f(), deep.k)\n"; ALPreprocessor::Result r = ALPreprocessor::run(script, lua); - ensure_equals("problems", messages(r), std::string()); - ensure("the reader finds every line: " + why, readsTrue(r, "main.luau", why)); - ensure("the header still first: " + r.text, r.text.rfind("--!strict\ndo\n", 0) == 0); - ensure("a module's first: " + r.text, r.text.find(" [1] = function()\n-- @line 1 \"util\"\nlocal M = {}\n") != std::string::npos); - ensure("one past a directive: " + r.text, r.text.find("-- @line 2 \"deep\"\nreturn { k = 2 }\n") != std::string::npos); - ensure("the script's after the modules: " + r.text, r.text.find("end\n-- @line 2 \"main.luau\"\nlocal util = require(1)\n") != std::string::npos); - ensure_equals("three, no more", std::count(r.text.begin(), r.text.end(), '@'), 3); - ensure("none mapped anywhere", !r.map.toSource(S32(std::count(r.text.begin(), r.text.begin() + r.text.find("-- @line 2 \"main"), '\n')), 0).found()); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("the reader finds every line: " << why); + CHECK(readsTrue(r, "main.luau", why)); + UNSCOPED_INFO("the header still first: " << r.text); + CHECK(r.text.rfind("--!strict\ndo\n", 0) == 0); + UNSCOPED_INFO("a module's first: " << r.text); + REQUIRE(r.text.find(" [1] = function()\n-- @line 1 \"util\"\nlocal M = {}\n") != std::string::npos); + UNSCOPED_INFO("one past a directive: " << r.text); + REQUIRE(r.text.find("-- @line 2 \"deep\"\nreturn { k = 2 }\n") != std::string::npos); + UNSCOPED_INFO("the script's after the modules: " << r.text); + REQUIRE(r.text.find("end\n-- @line 2 \"main.luau\"\nlocal util = require(1)\n") != std::string::npos); + UNSCOPED_INFO("three, no more"); + CHECK(std::count(r.text.begin(), r.text.end(), '@') == 3); + UNSCOPED_INFO("none mapped anywhere"); + REQUIRE_FALSE(r.map.toSource(S32(std::count(r.text.begin(), r.text.begin() + r.text.find("-- @line 2 \"main"), '\n')), 0).found()); const ALPreprocessor::Result plain = ALPreprocessor::run(script, options(true)); - ensure("off: none", plain.text.find("@line") == std::string::npos); + UNSCOPED_INFO("off: none"); + REQUIRE(plain.text.find("@line") == std::string::npos); // LSL: an include's first line, the script's after it, a stretch a // condition left out, a macro over lines. @@ -1972,19 +2382,20 @@ namespace tut ALPreprocessor::Options lsl = options(); lsl.lineComments = true; r = ALPreprocessor::run("integer a;\n#include \"inc.lsl\"\n#if 0\ninteger gone;\n\n#endif\n#define TWICE(v) \\\n ((v) * 2)\ninteger b = TWICE(3);\ndefault { state_entry() { } }\n", lsl); - ensure_equals("problems", messages(r), std::string()); - ensure("the reader finds every line: " + why, readsTrue(r, "main.lsl", why)); - ensure("in LSL's comment: " + r.text, r.text.find("// @line 1 \"inc.lsl\"\ninteger x;\n") != std::string::npos); + CHECK(messages(r) == std::string()); + UNSCOPED_INFO("the reader finds every line: " << why); + CHECK(readsTrue(r, "main.lsl", why)); + UNSCOPED_INFO("in LSL's comment: " << r.text); + REQUIRE(r.text.find("// @line 1 \"inc.lsl\"\ninteger x;\n") != std::string::npos); // Nothing moved: none at all. r = ALPreprocessor::run("integer a;\n\ndefault { state_entry() { } }\n", lsl); - ensure("nothing to say: " + r.text, r.text.find("@line") == std::string::npos); + UNSCOPED_INFO("nothing to say: " << r.text); + REQUIRE(r.text.find("@line") == std::string::npos); } - template<> template<> - void alpreprocessor_object::test<50>() + TEST_CASE_METHOD(alpreprocessor_data, "alpreprocessor: SLua: an unread require is noted with its weight and kept bundled", "[alpreprocessor]") { - set_test_name("SLua: a module required only where nothing reads it is said at each such require, with what it weighs; kept in the bundle, since a require runs it"); add("util", "local M = {}\nfunction M.twice(n) return n * 2 end\nreturn M\n"); add("used", "return 1\n"); const auto notes = [](const ALPreprocessor::Result& r) { @@ -2001,18 +2412,35 @@ namespace tut ALPreprocessor::Result r = ALPreprocessor::run("local util = require(\"util\")\nlocal used = require(\"used\")\nprint(used)\n", options(true)); std::vector said = notes(r); - ensure_equals("one note: " + messages(r), said.size(), size_t(1)); - ensure("a note, at the require", said[0]->severity == ALScriptProblem::Severity::Note && said[0]->line == 0 && said[0]->column == 13); - ensure("naming the local and the module", said[0]->args.size() == 3 && said[0]->args[0] == "util" && said[0]->args[1] == "util"); - ensure("with its weight: " + said[0]->args[2], std::atoi(said[0]->args[2].c_str()) > 0); - ensure("still in the bundle", r.text.find("M.twice") != std::string::npos); - - ensure("a name begun with _ is meant", notes(ALPreprocessor::run("local _util = require(\"util\")\n", options(true))).empty()); - ensure("read through another require of it: removing this one saves nothing", - notes(ALPreprocessor::run("local a = require(\"util\")\nlocal b = require(\"util\")\nprint(b)\n", options(true))).empty()); - ensure("read in an interpolated string", notes(ALPreprocessor::run("local util = require(\"util\")\nprint(`{util}`)\n", options(true))).empty()); - ensure("the require not the whole of the value", notes(ALPreprocessor::run("local util = require(\"util\") or {}\nprint(1)\n", options(true))).empty()); - ensure("a require as a statement, for what it does", notes(ALPreprocessor::run("require(\"util\")\n", options(true))).empty()); + UNSCOPED_INFO("one note: " << messages(r)); + REQUIRE(said.size() == size_t(1)); + { + INFO("a note, at the require"); + REQUIRE(said[0]->severity == ALScriptProblem::Severity::Note); + REQUIRE(said[0]->line == 0); + CHECK(said[0]->column == 13); + } + { + INFO("naming the local and the module"); + REQUIRE(said[0]->args.size() == 3); + REQUIRE(said[0]->args[0] == "util"); + CHECK(said[0]->args[1] == "util"); + } + UNSCOPED_INFO("with its weight: " << said[0]->args[2]); + CHECK(std::atoi(said[0]->args[2].c_str()) > 0); + UNSCOPED_INFO("still in the bundle"); + REQUIRE(r.text.find("M.twice") != std::string::npos); + + UNSCOPED_INFO("a name begun with _ is meant"); + REQUIRE(notes(ALPreprocessor::run("local _util = require(\"util\")\n", options(true))).empty()); + UNSCOPED_INFO("read through another require of it: removing this one saves nothing"); + REQUIRE(notes(ALPreprocessor::run("local a = require(\"util\")\nlocal b = require(\"util\")\nprint(b)\n", options(true))).empty()); + UNSCOPED_INFO("read in an interpolated string"); + REQUIRE(notes(ALPreprocessor::run("local util = require(\"util\")\nprint(`{util}`)\n", options(true))).empty()); + UNSCOPED_INFO("the require not the whole of the value"); + REQUIRE(notes(ALPreprocessor::run("local util = require(\"util\") or {}\nprint(1)\n", options(true))).empty()); + UNSCOPED_INFO("a require as a statement, for what it does"); + REQUIRE(notes(ALPreprocessor::run("require(\"util\")\n", options(true))).empty()); // In a module: said in its file. ALPreprocessor::Include wrap; @@ -2022,7 +2450,13 @@ namespace tut files["wrap"] = wrap; r = ALPreprocessor::run("local w = require(\"wrap\")\nprint(w)\n", options(true)); said = notes(r); - ensure_equals("one, in the module: " + messages(r), said.size(), size_t(1)); - ensure("in its file, at its require", said[0]->file == "disk:/lib/wrap.luau" && said[0]->line == 0 && said[0]->args[0] == "inner"); + UNSCOPED_INFO("one, in the module: " << messages(r)); + REQUIRE(said.size() == size_t(1)); + { + INFO("in its file, at its require"); + REQUIRE(said[0]->file == "disk:/lib/wrap.luau"); + REQUIRE(said[0]->line == 0); + CHECK(said[0]->args[0] == "inner"); + } } } diff --git a/indra/alscript/tests/alpreprocessorc11_test.cpp b/indra/alscript/tests/alpreprocessorc11_test.cpp index 8fd4b1c100..b499894890 100644 --- a/indra/alscript/tests/alpreprocessorc11_test.cpp +++ b/indra/alscript/tests/alpreprocessorc11_test.cpp @@ -27,7 +27,7 @@ #include "../preprocessor/alpreprocessor.h" -#include "../test/lltut.h" +#include #include #include @@ -40,7 +40,7 @@ // What comes out is compared as tokens, blanks between them made one space, // since how much space stands between two tokens is the implementation's // (6.10.3.2 aside, which the stringizing tests compare exactly). -namespace tut +namespace { struct alpreprocessorc11_data { @@ -152,80 +152,83 @@ namespace tut } }; - typedef test_group alpreprocessorc11_group; - typedef alpreprocessorc11_group::object alpreprocessorc11_object; - alpreprocessorc11_group alpreprocessorc11_instance("alpreprocessorc11"); - - template<> template<> - void alpreprocessorc11_object::test<1>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 5.1.1.2: backslash-newline joins lines first even in names and comments", "[alpreprocessorc11]") { - set_test_name("5.1.1.2: a backslash before a newline joins the lines first, inside a directive's name, a name, a string, a // comment"); - ensure_equals("a directive's name", pp(std::string("#def\\\n" + UNSCOPED_INFO("a directive's name"); + CHECK(pp(std::string("#def\\\n" "ine SPLICED 1\n" - "SPLICED\n")), + "SPLICED\n")) == std::string("1")); - ensure_equals("a name", pp(std::string("#define SPLICED 1\n" + UNSCOPED_INFO("a name"); + CHECK(pp(std::string("#define SPLICED 1\n" "SPLI\\\n" - "CED\n")), + "CED\n")) == std::string("1")); - ensure_equals("but in a string it is Firestorm's newline, which scripts rely on, the line after it", pp(std::string("string s = \"ab\\\n" - "cd\";\n")), + UNSCOPED_INFO("but in a string it is Firestorm's newline, which scripts rely on, the line after it"); + CHECK(pp(std::string("string s = \"ab\\\n" + "cd\";\n")) == std::string("string s = \"ab\\ncd\" ;")); { const ALPreprocessor::Result r = run("// a note \\\nint hidden;\nint shown;\n"); - ensure("a // comment goes on over the join: " + r.text, r.text.find("\nint hidden") == std::string::npos && - r.text.find("int shown;") != std::string::npos); + INFO("a // comment goes on over the join: " + r.text); + CHECK(r.text.find("\nint hidden") == std::string::npos); + CHECK(r.text.find("int shown;") != std::string::npos); } - ensure_equals("the lines after keep their numbers", pp(std::string("#define A 1 \\\n" + UNSCOPED_INFO("the lines after keep their numbers"); + CHECK(pp(std::string("#define A 1 \\\n" " + 2\n" - "A __LINE__\n")), + "A __LINE__\n")) == std::string("1 + 2 3")); } - template<> template<> - void alpreprocessorc11_object::test<2>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 5.1.1.2: a comment is one space; in a directive it may run over lines", "[alpreprocessorc11]") { - set_test_name("5.1.1.2: a comment is one space: between tokens, in a macro's body, and over the lines of a directive"); - ensure_equals("between tokens", pp(std::string("a/**/b\n")), + UNSCOPED_INFO("between tokens"); + CHECK(pp(std::string("a/**/b\n")) == std::string("a b")); - ensure_equals("in a body", pp(std::string("#define f(x) x/**/x\n" - "f(1)\n")), + UNSCOPED_INFO("in a body"); + CHECK(pp(std::string("#define f(x) x/**/x\n" + "f(1)\n")) == std::string("1 1")); - ensure_equals("a directive goes on past a comment over lines", pp(std::string("#define LONG 1 /* a\n" + UNSCOPED_INFO("a directive goes on past a comment over lines"); + CHECK(pp(std::string("#define LONG 1 /* a\n" "b */ + 2\n" - "LONG\n")), + "LONG\n")) == std::string("1 + 2")); - ensure_equals("comments at a body's ends are none of it", pp(std::string("#define E /* a */ 1 /* b */\n" - "[E]\n")), + UNSCOPED_INFO("comments at a body's ends are none of it"); + CHECK(pp(std::string("#define E /* a */ 1 /* b */\n" + "[E]\n")) == std::string("[1]")); } - template<> template<> - void alpreprocessorc11_object::test<3>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10: blanks around #; the null directive; # not first on its line is none", "[alpreprocessorc11]") { - set_test_name("6.10: blanks and comments may stand before #, blanks after it; the null directive; # not first on a line is no directive"); - ensure_equals("before and after", pp(std::string(" /* c */ # define X 1\n" - "X\n")), + UNSCOPED_INFO("before and after"); + CHECK(pp(std::string(" /* c */ # define X 1\n" + "X\n")) == std::string("1")); - ensure_equals("the null directive", pp(std::string("#\n" + UNSCOPED_INFO("the null directive"); + CHECK(pp(std::string("#\n" "# /* nothing */\n" - "ok\n")), + "ok\n")) == std::string("ok")); - ensure_equals("# after a token", pp(std::string("x # define Y 2\n" - "Y\n")), + UNSCOPED_INFO("# after a token"); + CHECK(pp(std::string("x # define Y 2\n" + "Y\n")) == std::string("x # define Y 2 Y")); - ensure_equals("a # a macro made is no directive (6.10.3.4)", pp(std::string("#define EMPTY\n" + UNSCOPED_INFO("a # a macro made is no directive (6.10.3.4)"); + CHECK(pp(std::string("#define EMPTY\n" "EMPTY # define W 3\n" - "W\n")), + "W\n")) == std::string("# define W 3 W")); - ensure_equals("a directive's name is as written, case and all", saidOn(run(std::string("#DEFINE Z 1\n"))), std::string("0")); + UNSCOPED_INFO("a directive's name is as written, case and all"); + CHECK(saidOn(run(std::string("#DEFINE Z 1\n"))) == std::string("0")); } - template<> template<> - void alpreprocessorc11_object::test<4>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: in a skipped group only the directives' names are read", "[alpreprocessorc11]") { - set_test_name("6.10.1: in a skipped group only the directives' names are read: any other line, an unknown directive, an unfinished one"); - ensure_equals("nothing said", pp(std::string("#if 0\n" + UNSCOPED_INFO("nothing said"); + CHECK(pp(std::string("#if 0\n" "#foo bar\n" "#include template<> - void alpreprocessorc11_object::test<5>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: defined in both forms; names left over are 0; every #if operator", "[alpreprocessorc11]") { - set_test_name("6.10.1: defined in both forms, names left over are 0, and each operator an #if may have"); - ensure_equals("defined", pp(std::string("#define A\n" + UNSCOPED_INFO("defined"); + CHECK(pp(std::string("#define A\n" "#if defined A && defined(A) && !defined B && defined ( A )\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("a name left over is 0, TRUE too", pp(std::string("#if false || TRUE || sizeof || other\n" + UNSCOPED_INFO("a name left over is 0, TRUE too"); + CHECK(pp(std::string("#if false || TRUE || sizeof || other\n" "no\n" "#else\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("but true is 1, as in C++ and Firestorm's Wave", pp(std::string("#if true && !false\n" + UNSCOPED_INFO("but true is 1, as in C++ and Firestorm's Wave"); + CHECK(pp(std::string("#if true && !false\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("relations", pp(std::string("#if 1 < 2 < 3 && (1 > 2) == 0 && 2 >= 2 && (3 <= 2) == 0 && 1 != 2\n" + UNSCOPED_INFO("relations"); + CHECK(pp(std::string("#if 1 < 2 < 3 && (1 > 2) == 0 && 2 >= 2 && (3 <= 2) == 0 && 1 != 2\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("bits and unary", pp(std::string("#if (7 & 3) == 3 && (4 | 1) == 5 && (6 ^ 3) == 5 && ~0 == -1 && -(-3) == 3 && +4 == 4 && !0 == 1 && !7 == 0\n" + UNSCOPED_INFO("bits and unary"); + CHECK(pp(std::string("#if (7 & 3) == 3 && (4 | 1) == 5 && (6 ^ 3) == 5 && ~0 == -1 && -(-3) == 3 && +4 == 4 && !0 == 1 && !7 == 0\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("arithmetic", pp(std::string("#if 17 % 5 == 2 && -7 / 2 == -3 && -7 % 2 == -1 && 1 << 62 > 0 && (1 << 3) >> 1 == 4 && 2 * 3 + 4 == 10 && 2 + 3 * 4 == 14\n" + UNSCOPED_INFO("arithmetic"); + CHECK(pp(std::string("#if 17 % 5 == 2 && -7 / 2 == -3 && -7 % 2 == -1 && 1 << 62 > 0 && (1 << 3) >> 1 == 4 && 2 * 3 + 4 == 10 && 2 + 3 * 4 == 14\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("integers every way C writes them", pp(std::string("#if 017 == 15 && 0XfF == 255 && 10U == 10 && 10L == 10 && 10ull == 10 && 10uLL == 10 && 0x7FFFFFFFFFFFFFFF > 0\n" + UNSCOPED_INFO("integers every way C writes them"); + CHECK(pp(std::string("#if 017 == 15 && 0XfF == 255 && 10U == 10 && 10L == 10 && 10ull == 10 && 10uLL == 10 && 0x7FFFFFFFFFFFFFFF > 0\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("?: and what it does not work out", pp(std::string("#if (1 ? 2 : 3) == 2 && (0 ? 2 : 3) == 3 && (1 ? 0 : 1 / 0) == 0\n" + UNSCOPED_INFO("?: and what it does not work out"); + CHECK(pp(std::string("#if (1 ? 2 : 3) == 2 && (0 ? 2 : 3) == 3 && (1 ? 0 : 1 / 0) == 0\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("&& and || do not work out their right side where the left decides", pp(std::string("#if 0 && 1 / 0\n" + UNSCOPED_INFO("&& and || do not work out their right side where the left decides"); + CHECK(pp(std::string("#if 0 && 1 / 0\n" "#endif\n" "#if 1 || 1 / 0\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("macros in an #if, function-like too", pp(std::string("#define F(x) ((x) + 1)\n" + UNSCOPED_INFO("macros in an #if, function-like too"); + CHECK(pp(std::string("#define F(x) ((x) + 1)\n" "#define TWO 2\n" "#if F(1) == TWO && F(F(1)) == 3\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); } - template<> template<> - void alpreprocessorc11_object::test<6>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: #if works in intmax_t and uintmax_t; one unsigned operand makes both", "[alpreprocessorc11]") { - set_test_name("6.10.1: an #if works in intmax_t and uintmax_t, and an unsigned operand makes the other unsigned"); - ensure_equals("-1 < 0u is false", pp(std::string("#if -1 < 0u\n" + UNSCOPED_INFO("-1 < 0u is false"); + CHECK(pp(std::string("#if -1 < 0u\n" "no\n" "#else\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("unsigned wraps", pp(std::string("#if 0u - 1 == 18446744073709551615u\n" + UNSCOPED_INFO("unsigned wraps"); + CHECK(pp(std::string("#if 0u - 1 == 18446744073709551615u\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("a decimal too big for intmax_t is unsigned", pp(std::string("#if 18446744073709551615 == -1\n" + UNSCOPED_INFO("a decimal too big for intmax_t is unsigned"); + CHECK(pp(std::string("#if 18446744073709551615 == -1\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("a hex one too", pp(std::string("#if 0xFFFFFFFFFFFFFFFF > 0 && 0xFFFFFFFFFFFFFFFF == -1\n" + UNSCOPED_INFO("a hex one too"); + CHECK(pp(std::string("#if 0xFFFFFFFFFFFFFFFF > 0 && 0xFFFFFFFFFFFFFFFF == -1\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("the least intmax_t", pp(std::string("#if -9223372036854775807 - 1 < 0\n" + UNSCOPED_INFO("the least intmax_t"); + CHECK(pp(std::string("#if -9223372036854775807 - 1 < 0\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("?:'s result is unsigned where either side is", pp(std::string("#if (1 ? -1 : 0u) > 0\n" + UNSCOPED_INFO("?:'s result is unsigned where either side is"); + CHECK(pp(std::string("#if (1 ? -1 : 0u) > 0\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); } - template<> template<> - void alpreprocessorc11_object::test<7>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: an #if that is no integer constant expression is said on its line", "[alpreprocessorc11]") { - set_test_name("6.10.1: an #if refuses what is no integer constant expression, each said on its line"); - ensure_equals("too big, a float, a string, a comma, an assignment, % by zero, a call of no macro, nothing", saidOn(run(std::string("#if 18446744073709551616\n" + UNSCOPED_INFO("too big, a float, a string, a comma, an assignment, % by zero, a call of no macro, nothing"); + CHECK(saidOn(run(std::string("#if 18446744073709551616\n" "#endif\n" "#if 1.0\n" "#endif\n" @@ -357,259 +373,290 @@ namespace tut "#if G(1)\n" "#endif\n" "#if\n" - "#endif\n"))), std::string("0 2 4 6 8 10 12 14")); + "#endif\n"))) == std::string("0 2 4 6 8 10 12 14")); } - template<> template<> - void alpreprocessorc11_object::test<8>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: #if takes character constants with the values C gives them", "[alpreprocessorc11]") { - set_test_name("6.10.1: #if takes character constants, as C gives their values"); - ensure_equals("letters, escapes, octal and hex", pp(std::string("#if 'A' == 65 && '\\n' == 10 && '\\x41' == 65 && '\\0' == 0 && '\\'' == 39 && '\\\\' == 92 && '0' == 48 && '\\101' == 65\n" + UNSCOPED_INFO("letters, escapes, octal and hex"); + CHECK(pp(std::string("#if 'A' == 65 && '\\n' == 10 && '\\x41' == 65 && '\\0' == 0 && '\\'' == 39 && '\\\\' == 92 && '0' == 48 && '\\101' == 65\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); } - template<> template<> - void alpreprocessorc11_object::test<9>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: extra tokens after #ifdef and #ifndef and #else and #endif are said", "[alpreprocessorc11]") { - set_test_name("6.10.1: #ifdef, #ifndef, #else and #endif take nothing more, and something more is said"); - ensure_equals("extra tokens", saidOn(run(std::string("#ifdef A B\n" + UNSCOPED_INFO("extra tokens"); + CHECK(saidOn(run(std::string("#ifdef A B\n" "#endif\n" "#ifndef A B\n" "#endif\n" "#if 1\n" "#else junk\n" "#endif junk\n" - "#undef A B\n"))), std::string("W0 W2 W5 W6 W7")); - ensure_equals("#ifdef needs a name", saidOn(run(std::string("#ifdef\n" + "#undef A B\n"))) == std::string("W0 W2 W5 W6 W7")); + UNSCOPED_INFO("#ifdef needs a name"); + CHECK(saidOn(run(std::string("#ifdef\n" "#endif\n" "#ifdef 1\n" - "#endif\n"))), std::string("0 2")); + "#endif\n"))) == std::string("0 2")); } - template<> template<> - void alpreprocessorc11_object::test<10>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.2: an #include's name may come of macros in quotes or angle brackets", "[alpreprocessorc11]") { - set_test_name("6.10.2: an #include's name may come of macros, in quotes or in angle brackets"); files["lib.lsl"] = "LIB\n"; files["vers2.h"] = "VERS\n"; - ensure_equals("angle brackets", pp(std::string("#define HDR \n" - "#include HDR\n")), + UNSCOPED_INFO("angle brackets"); + CHECK(pp(std::string("#define HDR \n" + "#include HDR\n")) == std::string("LIB")); - ensure("asked in brackets", !asked.empty() && asked.back().name == "lib.lsl" && asked.back().angled); - ensure_equals("quotes", pp(std::string("#define Q \"lib.lsl\"\n" - "#include Q\n")), + { + INFO("asked in brackets"); + REQUIRE_FALSE(asked.empty()); + CHECK(asked.back().name == "lib.lsl"); + CHECK(asked.back().angled); + } + UNSCOPED_INFO("quotes"); + CHECK(pp(std::string("#define Q \"lib.lsl\"\n" + "#include Q\n")) == std::string("LIB")); - ensure("asked in quotes", asked.back().name == "lib.lsl" && !asked.back().angled); - ensure_equals("made by stringizing", pp(std::string("#define STR(x) #x\n" + { + INFO("asked in quotes"); + CHECK(asked.back().name == "lib.lsl"); + CHECK_FALSE(asked.back().angled); + } + UNSCOPED_INFO("made by stringizing"); + CHECK(pp(std::string("#define STR(x) #x\n" "#define XSTR(x) STR(x)\n" "#define INCFILE(n) vers ## n\n" - "#include XSTR(INCFILE(2).h)\n")), + "#include XSTR(INCFILE(2).h)\n")) == std::string("VERS")); - ensure_equals("something after the name", saidOn(run(std::string("#include \"lib.lsl\" extra\n"))), std::string("W0")); - ensure_equals("no name at all", saidOn(run(std::string("#include\n" - "#include 42\n"))), std::string("0 1")); + UNSCOPED_INFO("something after the name"); + CHECK(saidOn(run(std::string("#include \"lib.lsl\" extra\n"))) == std::string("W0")); + UNSCOPED_INFO("no name at all"); + CHECK(saidOn(run(std::string("#include\n" + "#include 42\n"))) == std::string("0 1")); } - template<> template<> - void alpreprocessorc11_object::test<11>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3 EXAMPLE 6: a redefinition the same but for blanks is none; any other is said", "[alpreprocessorc11]") { - set_test_name("6.10.3 EXAMPLE 6: a redefinition the same but for blanks is none; any other is said"); - ensure_equals("valid ones say nothing", saidOn(run(std::string("#define OBJ_LIKE (1-1)\n" + UNSCOPED_INFO("valid ones say nothing"); + CHECK(saidOn(run(std::string("#define OBJ_LIKE (1-1)\n" "#define OBJ_LIKE /* white space */ (1-1) /* other */\n" "#define FUNC_LIKE(a) ( a )\n" "#define FUNC_LIKE( a )( /* note the white space */ \\\n" " a /* other stuff on this line\n" - " */ )\n"))), std::string("")); - ensure_equals("invalid ones are each said", saidOn(run(std::string("#define OBJ_LIKE (1-1)\n" + " */ )\n"))) == std::string("")); + UNSCOPED_INFO("invalid ones are each said"); + CHECK(saidOn(run(std::string("#define OBJ_LIKE (1-1)\n" "#define FUNC_LIKE(a) ( a )\n" "#define OBJ_LIKE (0)\n" "#define OBJ_LIKE (1 - 1)\n" "#define FUNC_LIKE(b) ( a )\n" - "#define FUNC_LIKE(b) ( b )\n"))), std::string("W2 W3 W4 W5")); + "#define FUNC_LIKE(b) ( b )\n"))) == std::string("W2 W3 W4 W5")); } - template<> template<> - void alpreprocessorc11_object::test<12>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3: a ( right after the name makes it function-like; uncalled it is a name", "[alpreprocessorc11]") { - set_test_name("6.10.3: object-like or function-like by a ( straight after the name; a function-like name without ( is a name"); - ensure_equals("a space before ( makes it object-like", pp(std::string("#define f (x)\n" - "f\n")), + UNSCOPED_INFO("a space before ( makes it object-like"); + CHECK(pp(std::string("#define f (x)\n" + "f\n")) == std::string("(x)")); - ensure_equals("a body straight after the name", pp(std::string("#define X+1\n" - "[X]\n")), + UNSCOPED_INFO("a body straight after the name"); + CHECK(pp(std::string("#define X+1\n" + "[X]\n")) == std::string("[+1]")); - ensure_equals("a name alone, at the end or before something else", pp(std::string("#define f(x) \n" - "f; f\n")), + UNSCOPED_INFO("a name alone, at the end or before something else"); + CHECK(pp(std::string("#define f(x) \n" + "f; f\n")) == std::string("f; f")); - ensure_equals("the ( may be on the next line", pp(std::string("#define f(x) \n" + UNSCOPED_INFO("the ( may be on the next line"); + CHECK(pp(std::string("#define f(x) \n" "f\n" - "(1)\n")), + "(1)\n")) == std::string("<1>")); - ensure_equals("a ( that a macro makes is not the invocation's", pp(std::string("#define f(x) \n" + UNSCOPED_INFO("a ( that a macro makes is not the invocation's"); + CHECK(pp(std::string("#define f(x) \n" "#define LP (\n" - "f LP 1)\n")), + "f LP 1)\n")) == std::string("f ( 1)")); } - template<> template<> - void alpreprocessorc11_object::test<13>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3: arguments part at commas not in parentheses; empty counts; arity said", "[alpreprocessorc11]") { - set_test_name("6.10.3: arguments: commas inside parentheses stay, inside brackets and braces they part; an empty argument is one; too many or few are said"); - ensure_equals("parentheses", pp(std::string("#define N(a) \n" - "N((1, 2))\n")), + UNSCOPED_INFO("parentheses"); + CHECK(pp(std::string("#define N(a) \n" + "N((1, 2))\n")) == std::string("<(1, 2)>")); - ensure_equals("brackets and braces", pp(std::string("#define M(a, b) \n" - "M([1, 2]) M({1, 2})\n")), + UNSCOPED_INFO("brackets and braces"); + CHECK(pp(std::string("#define M(a, b) \n" + "M([1, 2]) M({1, 2})\n")) == std::string("<[1|2]> <{1|2}>")); - ensure_equals("empty", pp(std::string("#define N(a) \n" + UNSCOPED_INFO("empty"); + CHECK(pp(std::string("#define N(a) \n" "#define Z() 0\n" - "N() Z() Z( )\n")), + "N() Z() Z( )\n")) == std::string("<> 0 0")); - ensure_equals("over lines", pp(std::string("#define M(a, b) \n" + UNSCOPED_INFO("over lines"); + CHECK(pp(std::string("#define M(a, b) \n" "M(1,\n" "2\n" - ")\n")), + ")\n")) == std::string("<1|2>")); - ensure_equals("arity", saidOn(run(std::string("#define Z() 0\n" + UNSCOPED_INFO("arity"); + CHECK(saidOn(run(std::string("#define Z() 0\n" "#define M(a, b) \n" "Z(1)\n" "M(1)\n" - "M(1, 2, 3)\n"))), std::string("2 3 4")); + "M(1, 2, 3)\n"))) == std::string("2 3 4")); } - template<> template<> - void alpreprocessorc11_object::test<14>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3: __VA_ARGS__ is a variadic macro's alone; it and defined are no macros", "[alpreprocessorc11]") { - set_test_name("6.10.3: __VA_ARGS__ is a variadic macro's alone, and neither it nor defined may be a macro"); - ensure_equals("variadic", pp(std::string("#define V(...) [__VA_ARGS__]\n" - "V() V(a) V(a, b) V((a, b), c)\n")), + UNSCOPED_INFO("variadic"); + CHECK(pp(std::string("#define V(...) [__VA_ARGS__]\n" + "V() V(a) V(a, b) V((a, b), c)\n")) == std::string("[] [a] [a, b] [(a, b), c]")); - ensure_equals("after named parameters", pp(std::string("#define W(a, ...) [a|__VA_ARGS__]\n" - "W(1,) W(1, 2, 3)\n")), + UNSCOPED_INFO("after named parameters"); + CHECK(pp(std::string("#define W(a, ...) [a|__VA_ARGS__]\n" + "W(1,) W(1, 2, 3)\n")) == std::string("[1|] [1|2, 3]")); - ensure_equals("refused", saidOn(run(std::string("#define A(x) __VA_ARGS__\n" + UNSCOPED_INFO("refused"); + CHECK(saidOn(run(std::string("#define A(x) __VA_ARGS__\n" "#define __VA_ARGS__ 1\n" "#define defined 1\n" "#undef defined\n" - "#define B(__VA_ARGS__) 1\n"))), std::string("0 1 2 3 4")); + "#define B(__VA_ARGS__) 1\n"))) == std::string("0 1 2 3 4")); } - template<> template<> - void alpreprocessorc11_object::test<15>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.1: an argument is expanded first on its own unless # or ## is beside it", "[alpreprocessorc11]") { - set_test_name("6.10.3.1: an argument is expanded first, on its own, unless # or ## is beside it"); - ensure_equals("beside # and ##, not", pp(std::string("#define STR(x) #x\n" + UNSCOPED_INFO("beside # and ##, not"); + CHECK(pp(std::string("#define STR(x) #x\n" "#define XSTR(x) STR(x)\n" "#define CAT(a, b) a ## b\n" "#define XCAT(a, b) CAT(a, b)\n" "#define FOUR 4\n" - "STR(FOUR) XSTR(FOUR) CAT(FOUR, x) XCAT(FOUR, x)\n")), + "STR(FOUR) XSTR(FOUR) CAT(FOUR, x) XCAT(FOUR, x)\n")) == std::string("\"FOUR\" \"4\" FOURx 4x")); - ensure_equals("on its own: a name at an argument's end takes nothing after the invocation", pp(std::string("#define id(x) x\n" + UNSCOPED_INFO("on its own: a name at an argument's end takes nothing after the invocation"); + CHECK(pp(std::string("#define id(x) x\n" "#define h() H\n" - "id(h)()\n")), + "id(h)()\n")) == std::string("H")); - ensure_equals("a comma an argument makes parts the arguments of the macro it goes on to", pp(std::string("#define COMMA ,\n" + UNSCOPED_INFO("a comma an argument makes parts the arguments of the macro it goes on to"); + CHECK(pp(std::string("#define COMMA ,\n" "#define f(a, b) [a|b]\n" "#define g(x) f(x)\n" - "g(1 COMMA 2)\n")), + "g(1 COMMA 2)\n")) == std::string("[1|2]")); } - template<> template<> - void alpreprocessorc11_object::test<16>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.2: # makes one string with blanks made one and literals escaped", "[alpreprocessorc11]") { - set_test_name("6.10.3.2: # makes one string: blanks at the ends dropped, those inside one space, a literal's quotes and backslashes escaped"); - ensure_equals("blanks", pp(std::string("#define STR(x) #x\n" - "STR( a + b )\n")), + UNSCOPED_INFO("blanks"); + CHECK(pp(std::string("#define STR(x) #x\n" + "STR( a + b )\n")) == std::string("\"a + b\"")); - ensure_equals("a comment is a blank", pp(std::string("#define STR(x) #x\n" - "STR(a/**/b)\n")), + UNSCOPED_INFO("a comment is a blank"); + CHECK(pp(std::string("#define STR(x) #x\n" + "STR(a/**/b)\n")) == std::string("\"a b\"")); - ensure_equals("a newline is a blank", pp(std::string("#define STR(x) #x\n" + UNSCOPED_INFO("a newline is a blank"); + CHECK(pp(std::string("#define STR(x) #x\n" "STR( a\n" - " b )\n")), + " b )\n")) == std::string("\"a b\"")); - ensure_equals("no blank where none was", pp(std::string("#define STR(x) #x\n" - "STR(a+b)\n")), + UNSCOPED_INFO("no blank where none was"); + CHECK(pp(std::string("#define STR(x) #x\n" + "STR(a+b)\n")) == std::string("\"a+b\"")); - ensure_equals("a string's quotes and backslashes", pp(std::string("#define STR(x) #x\n" - "STR(\"a\\n\")\n")), + UNSCOPED_INFO("a string's quotes and backslashes"); + CHECK(pp(std::string("#define STR(x) #x\n" + "STR(\"a\\n\")\n")) == std::string("\"\\\"a\\\\n\\\"\"")); - ensure_equals("a character's", pp(std::string("#define STR(x) #x\n" - "STR('\"') STR('\\'')\n")), + UNSCOPED_INFO("a character's"); + CHECK(pp(std::string("#define STR(x) #x\n" + "STR('\"') STR('\\'')\n")) == std::string("\"'\\\"'\" \"'\\\\''\"")); - ensure_equals("outside a literal, a backslash is as it is", pp(std::string("#define STR(x) #x\n" - "STR(: @\\n)\n")), + UNSCOPED_INFO("outside a literal, a backslash is as it is"); + CHECK(pp(std::string("#define STR(x) #x\n" + "STR(: @\\n)\n")) == std::string("\": @\\n\"")); - ensure_equals("nothing", pp(std::string("#define STR(x) #x\n" - "STR()\n")), + UNSCOPED_INFO("nothing"); + CHECK(pp(std::string("#define STR(x) #x\n" + "STR()\n")) == std::string("\"\"")); } - template<> template<> - void alpreprocessorc11_object::test<17>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.3: ## pastes; an empty argument is a placemarker; the result rescans", "[alpreprocessorc11]") { - set_test_name("6.10.3.3: ## pastes, an empty argument is a placemarker, and what it makes is expanded again"); - ensure_equals("tokens", pp(std::string("#define CAT(a, b) a ## b\n" - "CAT(ll, Say) CAT(-, -) CAT(<, <=) CAT(1, 2) CAT(+, =)\n")), + UNSCOPED_INFO("tokens"); + CHECK(pp(std::string("#define CAT(a, b) a ## b\n" + "CAT(ll, Say) CAT(-, -) CAT(<, <=) CAT(1, 2) CAT(+, =)\n")) == std::string("llSay -- <<= 12 +=")); - ensure_equals("a name made is expanded", pp(std::string("#define CAT(a, b) a ## b\n" + UNSCOPED_INFO("a name made is expanded"); + CHECK(pp(std::string("#define CAT(a, b) a ## b\n" "#define AB 42\n" - "CAT(A, B)\n")), + "CAT(A, B)\n")) == std::string("42")); - ensure_equals("empty on either side", pp(std::string("#define CAT(a, b) a ## b\n" - "[CAT(, x)] [CAT(x, )] [CAT(, )]\n")), + UNSCOPED_INFO("empty on either side"); + CHECK(pp(std::string("#define CAT(a, b) a ## b\n" + "[CAT(, x)] [CAT(x, )] [CAT(, )]\n")) == std::string("[x] [x] []")); - ensure_equals("EXAMPLE 5", pp(std::string("#define t(x,y,z) x ## y ## z\n" + UNSCOPED_INFO("EXAMPLE 5"); + CHECK(pp(std::string("#define t(x,y,z) x ## y ## z\n" "int j[] = { t(1,2,3), t(,4,5), t(6,,7), t(8,9,),\n" - " t(10,,), t(,11,), t(,,12), t(,,) };\n")), + " t(10,,), t(,11,), t(,,12), t(,,) };\n")) == std::string("int j[] = { 123, 45, 67, 89, 10, 11, 12, };")); - ensure_equals("the EXAMPLE's hash_hash: ## made of # ## # is no operator", pp(std::string("#define hash_hash # ## #\n" + UNSCOPED_INFO("the EXAMPLE's hash_hash: ## made of # ## # is no operator"); + CHECK(pp(std::string("#define hash_hash # ## #\n" "#define mkstr(a) # a\n" "#define in_between(a) mkstr(a)\n" "#define join(c, d) in_between(c hash_hash d)\n" - "char p[] = join(x, y);\n")), + "char p[] = join(x, y);\n")) == std::string("char p[] = \"x ## y\";")); - ensure_equals("a paste that makes no one token is said", saidOn(run(std::string("#define CAT(a, b) a ## b\n" + UNSCOPED_INFO("a paste that makes no one token is said"); + CHECK(saidOn(run(std::string("#define CAT(a, b) a ## b\n" "CAT(x, +)\n" - "CAT(\"a\", \"b\")\n"))), std::string("1 2")); + "CAT(\"a\", \"b\")\n"))) == std::string("1 2")); } - template<> template<> - void alpreprocessorc11_object::test<18>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.4: a macro's own name stays a name; one handed on takes later arguments", "[alpreprocessorc11]") { - set_test_name("6.10.3.4: rescanning: a macro's own name is not expanded again, even later, and one it hands on takes its arguments from what follows"); - ensure_equals("the EXAMPLE", pp(std::string("#define f(a) a*g\n" + UNSCOPED_INFO("the EXAMPLE"); + CHECK(pp(std::string("#define f(a) a*g\n" "#define g(a) f(a)\n" - "f(2)(9)\n")), + "f(2)(9)\n")) == std::string("2*9*g")); - ensure_equals("mutual", pp(std::string("#define x y\n" + UNSCOPED_INFO("mutual"); + CHECK(pp(std::string("#define x y\n" "#define y x\n" - "x y\n")), + "x y\n")) == std::string("x y")); - ensure_equals("painted: a name met while its macro was being replaced stays a name", pp(std::string("#define obj(x) x obj\n" - "obj(obj)(1)\n")), + UNSCOPED_INFO("painted: a name met while its macro was being replaced stays a name"); + CHECK(pp(std::string("#define obj(x) x obj\n" + "obj(obj)(1)\n")) == std::string("obj obj(1)")); - ensure_equals("a name handed on finds its ( in the source", pp(std::string("#define f(x) \n" + UNSCOPED_INFO("a name handed on finds its ( in the source"); + CHECK(pp(std::string("#define f(x) \n" "#define g f\n" "g(1) g\n" - "(2)\n")), + "(2)\n")) == std::string("<1> <2>")); - ensure_equals("and through two macros", pp(std::string("#define NIL(xxx) xxx\n" + UNSCOPED_INFO("and through two macros"); + CHECK(pp(std::string("#define NIL(xxx) xxx\n" "#define G_0(arg) NIL(G_1)(arg)\n" "#define G_1(arg) NIL(arg)\n" - "G_0(42)\n")), + "G_0(42)\n")) == std::string("42")); } - template<> template<> - void alpreprocessorc11_object::test<19>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.5 EXAMPLE 3: macros naming themselves or in arguments; placemarkers; #", "[alpreprocessorc11]") { - set_test_name("6.10.3.5 EXAMPLE 3: macros that name themselves, macros in arguments, placemarkers and #"); - ensure_equals("the whole", pp(std::string("#define x 3\n" + UNSCOPED_INFO("the whole"); + CHECK(pp(std::string("#define x 3\n" "#define f(a) f(x * (a))\n" "#undef x\n" "#define x 2\n" @@ -627,15 +674,14 @@ namespace tut "g(x+(3,4)-w) | h 5) & m\n" "(f)^m(m);\n" "p() i[q()] = { q(1), r(2,3), r(4,), r(,5), r(,) };\n" - "char c[2][6] = { str(hello), str() };\n")), + "char c[2][6] = { str(hello), str() };\n")) == std::string("f(2 * (y+1)) + f(2 * (f(2 * (z[0])))) % f(2 * (0)) + t(1); f(2 * (2+(3,4)-0,1)) | f(2 * (~ 5)) & f(2 * (0,1))^m(0,1); int i[] = { 1, 23, 4, 5, }; char c[2][6] = { \"hello\", \"\" };")); } - template<> template<> - void alpreprocessorc11_object::test<20>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.5 EXAMPLE 4: # and ## and macros made of both", "[alpreprocessorc11]") { - set_test_name("6.10.3.5 EXAMPLE 4: #, ##, and macros made of both"); - ensure_equals("debug, fputs, glue", pp(std::string("#define str(s) # s\n" + UNSCOPED_INFO("debug, fputs, glue"); + CHECK(pp(std::string("#define str(s) # s\n" "#define xstr(s) str(s)\n" "#define debug(s, t) printf(\"x\" # s \"= %d, x\" # t \"= %s\", \\\n" " x ## s, x ## t)\n" @@ -648,193 +694,201 @@ namespace tut "fputs(str(strncmp(\"abc\\0d\", \"abc\", '\\4') // this goes away\n" " == 0) str(: @\\n), s);\n" "glue(HIGH, LOW);\n" - "xglue(HIGH, LOW)\n")), + "xglue(HIGH, LOW)\n")) == std::string("printf(\"x\" \"1\" \"= %d, x\" \"2\" \"= %s\", x1, x2); fputs(\"strncmp(\\\"abc\\\\0d\\\", \\\"abc\\\", '\\\\4') == 0\" \": @\\n\", s); \"hello\"; \"hello\" \", world\"")); } - template<> template<> - void alpreprocessorc11_object::test<21>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.5 EXAMPLE 7: variadic macros", "[alpreprocessorc11]") { - set_test_name("6.10.3.5 EXAMPLE 7: variadic macros"); - ensure_equals("debug, showlist, report", pp(std::string("#define debug(...) fprintf(stderr, __VA_ARGS__)\n" + UNSCOPED_INFO("debug, showlist, report"); + CHECK(pp(std::string("#define debug(...) fprintf(stderr, __VA_ARGS__)\n" "#define showlist(...) puts(#__VA_ARGS__)\n" "#define report(test, ...) ((test)?puts(#test):\\\n" " printf(__VA_ARGS__))\n" "debug(\"Flag\");\n" "debug(\"X = %d\\n\", x);\n" "showlist(The first, second, and third items.);\n" - "report(x>y, \"x is %d but y is %d\", x, y);\n")), + "report(x>y, \"x is %d but y is %d\", x, y);\n")) == std::string("fprintf(stderr, \"Flag\"); fprintf(stderr, \"X = %d\\n\", x); puts(\"The first, second, and third items.\"); ((x>y)?puts(\"x>y\"): printf(\"x is %d but y is %d\", x, y));")); } - template<> template<> - void alpreprocessorc11_object::test<22>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.4: #line sets __LINE__ and __FILE__ even from macros; a bad one is said", "[alpreprocessorc11]") { - set_test_name("6.10.4: #line sets the next line's __LINE__, and __FILE__ with a name; its numbers may come of macros; out of range is said"); - ensure_equals("a number", pp(std::string("#line 100\n" + UNSCOPED_INFO("a number"); + CHECK(pp(std::string("#line 100\n" "__LINE__\n" - "__LINE__\n")), + "__LINE__\n")) == std::string("100 101")); - ensure_equals("and a name", pp(std::string("#line 7 \"other.lsl\"\n" - "__LINE__ __FILE__\n")), + UNSCOPED_INFO("and a name"); + CHECK(pp(std::string("#line 7 \"other.lsl\"\n" + "__LINE__ __FILE__\n")) == std::string("7 \"other.lsl\"")); - ensure_equals("of macros", pp(std::string("#define L 40\n" + UNSCOPED_INFO("of macros"); + CHECK(pp(std::string("#define L 40\n" "#line L\n" - "__LINE__\n")), + "__LINE__\n")) == std::string("40")); - ensure_equals("refused", saidOn(run(std::string("#line 0\n" + UNSCOPED_INFO("refused"); + CHECK(saidOn(run(std::string("#line 0\n" "#line abc\n" "#line\n" - "#line 10 name\n"))), std::string("0 1 2 3")); + "#line 10 name\n"))) == std::string("0 1 2 3")); } - template<> template<> - void alpreprocessorc11_object::test<23>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.5 to 6.10.7: #error goes on; unknown #pragmas pass; # alone is nothing", "[alpreprocessorc11]") { - set_test_name("6.10.5, 6.10.6, 6.10.7: #error says its words and the run goes on; a #pragma not known is passed over; # alone is nothing"); { const ALPreprocessor::Result r = run("#error one two\nafter\n"); - ensure_equals("said as written", messages(r), std::string("E 0: one two\n")); - ensure_equals("and on", flat(r.text), std::string("after")); + UNSCOPED_INFO("said as written"); + CHECK(messages(r) == std::string("E 0: one two\n")); + UNSCOPED_INFO("and on"); + CHECK(flat(r.text) == std::string("after")); } - ensure_equals("pragmas passed over", pp(std::string("#pragma STDC FP_CONTRACT ON\n" + UNSCOPED_INFO("pragmas passed over"); + CHECK(pp(std::string("#pragma STDC FP_CONTRACT ON\n" "#pragma whatever it says\n" - "ok\n")), + "ok\n")) == std::string("ok")); - ensure_equals("the null directive", pp(std::string("#\n" - "ok\n")), + UNSCOPED_INFO("the null directive"); + CHECK(pp(std::string("#\n" + "ok\n")) == std::string("ok")); } - template<> template<> - void alpreprocessorc11_object::test<24>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.8: __LINE__ __FILE__ __DATE__ __TIME__ as C has them; each is defined", "[alpreprocessorc11]") { - set_test_name("6.10.8: __LINE__ and __FILE__ where they stand, __DATE__ and __TIME__ as C writes them, and #ifdef knows each"); - ensure_equals("lines from 1", pp(std::string("__LINE__\n" + UNSCOPED_INFO("lines from 1"); + CHECK(pp(std::string("__LINE__\n" "\n" - "__LINE__\n")), + "__LINE__\n")) == std::string("1 3")); - ensure_equals("defined", pp(std::string("#if defined(__LINE__) && defined __FILE__ && defined(__DATE__) && defined __TIME__\n" + UNSCOPED_INFO("defined"); + CHECK(pp(std::string("#if defined(__LINE__) && defined __FILE__ && defined(__DATE__) && defined __TIME__\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); { const std::string text = flat(run("__DATE__ __TIME__\n").text); - ensure("\"Mmm dd yyyy\" \"hh:mm:ss\", a day under 10 with a space: " + text, - std::regex_match(text, std::regex("\"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ 123][0-9] [0-9]{4}\" \"[0-2][0-9]:[0-5][0-9]:[0-6][0-9]\""))); + UNSCOPED_INFO("\"Mmm dd yyyy\" \"hh:mm:ss\", a day under 10 with a space: " + text); + CHECK(std::regex_match(text, std::regex("\"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ 123][0-9] [0-9]{4}\" \"[0-2][0-9]:[0-5][0-9]:[0-6][0-9]\""))); } } - template<> template<> - void alpreprocessorc11_object::test<25>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.9: _Pragma is #pragma as an operator a macro can make", "[alpreprocessorc11]") { - set_test_name("6.10.9: _Pragma is #pragma as an operator, where a macro can make it"); files["p.lsl"] = "_Pragma(\"once\")\nP\n"; - ensure_equals("once", pp(std::string("#include \"p.lsl\"\n" - "#include \"p.lsl\"\n")), + UNSCOPED_INFO("once"); + CHECK(pp(std::string("#include \"p.lsl\"\n" + "#include \"p.lsl\"\n")) == std::string("P")); - ensure_equals("made by a macro, and gone from the text", pp(std::string("#define DO(x) _Pragma(#x)\n" - "DO(whatever) ok\n")), + UNSCOPED_INFO("made by a macro, and gone from the text"); + CHECK(pp(std::string("#define DO(x) _Pragma(#x)\n" + "DO(whatever) ok\n")) == std::string("ok")); } - template<> template<> - void alpreprocessorc11_object::test<26>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: tokens a macro sets side by side stay apart: -f(1) is two minuses", "[alpreprocessorc11]") { - set_test_name("the text keeps apart tokens that a macro set beside each other: -f(1) is still two minuses"); - ensure_equals("a minus before a macro's", pp(std::string("#define f(x) -x\n" - "-f(1)\n")), + UNSCOPED_INFO("a minus before a macro's"); + CHECK(pp(std::string("#define f(x) -x\n" + "-f(1)\n")) == std::string("- -1")); - ensure_equals("a plus", pp(std::string("#define PLUS +\n" - "+PLUS\n")), + UNSCOPED_INFO("a plus"); + CHECK(pp(std::string("#define PLUS +\n" + "+PLUS\n")) == std::string("+ +")); - ensure_equals("an empty macro between two", pp(std::string("#define E\n" - "x E-E-y\n")), + UNSCOPED_INFO("an empty macro between two"); + CHECK(pp(std::string("#define E\n" + "x E-E-y\n")) == std::string("x - -y")); - ensure_equals("two invocations", pp(std::string("#define I(x) x\n" - "I(-)I(-)\n")), + UNSCOPED_INFO("two invocations"); + CHECK(pp(std::string("#define I(x) x\n" + "I(-)I(-)\n")) == std::string("- -")); - ensure_equals("names", pp(std::string("#define A a\n" + UNSCOPED_INFO("names"); + CHECK(pp(std::string("#define A a\n" "#define B b\n" - "A B\n")), + "A B\n")) == std::string("a b")); - ensure_equals("a paste means it", pp(std::string("#define CAT(a, b) a ## b\n" - "CAT(-, -)\n")), + UNSCOPED_INFO("a paste means it"); + CHECK(pp(std::string("#define CAT(a, b) a ## b\n" + "CAT(-, -)\n")) == std::string("--")); } - template<> template<> - void alpreprocessorc11_object::test<27>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.4: an empty macro defers an expansion that one more scan makes", "[alpreprocessorc11]") { - set_test_name("6.10.3.4: expansion deferred by an empty macro, and made to happen by a macro that rescans"); - ensure_equals("deferred", pp(std::string("#define EMPTY()\n" + UNSCOPED_INFO("deferred"); + CHECK(pp(std::string("#define EMPTY()\n" "#define DEFER(id) id EMPTY()\n" "#define A() 123\n" - "DEFER(A)()\n")), + "DEFER(A)()\n")) == std::string("A ()")); - ensure_equals("and expanded by one more scan", pp(std::string("#define EMPTY()\n" + UNSCOPED_INFO("and expanded by one more scan"); + CHECK(pp(std::string("#define EMPTY()\n" "#define DEFER(id) id EMPTY()\n" "#define EXPAND(...) __VA_ARGS__\n" "#define A() 123\n" - "EXPAND(DEFER(A)())\n")), + "EXPAND(DEFER(A)())\n")) == std::string("123")); - ensure_equals("a macro passed as an argument and called in the body", pp(std::string("#define apply(m, x) m(x)\n" + UNSCOPED_INFO("a macro passed as an argument and called in the body"); + CHECK(pp(std::string("#define apply(m, x) m(x)\n" "#define sq(x) ((x)*(x))\n" - "apply(sq, 3)\n")), + "apply(sq, 3)\n")) == std::string("((3)*(3))")); - ensure_equals("each other through arguments", pp(std::string("#define f(x) g(x)\n" + UNSCOPED_INFO("each other through arguments"); + CHECK(pp(std::string("#define f(x) g(x)\n" "#define g(x) f(x)\n" - "f(1) g(2)\n")), + "f(1) g(2)\n")) == std::string("f(1) g(2)")); } - template<> template<> - void alpreprocessorc11_object::test<28>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.2: #__VA_ARGS__ is the arguments with their commas and blanks made one", "[alpreprocessorc11]") { - set_test_name("6.10.3.2: # of __VA_ARGS__: the arguments and their commas, blanks as written between them made one"); - ensure_equals("none", pp(std::string("#define S(...) #__VA_ARGS__\n" - "S()\n")), + UNSCOPED_INFO("none"); + CHECK(pp(std::string("#define S(...) #__VA_ARGS__\n" + "S()\n")) == std::string("\"\"")); - ensure_equals("commas kept as written", pp(std::string("#define S(...) #__VA_ARGS__\n" - "S(a,b) S( a , b ) S(a, b)\n")), + UNSCOPED_INFO("commas kept as written"); + CHECK(pp(std::string("#define S(...) #__VA_ARGS__\n" + "S(a,b) S( a , b ) S(a, b)\n")) == std::string("\"a,b\" \"a , b\" \"a, b\"")); - ensure_equals("a string among them", pp(std::string("#define S(...) #__VA_ARGS__\n" - "S(\"x\", y)\n")), + UNSCOPED_INFO("a string among them"); + CHECK(pp(std::string("#define S(...) #__VA_ARGS__\n" + "S(\"x\", y)\n")) == std::string("\"\\\"x\\\", y\"")); } - template<> template<> - void alpreprocessorc11_object::test<29>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3.3: a paste making // or two tokens is said and leaves them as they were", "[alpreprocessorc11]") { - set_test_name("6.10.3.3: a paste that makes a comment's start, or any two tokens, is said, and the tokens are left as they were"); - ensure_equals("// and a name with a mark", saidOn(run(std::string("#define CAT(a, b) a ## b\n" + UNSCOPED_INFO("// and a name with a mark"); + CHECK(saidOn(run(std::string("#define CAT(a, b) a ## b\n" "CAT(/, /)\n" - "CAT(x, .)\n"))), std::string("1 2")); + "CAT(x, .)\n"))) == std::string("1 2")); } - template<> template<> - void alpreprocessorc11_object::test<30>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: #ifdef knows function-like and predefined macros; #undef says nothing", "[alpreprocessorc11]") { - set_test_name("6.10.1: #ifdef knows a function-like macro and a predefined one; #undef and a new body say nothing"); - ensure_equals("defined", pp(std::string("#define F(x) x\n" + UNSCOPED_INFO("defined"); + CHECK(pp(std::string("#define F(x) x\n" "#ifdef F\n" "yes\n" "#endif\n" "#ifndef __LINE__\n" "no\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("undef, then any body", saidOn(run(std::string("#define A 1\n" + UNSCOPED_INFO("undef, then any body"); + CHECK(saidOn(run(std::string("#define A 1\n" "#undef A\n" "#define A 2\n" - "#undef NEVER_DEFINED\n"))), std::string("")); + "#undef NEVER_DEFINED\n"))) == std::string("")); } - template<> template<> - void alpreprocessorc11_object::test<31>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: #elif chains take the first true group and skip nested groups whole", "[alpreprocessorc11]") { - set_test_name("6.10.1: #elif and #else chains: the first true group only, nested groups inside skipped ones skipped whole"); - ensure_equals("a chain", pp(std::string("#define V 3\n" + UNSCOPED_INFO("a chain"); + CHECK(pp(std::string("#define V 3\n" "#if V == 1\n" "one\n" "#elif V == 2\n" @@ -845,9 +899,10 @@ namespace tut "again\n" "#else\n" "else\n" - "#endif\n")), + "#endif\n")) == std::string("three")); - ensure_equals("nested", pp(std::string("#if 1\n" + UNSCOPED_INFO("nested"); + CHECK(pp(std::string("#if 1\n" "#if 0\n" "a\n" "#elif 1\n" @@ -862,78 +917,79 @@ namespace tut "#if 1\n" "e\n" "#endif\n" - "#endif\n")), + "#endif\n")) == std::string("b d")); - ensure_equals("an #elif with nothing, where it is worked out", saidOn(run(std::string("#if 0\n" + UNSCOPED_INFO("an #elif with nothing, where it is worked out"); + CHECK(saidOn(run(std::string("#if 0\n" "#elif\n" - "#endif\n"))), std::string("1")); - ensure_equals("but not where it is skipped", saidOn(run(std::string("#if 1\n" + "#endif\n"))) == std::string("1")); + UNSCOPED_INFO("but not where it is skipped"); + CHECK(saidOn(run(std::string("#if 1\n" "#elif\n" - "#endif\n"))), std::string("")); + "#endif\n"))) == std::string("")); } - template<> template<> - void alpreprocessorc11_object::test<32>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.1: unsigned shift and divide; GCC's negative right shift; bare 0x refused", "[alpreprocessorc11]") { - set_test_name("6.10.1: more integer arithmetic: unsigned shifts and division, a negative right shift as GCC and Wave do it, a bare 0x refused"); - ensure_equals("unsigned", pp(std::string("#if 1u << 63 > 0 && 18446744073709551615u / 2 == 9223372036854775807 && (0u - 1) >> 63 == 1\n" + UNSCOPED_INFO("unsigned"); + CHECK(pp(std::string("#if 1u << 63 > 0 && 18446744073709551615u / 2 == 9223372036854775807 && (0u - 1) >> 63 == 1\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("signed", pp(std::string("#if -1 >> 1 == -1 && (-1) / 2 == 0 && -8 >> 1 == -4\n" + UNSCOPED_INFO("signed"); + CHECK(pp(std::string("#if -1 >> 1 == -1 && (-1) / 2 == 0 && -8 >> 1 == -4\n" "yes\n" - "#endif\n")), + "#endif\n")) == std::string("yes")); - ensure_equals("0x alone, and a suffix C has not got", saidOn(run(std::string("#if 0x\n" + UNSCOPED_INFO("0x alone, and a suffix C has not got"); + CHECK(saidOn(run(std::string("#if 0x\n" "#endif\n" "#if 10lul\n" - "#endif\n"))), std::string("0 2")); + "#endif\n"))) == std::string("0 2")); } - template<> template<> - void alpreprocessorc11_object::test<33>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.2: an include has its own lines and name until the file it returns to", "[alpreprocessorc11]") { - set_test_name("6.10.2: an include's own lines and name, and the file it came back to after it"); files["inc.lsl"] = "inc __LINE__ __FILE__\n#define FROM_INC 1\n"; - ensure_equals("inside and after", pp(std::string("one\n" + UNSCOPED_INFO("inside and after"); + CHECK(pp(std::string("one\n" "#include \"inc.lsl\"\n" - "__LINE__ __FILE__ FROM_INC\n")), + "__LINE__ __FILE__ FROM_INC\n")) == std::string("one inc 1 \"inc.lsl\" 3 \"main.lsl\" 1")); files["half.lsl"] = "#define f(x) \nf(\n"; - ensure_equals("an invocation cannot run out of an include", saidOn(run(std::string("#include \"half.lsl\"\n" - "1)\n"))), std::string("1")); + UNSCOPED_INFO("an invocation cannot run out of an include"); + CHECK(saidOn(run(std::string("#include \"half.lsl\"\n" + "1)\n"))) == std::string("1")); } - template<> template<> - void alpreprocessorc11_object::test<34>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: files that end without a newline: a directive or invocation or conditional", "[alpreprocessorc11]") { - set_test_name("files that end without a newline: a directive, an invocation, a conditional"); - ensure_equals("a directive last", pp(std::string("#define X 1\n" + UNSCOPED_INFO("a directive last"); + CHECK(pp(std::string("#define X 1\n" "X\n" - "#undef X")), + "#undef X")) == std::string("1")); - ensure_equals("an invocation last", pp(std::string("#define f(x) \n" - "f(1)")), + UNSCOPED_INFO("an invocation last"); + CHECK(pp(std::string("#define f(x) \n" + "f(1)")) == std::string("<1>")); - ensure_equals("an #if left open", saidOn(run(std::string("#if 1\n" - "x"))), std::string("0")); + UNSCOPED_INFO("an #if left open"); + CHECK(saidOn(run(std::string("#if 1\n" + "x"))) == std::string("0")); } - template<> template<> - void alpreprocessorc11_object::test<35>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 6.10.3: LSL's own words may name macros since C knows no keywords", "[alpreprocessorc11]") { - set_test_name("6.10.3: a macro named as LSL names its own words: C knows no keywords, so any name is a macro's"); - ensure_equals("words and events", pp(std::string("#define default state_x\n" + UNSCOPED_INFO("words and events"); + CHECK(pp(std::string("#define default state_x\n" "#define state_entry on_rez\n" "#define jump return\n" - "default { state_entry() { jump; } }\n")), + "default { state_entry() { jump; } }\n")) == std::string("state_x { on_rez() { return; } }")); } - template<> template<> - void alpreprocessorc11_object::test<36>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: SLua: #if takes Luau's and/or/not/~= as well as C's operators", "[alpreprocessorc11]") { - set_test_name("SLua: #if written as Luau writes it: and, or, not, ~=, and C's too"); { ALPreprocessor::Options lua = options(); lua.lua = true; @@ -942,15 +998,13 @@ namespace tut "--#if B or A ~= 2\ntwo\n--#endif\n" "--#if (A && !B) == 1\nthree\n--#endif\n" "--#if not A or B\nno\n--#else\nfour\n--#endif\n", lua); - ensure_equals("said", messages(r), std::string()); - ensure_equals("taken", flat(r.text), std::string("one two three four")); + CHECK(messages(r) == std::string()); + CHECK(flat(r.text) == std::string("one two three four")); } } - template<> template<> - void alpreprocessorc11_object::test<37>() + TEST_CASE_METHOD(alpreprocessorc11_data, "alpreprocessorc11: 5.1.1.2: lines end at CRLF and CR too; blanks after a backslash are warned of", "[alpreprocessorc11]") { - set_test_name("5.1.1.2: a line ends at CRLF and at a lone CR as at LF, so a backslash before either joins; on a directive's line blanks between the backslash and the newline are let by with a warning, elsewhere they join nothing"); const auto ended = [](std::string text, const char* end) { std::string out; for (char c : text) @@ -964,25 +1018,36 @@ namespace tut " llSay(0, x)\n" "default { state_entry() { SAY(\"hi\"); } }\n"; const std::string joined = pp(lf); - ensure("the macro's lines are its body: " + joined, joined.find("PROBLEMS") == std::string::npos && joined.find("llSay") != std::string::npos && - joined.find('\\') == std::string::npos && joined.find("default") == 0); - ensure_equals("CRLF", pp(ended(lf, "\r\n")), joined); - ensure_equals("a lone CR", pp(ended(lf, "\r")), joined); - ensure_equals("the lines after keep their numbers", pp(ended("#define A 1 \\\n + 2\nA __LINE__\n", "\r\n")), std::string("1 + 2 3")); + { + INFO("the macro's lines are its body: " + joined); + CHECK(joined.find("PROBLEMS") == std::string::npos); + CHECK(joined.find("llSay") != std::string::npos); + CHECK(joined.find('\\') == std::string::npos); + CHECK(joined.find("default") == 0); + } + UNSCOPED_INFO("CRLF"); + CHECK(pp(ended(lf, "\r\n")) == joined); + UNSCOPED_INFO("a lone CR"); + CHECK(pp(ended(lf, "\r")) == joined); + UNSCOPED_INFO("the lines after keep their numbers"); + CHECK(pp(ended("#define A 1 \\\n + 2\nA __LINE__\n", "\r\n")) == std::string("1 + 2 3")); { const ALPreprocessor::Result r = run("#define SAY(x) \\ \t\n" " llOwnerSay(x); \\ \n" " llSay(0, x)\n" "default { state_entry() { SAY(\"hi\"); } }\n"); - ensure_equals("blanks after a directive's backslash", flat(r.text), joined); - ensure_equals("said of each", messages(r), + UNSCOPED_INFO("blanks after a directive's backslash"); + CHECK(flat(r.text) == joined); + UNSCOPED_INFO("said of each"); + CHECK(messages(r) == std::string("W 0: backslash and newline separated by space\nW 1: backslash and newline separated by space\n")); } { const ALPreprocessor::Result r = run("// a note \\ \nint shown;\n"); - ensure("a note's backslash and a blank take no line: " + r.text, r.text.find("int shown;") != std::string::npos); - ensure_equals("nothing said", messages(r), std::string()); + UNSCOPED_INFO("a note's backslash and a blank take no line: " + r.text); + CHECK(r.text.find("int shown;") != std::string::npos); + CHECK(messages(r) == std::string()); } } } diff --git a/indra/alscript/tests/alscript_test_checks.cpp b/indra/alscript/tests/alscript_test_checks.cpp new file mode 100644 index 0000000000..2783173505 --- /dev/null +++ b/indra/alscript/tests/alscript_test_checks.cpp @@ -0,0 +1,118 @@ +/** + * @file alscript_test_checks.cpp + * @brief What every binary of alscript's tests sets up for the process once, + * as the viewer has it before any script is checked, and the state + * guard's checks on what the script engines keep for the process. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../core/alscriptengine.h" +#include "../lsl/allslservice.h" +#include "../luau/alluauservice.h" + +#include "alteststateguard.h" + +#include "Luau/Common.h" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + // Every one of Luau's flags, name=value a line, sorted. + std::string luauFlags() + { + std::vector lines; + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + lines.push_back(std::string(flag->name) + "=" + (flag->value ? "true" : "false")); + } + for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) + { + lines.push_back(std::string(flag->name) + "=" + std::to_string(flag->value)); + } + std::sort(lines.begin(), lines.end()); + std::string text; + for (const std::string& line : lines) + { + text += line + "\n"; + } + return text; + } + + // Whether a thread is still inside Tailslide. + std::string engineOwner() + { + return al_script_engine::owner().load() == std::thread::id() ? "free" : "held by a thread"; + } + + // How many names Tailslide's builtins table holds: one table for the + // process, only ever added to. + std::string builtinCount() + { + AL_SCRIPT_ENGINE_HELD; + Tailslide::ScopedScriptParser probe(nullptr); + Tailslide::LSLSymbolTable* table = probe.context.builtins; + return table ? std::to_string(table->getMap().size()) : std::string("none"); + } + + class ALScriptChecks : public Catch::EventListenerBase + { + public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) override + { + // The process as the viewer has it before it checks a script: + // Luau's flags set as the grid's SLua needs them, which a + // .config.luau run before any service was made would not see; + // and the grid's builtins loaded first, so that every file of + // builtins a test loads after them only adds to them, whatever + // order the test files run in. + ALLuauService::setUpProcess(); + ALLSLService lsl; + std::string error; + if (!lsl.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error)) + { + LL_WARNS() << "The grid's builtins did not load: " << error << LL_ENDL; + } + + // A case flips a flag of Luau's only for itself. + ll_test::StateGuard::add("Luau's flags", luauFlags); + // A case leaves no thread inside Tailslide, where the next + // case's would count as a second thread in at once. + ll_test::StateGuard::add("Tailslide's engine", engineOwner); + // A case that loads builtins of its own says so: they stay for + // every case after it. + ll_test::StateGuard::add("Tailslide's builtins", builtinCount); + } + }; +} + +CATCH_REGISTER_LISTENER(ALScriptChecks) diff --git a/indra/alscript/tests/alscriptenvelope_test.cpp b/indra/alscript/tests/alscriptenvelope_test.cpp index ae9583de04..56f5dd0e8f 100644 --- a/indra/alscript/tests/alscriptenvelope_test.cpp +++ b/indra/alscript/tests/alscriptenvelope_test.cpp @@ -27,13 +27,13 @@ #include "../preprocessor/alscriptenvelope.h" #include "../preprocessor/aluploadheader.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { struct alscriptenvelope_data { @@ -90,28 +90,27 @@ namespace tut } }; - typedef test_group alscriptenvelope_group; - typedef alscriptenvelope_group::object alscriptenvelope_object; - alscriptenvelope_group alscriptenvelope_instance("alscriptenvelope"); - - template<> template<> - void alscriptenvelope_object::test<1>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: Firestorm's escaping puts a bar after each / or * before a / or * or |", "[alscriptenvelope]") { - set_test_name("the escaping is Firestorm's: a bar after every / or * that a /, * or | follows"); - ensure_equals("comments", ALScriptEnvelope::encodeSource("/* a */ // b"), std::string("/|* a *|/ /|/ b")); - ensure_equals("a bar of its own", ALScriptEnvelope::encodeSource("x /| y"), std::string("x /|| y")); - ensure_equals("a run of stars", ALScriptEnvelope::encodeSource("***"), std::string("*|*|*")); - ensure_equals("nothing to escape", ALScriptEnvelope::encodeSource("a / b * c"), std::string("a / b * c")); - ensure_equals("and back", ALScriptEnvelope::decodeSource("/|* a *|/ /|/ b"), std::string("/* a */ // b")); - ensure_equals("back from stars", ALScriptEnvelope::decodeSource("*|*|*"), std::string("***")); + UNSCOPED_INFO("comments"); + CHECK(ALScriptEnvelope::encodeSource("/* a */ // b") == std::string("/|* a *|/ /|/ b")); + UNSCOPED_INFO("a bar of its own"); + CHECK(ALScriptEnvelope::encodeSource("x /| y") == std::string("x /|| y")); + UNSCOPED_INFO("a run of stars"); + CHECK(ALScriptEnvelope::encodeSource("***") == std::string("*|*|*")); + UNSCOPED_INFO("nothing to escape"); + CHECK(ALScriptEnvelope::encodeSource("a / b * c") == std::string("a / b * c")); + UNSCOPED_INFO("and back"); + CHECK(ALScriptEnvelope::decodeSource("/|* a *|/ /|/ b") == std::string("/* a */ // b")); + UNSCOPED_INFO("back from stars"); + CHECK(ALScriptEnvelope::decodeSource("*|*|*") == std::string("***")); const std::string source = "default\n{\n state_entry()\n {\n /* hi */ llSay(0, \"a // b\");\n }\n}\n"; - ensure_equals("round trip", ALScriptEnvelope::decodeSource(ALScriptEnvelope::encodeSource(source)), source); + UNSCOPED_INFO("round trip"); + CHECK(ALScriptEnvelope::decodeSource(ALScriptEnvelope::encodeSource(source)) == source); } - template<> template<> - void alscriptenvelope_object::test<2>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: an LSL asset the preprocessor wrote reads back as its parts", "[alscriptenvelope]") { - set_test_name("an LSL asset the preprocessor wrote reads back as its parts"); const std::string asset = "//start_unprocessed_text\n" "/*#define X 1\n" @@ -123,25 +122,25 @@ namespace tut "//last_compiled 09/20/2026 10:11:12\n" "//mono\n" "default { state_entry() { llSay(0, (string)1); } }\n"; - ensure("looks wrapped", ALScriptEnvelope::looksWrapped(asset)); + CHECK(ALScriptEnvelope::looksWrapped(asset)); std::optional envelope = ALScriptEnvelope::parse(asset); - ensure("parsed", envelope.has_value()); - ensure("lsl", !envelope->lua); - ensure_equals("the source", envelope->source, + REQUIRE(envelope.has_value()); + CHECK_FALSE(envelope->lua); + CHECK(envelope->source == std::string("#define X 1\n/* a comment */\ndefault { state_entry() { llSay(0, (string)X); } }")); - ensure_equals("the target", envelope->compileTarget, std::string("mono")); - ensure_equals("the program", envelope->programVersion, std::string("Firestorm-Releasex64 7.1.11 (76496)")); - ensure_equals("the date", envelope->lastCompiled, std::string("09/20/2026 10:11:12")); - ensure_equals("the expanded code", envelope->expanded, std::string("default { state_entry() { llSay(0, (string)1); } }\n")); - - ensure("plain source is not an envelope", !ALScriptEnvelope::parse("default { }").has_value()); - ensure("a start without an end is not one", !ALScriptEnvelope::parse("//start_unprocessed_text\n/* x").has_value()); + CHECK(envelope->compileTarget == std::string("mono")); + CHECK(envelope->programVersion == std::string("Firestorm-Releasex64 7.1.11 (76496)")); + CHECK(envelope->lastCompiled == std::string("09/20/2026 10:11:12")); + CHECK(envelope->expanded == std::string("default { state_entry() { llSay(0, (string)1); } }\n")); + + UNSCOPED_INFO("plain source is not an envelope"); + REQUIRE_FALSE(ALScriptEnvelope::parse("default { }").has_value()); + UNSCOPED_INFO("a start without an end is not one"); + REQUIRE_FALSE(ALScriptEnvelope::parse("//start_unprocessed_text\n/* x").has_value()); } - template<> template<> - void alscriptenvelope_object::test<3>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: what is written is what Firestorm writes and reads back", "[alscriptenvelope]") { - set_test_name("what is written is what Firestorm writes, and reads back"); ALScriptEnvelope envelope; envelope.source = "// src\ndefault { }"; envelope.expanded = "default { }\n"; @@ -149,21 +148,19 @@ namespace tut envelope.programVersion = "Alchemy 9.0.0"; envelope.lastCompiled = "now"; const std::string asset = envelope.wrap(); - ensure_equals("byte for byte", - asset, + UNSCOPED_INFO("byte for byte"); + CHECK(asset == std::string("//start_unprocessed_text\n/*/|/ src\ndefault { }*/\n//end_unprocessed_text\n" "//nfo_preprocessor_version 0\n//program_version Alchemy 9.0.0\n//last_compiled now\n//lsl2\ndefault { }\n")); std::optional back = ALScriptEnvelope::parse(asset); - ensure("reads back", back.has_value()); - ensure_equals("the source", back->source, envelope.source); - ensure_equals("the expanded code", back->expanded, envelope.expanded); - ensure_equals("the target", back->compileTarget, envelope.compileTarget); + REQUIRE(back.has_value()); + CHECK(back->source == envelope.source); + CHECK(back->expanded == envelope.expanded); + CHECK(back->compileTarget == envelope.compileTarget); } - template<> template<> - void alscriptenvelope_object::test<4>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: SLua gets a long comment at a level the source has none of", "[alscriptenvelope]") { - set_test_name("SLua gets a long comment at a level the source has none of"); ALScriptEnvelope envelope; envelope.lua = true; envelope.source = "local t = {[[a]], [=[b]=]}\nprint(t)"; @@ -172,35 +169,37 @@ namespace tut envelope.programVersion = "Alchemy"; envelope.lastCompiled = "now"; const std::string asset = envelope.wrap(); - ensure("level two, since the source closes levels zero and one", - asset.find("--[==[local t") != std::string::npos && asset.find("]==]\n--end_unprocessed_text\n") != std::string::npos); - ensure("looks wrapped", ALScriptEnvelope::looksWrapped(asset)); + { + INFO("level two, since the source closes levels zero and one"); + REQUIRE(asset.find("--[==[local t") != std::string::npos); + REQUIRE(asset.find("]==]\n--end_unprocessed_text\n") != std::string::npos); + } + CHECK(ALScriptEnvelope::looksWrapped(asset)); std::optional back = ALScriptEnvelope::parse(asset); - ensure("reads back", back.has_value()); - ensure("lua", back->lua); - ensure_equals("the source", back->source, envelope.source); - ensure_equals("the expanded code", back->expanded, envelope.expanded); - ensure_equals("the target", back->compileTarget, std::string("luau")); + REQUIRE(back.has_value()); + CHECK(back->lua); + CHECK(back->source == envelope.source); + CHECK(back->expanded == envelope.expanded); + CHECK(back->compileTarget == std::string("luau")); } - template<> template<> - void alscriptenvelope_object::test<5>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: a directive is a line of its own", "[alscriptenvelope]") { - set_test_name("a directive is a line of its own"); - ensure_equals("mono", ALScriptEnvelope::directiveOf("// x\n//mono\ndefault{}", false), std::string("mono")); + CHECK(ALScriptEnvelope::directiveOf("// x\n//mono\ndefault{}", false) == std::string("mono")); // As Firestorm has it: mono anywhere wins over lsl2 anywhere. - ensure_equals("mono over lsl2", ALScriptEnvelope::directiveOf("//lsl2\n//mono\n", false), std::string("mono")); - ensure_equals("lsl2", ALScriptEnvelope::directiveOf("//lsl2\n", false), std::string("lsl2")); - ensure_equals("none", ALScriptEnvelope::directiveOf("//monolith\n", false), std::string()); - ensure_equals("not mid-line", ALScriptEnvelope::directiveOf("x //mono\n", false), std::string()); - ensure_equals("lua", ALScriptEnvelope::directiveOf("--luau\n", true), std::string("luau")); - ensure_equals("not lua's in lsl", ALScriptEnvelope::directiveOf("--luau\n", false), std::string()); + UNSCOPED_INFO("mono over lsl2"); + CHECK(ALScriptEnvelope::directiveOf("//lsl2\n//mono\n", false) == std::string("mono")); + CHECK(ALScriptEnvelope::directiveOf("//lsl2\n", false) == std::string("lsl2")); + CHECK(ALScriptEnvelope::directiveOf("//monolith\n", false) == std::string()); + UNSCOPED_INFO("not mid-line"); + CHECK(ALScriptEnvelope::directiveOf("x //mono\n", false) == std::string()); + CHECK(ALScriptEnvelope::directiveOf("--luau\n", true) == std::string("luau")); + UNSCOPED_INFO("not lua's in lsl"); + CHECK(ALScriptEnvelope::directiveOf("--luau\n", false) == std::string()); } - template<> template<> - void alscriptenvelope_object::test<6>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: SLua's long comment level avoids a closer formed with the source's tail", "[alscriptenvelope]") { - set_test_name("SLua's long comment closes where the source ends, not at a closer the source's tail makes with it"); struct Case { std::string name; @@ -231,56 +230,67 @@ namespace tut size_t level = 0; size_t body = 0; const size_t end = luaCommentEnd(asset, level, body); - ensure_equals(c.name + ": the level", level, c.level); - ensure_equals(c.name + ": the comment closes where the source ends", end, body + c.source.size()); - ensure_equals(c.name + ": and the end line follows the closer", - asset.substr(end + level + 2, 24), + CAPTURE(c.name); + REQUIRE(level == c.level); + UNSCOPED_INFO("the comment closes where the source ends"); + REQUIRE(end == body + c.source.size()); + UNSCOPED_INFO("and the end line follows the closer"); + REQUIRE(asset.substr(end + level + 2, 24) == std::string("\n--end_unprocessed_text\n")); std::optional back = ALScriptEnvelope::parse(asset); - ensure(c.name + ": reads back", back.has_value()); - ensure_equals(c.name + ": the source", back->source, c.source); - ensure_equals(c.name + ": the expanded code", back->expanded, envelope.expanded); + REQUIRE(back.has_value()); + REQUIRE(back->source == c.source); + REQUIRE(back->expanded == envelope.expanded); } } - template<> template<> - void alscriptenvelope_object::test<7>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: compiledFrom sees past spacing and comments and dropped parts but not edits", "[alscriptenvelope]") { - set_test_name("a compiled half the source makes now, spacing, comments and an optimizer's leavings aside; and one it does not"); const std::string made = "integer g = 3;\nfloat unused = 1.0;\nhelper() { llSay(0, \"a\"); }\n" "default\n{\n state_entry()\n {\n g = 4;\n llSay(0, (string)g);\n }\n}\n"; - ensure("itself", ALScriptEnvelope::compiledFrom(made, made, false)); - ensure("spaced and commented otherwise", - ALScriptEnvelope::compiledFrom(made, "// made here\ninteger g=3;float unused=1.0;helper(){llSay(0,\"a\");}" + UNSCOPED_INFO("itself"); + CHECK(ALScriptEnvelope::compiledFrom(made, made, false)); + UNSCOPED_INFO("spaced and commented otherwise"); + CHECK(ALScriptEnvelope::compiledFrom(made, "// made here\ninteger g=3;float unused=1.0;helper(){llSay(0,\"a\");}" "default{state_entry(){g=4;llSay(0,(string)g);}}", false)); - ensure("an unused global and function left out", - ALScriptEnvelope::compiledFrom(made, "integer g = 3;\ndefault { state_entry() { g = 4; llSay(0, (string)g); } }", false)); + UNSCOPED_INFO("an unused global and function left out"); + CHECK(ALScriptEnvelope::compiledFrom(made, "integer g = 3;\ndefault { state_entry() { g = 4; llSay(0, (string)g); } }", false)); // 3 is a token the script has elsewhere: changed here, it is // still not what the source makes. - ensure("a number changed", !ALScriptEnvelope::compiledFrom(made, "integer g = 3;\ndefault { state_entry() { g = 3; llSay(0, (string)g); } }", false)); - ensure("a line added", - !ALScriptEnvelope::compiledFrom(made, "integer g = 3;\ndefault { state_entry() { g = 4; llSay(0, (string)g); llDie(); } }", false)); - ensure("a part added", !ALScriptEnvelope::compiledFrom(made, made + "extra() { }\n", false)); - ensure("in another order", !ALScriptEnvelope::compiledFrom(made, "default { state_entry() { g = 4; llSay(0, (string)g); } }\ninteger g = 3;", false)); + UNSCOPED_INFO("a number changed"); + CHECK_FALSE(ALScriptEnvelope::compiledFrom(made, "integer g = 3;\ndefault { state_entry() { g = 3; llSay(0, (string)g); } }", false)); + UNSCOPED_INFO("a line added"); + CHECK_FALSE(ALScriptEnvelope::compiledFrom(made, "integer g = 3;\ndefault { state_entry() { g = 4; llSay(0, (string)g); llDie(); } }", false)); + UNSCOPED_INFO("a part added"); + CHECK_FALSE(ALScriptEnvelope::compiledFrom(made, made + "extra() { }\n", false)); + UNSCOPED_INFO("in another order"); + CHECK_FALSE(ALScriptEnvelope::compiledFrom(made, "default { state_entry() { g = 4; llSay(0, (string)g); } }\ninteger g = 3;", false)); const std::string lua = "local n = 1\nprint(n)\n"; - ensure("SLua: the same tokens", ALScriptEnvelope::compiledFrom(lua, "local n=1 -- one\nprint(n)", true)); - ensure("SLua: not", !ALScriptEnvelope::compiledFrom(lua, "local n = 2\nprint(n)\n", true)); - ensure("SLua: nothing left out", !ALScriptEnvelope::compiledFrom(lua, "local n = 1\n", true)); - - ensure("compiled here: told, transformed or not", ALScriptEnvelope::comparable(made, false, true, true)); - ensure("elsewhere, untransformed: told", ALScriptEnvelope::comparable(made, false, false, false)); - ensure("elsewhere, transformed: not", !ALScriptEnvelope::comparable(made, false, false, true)); - ensure("the time asked for: not", !ALScriptEnvelope::comparable("string t = __TIME__;", false, true, false)); - ensure("who compiled it: not", !ALScriptEnvelope::comparable("local a = __AGENTNAME__", true, true, false)); - ensure("named in a comment only: told", ALScriptEnvelope::comparable("// built __DATE__\ninteger x;", false, true, false)); + UNSCOPED_INFO("SLua: the same tokens"); + CHECK(ALScriptEnvelope::compiledFrom(lua, "local n=1 -- one\nprint(n)", true)); + UNSCOPED_INFO("SLua: not"); + CHECK_FALSE(ALScriptEnvelope::compiledFrom(lua, "local n = 2\nprint(n)\n", true)); + UNSCOPED_INFO("SLua: nothing left out"); + CHECK_FALSE(ALScriptEnvelope::compiledFrom(lua, "local n = 1\n", true)); + + UNSCOPED_INFO("compiled here: told, transformed or not"); + CHECK(ALScriptEnvelope::comparable(made, false, true, true)); + UNSCOPED_INFO("elsewhere, untransformed: told"); + CHECK(ALScriptEnvelope::comparable(made, false, false, false)); + UNSCOPED_INFO("elsewhere, transformed: not"); + CHECK_FALSE(ALScriptEnvelope::comparable(made, false, false, true)); + UNSCOPED_INFO("the time asked for: not"); + CHECK_FALSE(ALScriptEnvelope::comparable("string t = __TIME__;", false, true, false)); + UNSCOPED_INFO("who compiled it: not"); + CHECK_FALSE(ALScriptEnvelope::comparable("local a = __AGENTNAME__", true, true, false)); + UNSCOPED_INFO("named in a comment only: told"); + CHECK(ALScriptEnvelope::comparable("// built __DATE__\ninteger x;", false, true, false)); } - template<> template<> - void alscriptenvelope_object::test<8>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: codeLine counts the envelope's own lines before the code", "[alscriptenvelope]") { - set_test_name("the line the code begins on in the asset, as the region counts a line it names: the envelope's own before it"); for (const bool lua : { true, false }) { ALScriptEnvelope envelope; @@ -303,15 +313,15 @@ namespace tut from = nl + 1; } const int at = envelope.codeLine(); - ensure_equals(std::string(lua ? "SLua" : "LSL") + ": the code's first line there", lines[static_cast(at)], std::string("first")); - ensure_equals(std::string(lua ? "SLua" : "LSL") + ": after the source's lines and the envelope's own", at, 9); + CAPTURE(lua); + REQUIRE(lines[static_cast(at)] == std::string("first")); + UNSCOPED_INFO("after the source's lines and the envelope's own"); + REQUIRE(at == 9); } } - template<> template<> - void alscriptenvelope_object::test<9>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: wrappedSize is the size of the asset wrap makes", "[alscriptenvelope]") { - set_test_name("how long an envelope's asset is, worked out without making it, is how long the asset made is"); const char* sources[] = { "", "default { }\n", "a /* b */ c // d\n/*|*/ **// x", "local s = [[x]] .. [==[y]==]\n", "]]=]==]" }; for (const bool lua : { false, true }) { @@ -326,90 +336,109 @@ namespace tut envelope.compileTarget = target; envelope.programVersion = "Alchemy Test 1.2.3"; envelope.lastCompiled = "2026-09-27T12:00:00Z"; - ensure_equals(std::string(lua ? "SLua " : "LSL ") + "'" + source + "' for '" + target + "'", envelope.wrappedSize(), + CAPTURE(lua, source, target); + REQUIRE(envelope.wrappedSize() == envelope.wrap().size()); } } } } - template<> template<> - void alscriptenvelope_object::test<10>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: a header goes between the target line and the code and parses back out", "[alscriptenvelope]") { - set_test_name("a header goes between the target line and the code, and comes back out of it, the code alone left"); const ALScriptEnvelope lsl = alscriptenvelope_data::made(false, true); const std::string asset = lsl.wrap(); - ensure_equals("byte for byte", - asset, + UNSCOPED_INFO("byte for byte"); + CHECK(asset == std::string("//start_unprocessed_text\n/*") + ALScriptEnvelope::encodeSource(lsl.source) + "*/\n//end_unprocessed_text\n//nfo_preprocessor_version 0\n//program_version Alchemy\n//last_compiled today\n//mono\n" + lsl.header + lsl.expanded); for (const bool lua : { false, true }) { - const std::string name = lua ? "SLua: " : "LSL: "; + CAPTURE(lua); const ALScriptEnvelope written = alscriptenvelope_data::made(lua, true); std::optional back = ALScriptEnvelope::parse(written.wrap()); - ensure(name + "reads back", back.has_value()); - ensure_equals(name + "the header", back->header, written.header); - ensure_equals(name + "the code alone", back->expanded, written.expanded); - ensure_equals(name + "the source", back->source, written.source); - ensure_equals(name + "the target", back->compileTarget, written.compileTarget); - ensure_equals(name + "written again, the same", back->wrap(), written.wrap()); - ensure_equals(name + "its size worked out", written.wrappedSize(), written.wrap().size()); - ensure_equals(name + "and from the fields", - ALScriptEnvelope::wrappedSize(lua, written.source, written.expanded, written.compileTarget, written.programVersion, - written.lastCompiled, written.header), + REQUIRE(back.has_value()); + REQUIRE(back->header == written.header); + UNSCOPED_INFO("the code alone"); + REQUIRE(back->expanded == written.expanded); + REQUIRE(back->source == written.source); + REQUIRE(back->compileTarget == written.compileTarget); + UNSCOPED_INFO("written again, the same"); + REQUIRE(back->wrap() == written.wrap()); + UNSCOPED_INFO("its size worked out"); + REQUIRE(written.wrappedSize() == written.wrap().size()); + UNSCOPED_INFO("and from the fields"); + REQUIRE(ALScriptEnvelope::wrappedSize(lua, written.source, written.expanded, written.compileTarget, written.programVersion, + written.lastCompiled, written.header) == written.wrap().size()); // Without one, as it was: nothing taken, and Firestorm's bytes // with nothing added. const ALScriptEnvelope plain = alscriptenvelope_data::made(lua, false); back = ALScriptEnvelope::parse(plain.wrap()); - ensure(name + "none: reads back", back.has_value()); - ensure_equals(name + "none: no header", back->header, std::string()); - ensure_equals(name + "none: the code", back->expanded, plain.expanded); - ensure_equals(name + "none: the header's bytes and no more", plain.wrap().size() + written.header.size(), written.wrap().size()); + REQUIRE(back.has_value()); + REQUIRE(back->header == std::string()); + REQUIRE(back->expanded == plain.expanded); + UNSCOPED_INFO("without one: the header's bytes and no more"); + REQUIRE(plain.wrap().size() + written.header.size() == written.wrap().size()); } // A header that does not end its line gets a newline of its own. ALScriptEnvelope unended = alscriptenvelope_data::made(false, true); unended.header.pop_back(); - ensure("a newline after it", unended.wrap() == alscriptenvelope_data::made(false, true).wrap()); - ensure_equals("and counted", unended.wrappedSize(), unended.wrap().size()); + UNSCOPED_INFO("a newline after it"); + CHECK(unended.wrap() == alscriptenvelope_data::made(false, true).wrap()); + UNSCOPED_INFO("and counted"); + REQUIRE(unended.wrappedSize() == unended.wrap().size()); // With no target line, it follows the date. ALScriptEnvelope untargeted = alscriptenvelope_data::made(false, true); untargeted.compileTarget.clear(); std::optional back = ALScriptEnvelope::parse(untargeted.wrap()); - ensure("untargeted: the header taken", back && back->header == untargeted.header && back->expanded == untargeted.expanded); + { + INFO("untargeted: the header taken"); + REQUIRE(back); + CHECK(back->header == untargeted.header); + CHECK(back->expanded == untargeted.expanded); + } // The plugin's banner, should one stand there, is taken out too. ALScriptEnvelope theirs = alscriptenvelope_data::made(false, false); theirs.header = "// ================ sl-vscode-plugin meta ================\n// @file a.lsl\n" "// =======================================================\n"; back = ALScriptEnvelope::parse(theirs.wrap()); - ensure("the plugin's taken", back && back->header == theirs.header && back->expanded == theirs.expanded); + { + INFO("the plugin's taken"); + REQUIRE(back); + CHECK(back->header == theirs.header); + CHECK(back->expanded == theirs.expanded); + } // Only the first: a second, which the author's code begins with, is // the code's. ALScriptEnvelope twice = alscriptenvelope_data::made(false, true); twice.expanded = twice.header + twice.expanded; back = ALScriptEnvelope::parse(twice.wrap()); - ensure("one header taken", back && back->header == twice.header && back->expanded == twice.expanded); + { + INFO("one header taken"); + REQUIRE(back); + CHECK(back->header == twice.header); + CHECK(back->expanded == twice.expanded); + } } - template<> template<> - void alscriptenvelope_object::test<11>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: a header moves the code's first line by its own lines and nothing else", "[alscriptenvelope]") { - set_test_name("a header moves the line the code begins on by its own lines, and nothing else"); for (const bool lua : { false, true }) { - const std::string name = lua ? "SLua: " : "LSL: "; + CAPTURE(lua); const ALScriptEnvelope without = alscriptenvelope_data::made(lua, false); const ALScriptEnvelope with = alscriptenvelope_data::made(lua, true); const int lines = static_cast(std::count(with.header.begin(), with.header.end(), '\n')); - ensure_equals(name + "the header's lines", lines, 5); - ensure_equals(name + "the code begins that much later", with.codeLine(), without.codeLine() + lines); + REQUIRE(lines == 5); + UNSCOPED_INFO("the code begins that much later"); + REQUIRE(with.codeLine() == without.codeLine() + lines); // The asset's line codeLine() + n is the code's line n either // way, and the code's lines are the ones the source map is of. @@ -418,23 +447,25 @@ namespace tut const std::vector code = alscriptenvelope_data::linesOf(with.expanded); for (size_t i = 0; i < code.size(); ++i) { - ensure_equals(name + "code line " + std::to_string(i) + " without", plain[static_cast(without.codeLine()) + i], code[i]); - ensure_equals(name + "code line " + std::to_string(i) + " with", headed[static_cast(with.codeLine()) + i], code[i]); + CAPTURE(i); + UNSCOPED_INFO("without a header"); + REQUIRE(plain[static_cast(without.codeLine()) + i] == code[i]); + UNSCOPED_INFO("with a header"); + REQUIRE(headed[static_cast(with.codeLine()) + i] == code[i]); } // And read back from the asset, as a save hears of it. const std::optional back = ALScriptEnvelope::parse(with.wrap()); - ensure(name + "reads back", back.has_value()); - ensure_equals(name + "read back, the same line", back->codeLine(), with.codeLine()); + REQUIRE(back.has_value()); + UNSCOPED_INFO("read back, the same line"); + REQUIRE(back->codeLine() == with.codeLine()); } } - template<> template<> - void alscriptenvelope_object::test<12>() + TEST_CASE_METHOD(alscriptenvelope_data, "alscriptenvelope: a header fools neither compiledFrom nor the check that the code was edited", "[alscriptenvelope]") { - set_test_name("a header fools neither compiledFrom nor the check that the code was edited"); for (const bool lua : { false, true }) { - const std::string name = lua ? "SLua: " : "LSL: "; + CAPTURE(lua); const ALScriptEnvelope sent = alscriptenvelope_data::made(lua, true); ALScriptEnvelope later = alscriptenvelope_data::made(lua, true); ALUploadHeader again = *ALUploadHeader::parse(later.header, lua); @@ -443,18 +474,25 @@ namespace tut const std::optional held = ALScriptEnvelope::parse(sent.wrap()); const std::optional came = ALScriptEnvelope::parse(later.wrap()); - ensure(name + "both read", held && came); - ensure(name + "the headers apart", held->header != came->header); + REQUIRE(held); + REQUIRE(came); + UNSCOPED_INFO("the headers apart"); + REQUIRE(held->header != came->header); // What the bridge holds up to tell an edit of the compiled half: // the code the same, whenever it went up. - ensure(name + "the code the same", held->expanded == came->expanded); - ensure(name + "compiled from the expansion", ALScriptEnvelope::compiledFrom(sent.expanded, came->expanded, lua)); + UNSCOPED_INFO("the code the same"); + REQUIRE(held->expanded == came->expanded); + UNSCOPED_INFO("compiled from the expansion"); + REQUIRE(ALScriptEnvelope::compiledFrom(sent.expanded, came->expanded, lua)); // Even left in, a header is comments, which it passes over. - ensure(name + "a header in the code passed over", ALScriptEnvelope::compiledFrom(sent.expanded, sent.header + sent.expanded, lua)); - ensure(name + "the source told as it was", ALScriptEnvelope::comparable(came->source, lua, true, false)); + UNSCOPED_INFO("a header in the code passed over"); + REQUIRE(ALScriptEnvelope::compiledFrom(sent.expanded, sent.header + sent.expanded, lua)); + UNSCOPED_INFO("the source told as it was"); + REQUIRE(ALScriptEnvelope::comparable(came->source, lua, true, false)); // The hash is of what was sent, the header aside: worked out // again from the halves read back, it is what the header says. - ensure_equals(name + "the hash again", ALUploadHeader::hashOf(came->compileTarget, came->source, came->expanded), + UNSCOPED_INFO("the hash again"); + REQUIRE(ALUploadHeader::hashOf(came->compileTarget, came->source, came->expanded) == ALUploadHeader::parse(came->header, lua)->hash); } } diff --git a/indra/alscript/tests/alscriptfixes_test.cpp b/indra/alscript/tests/alscriptfixes_test.cpp index 7cccc172b4..4ad1108daa 100644 --- a/indra/alscript/tests/alscriptfixes_test.cpp +++ b/indra/alscript/tests/alscriptfixes_test.cpp @@ -35,13 +35,13 @@ #include "../preprocessor/alpreprocessor.h" #include "../lsl/optimizer/allsloptimizer.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { struct alscriptfixes_data { @@ -105,29 +105,34 @@ namespace tut // The problem keyed so, its preferred fix, the text with it made, // and that text checked again: the problem gone, and no error in // its place. - std::string fixed(const std::string& script, bool lua, const std::string& key, const std::string& title) + std::string require_fixed(const std::string& script, bool lua, const std::string& key, const std::string& title) { const ALScriptProblems problems = check(script, lua); const ALScriptProblem* problem = keyed(problems, key); - ensure("said: " + key + "\n" + said(problems), problem != nullptr); - ensure("a fix: " + said(problems), !problem->fixes.empty()); + CAPTURE(key); + INFO("said:\n" + said(problems)); + REQUIRE(problem != nullptr); + REQUIRE_FALSE(problem->fixes.empty()); const ALScriptFix& fix = problem->fixes.front(); - ensure("preferred", fix.preferred); - ensure_equals("its words", fix.title, title); + REQUIRE(fix.preferred); + REQUIRE(fix.title == title); const std::optional made = ALScriptFixes::apply(script, fix); - ensure("applies", made.has_value()); + REQUIRE(made.has_value()); const ALScriptProblems after = check(*made, lua); - ensure("gone from:\n" + *made + "\n" + said(after), keyed(after, key) == nullptr); - ensure("no error in its place:\n" + *made + "\n" + said(after), errors(after) <= errors(problems) - (problem->severity == ALScriptProblem::Severity::Error ? 1 : 0)); + INFO("made:\n" + *made + "\nsaid then:\n" + said(after)); + REQUIRE(keyed(after, key) == nullptr); + UNSCOPED_INFO("no error in its place"); + REQUIRE(errors(after) <= errors(problems) - (problem->severity == ALScriptProblem::Severity::Error ? 1 : 0)); return *made; } // Where a piece of the script is, zero-based: its first line and // column, and where it ends on that line. - static void place(const std::string& script, const std::string& piece, S32& line, S32& column, S32& end) + static void require_place(const std::string& script, const std::string& piece, S32& line, S32& column, S32& end) { const size_t at = script.find(piece); - ensure("in the script: " + piece, at != std::string::npos); + CAPTURE(piece); + REQUIRE(at != std::string::npos); const size_t start = script.rfind('\n', at); line = static_cast(std::count(script.begin(), script.begin() + at, '\n')); column = static_cast(at - (start == std::string::npos ? 0 : start + 1)); @@ -152,7 +157,7 @@ namespace tut // The text with the refactor titled so made, among those offered at // a place, and that text checked again: no error in it. Where none // is titled so, what was offered. - std::string acted(const std::string& script, bool lua, S32 line, S32 column, S32 endLine, S32 endColumn, const std::string& title) + std::string require_acted(const std::string& script, bool lua, S32 line, S32 column, S32 endLine, S32 endColumn, const std::string& title) { const std::vector offered = actions(script, lua, line, column, endLine, endColumn); for (const ALScriptFix& action : offered) @@ -161,11 +166,13 @@ namespace tut { continue; } - ensure("a refactor", action.kind == ALScriptFix::Kind::Refactor); + CAPTURE(title); + REQUIRE(action.kind == ALScriptFix::Kind::Refactor); const std::optional made = ALScriptFixes::apply(script, action); - ensure("applies", made.has_value()); + REQUIRE(made.has_value()); const ALScriptProblems after = check(*made, lua); - ensure("no error in:\n" + *made + "\n" + said(after), errors(after) == 0); + INFO("made:\n" + *made + "\nsaid then:\n" + said(after)); + REQUIRE(errors(after) == 0); return *made; } return "offered: " + titles(offered); @@ -173,80 +180,84 @@ namespace tut // The same with the caret, or the stretch chosen, on a piece of the // script. - std::string actedOn(const std::string& script, bool lua, const std::string& piece, bool chosen, const std::string& title) + std::string require_acted_on(const std::string& script, bool lua, const std::string& piece, bool chosen, const std::string& title) { S32 line = 0, column = 0, end = 0; - place(script, piece, line, column, end); - return acted(script, lua, line, column, line, chosen ? end : column, title); + require_place(script, piece, line, column, end); + return require_acted(script, lua, line, column, line, chosen ? end : column, title); } }; - typedef test_group alscriptfixes_group; - typedef alscriptfixes_group::object object; - tut::alscriptfixes_group alscriptfixes_test("ALScriptFixes"); - template<> template<> - void object::test<1>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a missing semicolon is put where it is missing", "[ALScriptFixes]") { - set_test_name("a missing semicolon is put where it is missing"); - ensure("builtins: " + error, lslLoaded); - const std::string made = fixed("default\n{\n state_entry()\n {\n integer a = 1\n llOwnerSay((string)a);\n }\n}\n", false, + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + const std::string made = require_fixed("default\n{\n state_entry()\n {\n integer a = 1\n llOwnerSay((string)a);\n }\n}\n", false, "LSLSyntaxMissing", "Insert ';'"); - ensure("after the 1: " + made, made.find("integer a = 1;\n") != std::string::npos); + UNSCOPED_INFO("after the 1: " + made); + REQUIRE(made.find("integer a = 1;\n") != std::string::npos); } - template<> template<> - void object::test<2>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an LSL name spelt wrong becomes the nearest in scope or builtin", "[ALScriptFixes]") { - set_test_name("an LSL name spelt wrong is changed to the nearest in scope or among the builtins"); - ensure("builtins: " + error, lslLoaded); - std::string made = fixed("default\n{\n state_entry()\n {\n llownersay(\"hi\");\n }\n}\n", false, "LSLUndeclared", + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + std::string made = require_fixed("default\n{\n state_entry()\n {\n llownersay(\"hi\");\n }\n}\n", false, "LSLUndeclared", "Change 'llownersay' to 'llOwnerSay'"); - ensure("the call: " + made, made.find("llOwnerSay(\"hi\")") != std::string::npos); - made = fixed("default\n{\n state_entry()\n {\n integer count = 1;\n llOwnerSay((string)coutn);\n }\n}\n", false, + UNSCOPED_INFO("the call: " + made); + REQUIRE(made.find("llOwnerSay(\"hi\")") != std::string::npos); + made = require_fixed("default\n{\n state_entry()\n {\n integer count = 1;\n llOwnerSay((string)coutn);\n }\n}\n", false, "LSLUndeclared", "Change 'coutn' to 'count'"); - ensure("the local: " + made, made.find("(string)count)") != std::string::npos); + UNSCOPED_INFO("the local: " + made); + REQUIRE(made.find("(string)count)") != std::string::npos); // A local declared after the place is not in scope there, and not // offered. const ALScriptProblems problems = check("default\n{\n state_entry()\n {\n llOwnerSay((string)coutn);\n integer count = 1;\n }\n}\n", false); const ALScriptProblem* later = keyed(problems, "LSLUndeclared"); - ensure("said: " + said(problems), later != nullptr); - ensure("not a name declared after: " + said(problems), later->fixes.empty() || later->fixes.front().args[1] != "count"); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(later != nullptr); + UNSCOPED_INFO("not a name declared after: " + said(problems)); + REQUIRE((later->fixes.empty() || later->fixes.front().args[1] != "count")); } - template<> template<> - void object::test<3>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a deprecated LSL function is replaced only where it has one replacement", "[ALScriptFixes]") { - set_test_name("a deprecated LSL function with one replacement is replaced, and one with several is not"); - ensure("builtins: " + error, lslLoaded); - const std::string made = fixed("default\n{\n state_entry()\n {\n llSoundPreload(\"boom\");\n }\n}\n", false, + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + const std::string made = require_fixed("default\n{\n state_entry()\n {\n llSoundPreload(\"boom\");\n }\n}\n", false, "LSLDeprecatedWithReplacement", "Use 'llPreloadSound' instead of 'llSoundPreload'"); - ensure("the call: " + made, made.find("llPreloadSound(\"boom\");") != std::string::npos); + UNSCOPED_INFO("the call: " + made); + REQUIRE(made.find("llPreloadSound(\"boom\");") != std::string::npos); const ALScriptProblems problems = check("default\n{\n state_entry()\n {\n llSound(\"boom\", 1.0, TRUE, FALSE);\n }\n}\n", false); const ALScriptProblem* several = keyed(problems, "LSLDeprecatedWithReplacement"); - ensure("said: " + said(problems), several != nullptr); - ensure("no fix among several: " + said(problems), several->fixes.empty()); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(several != nullptr); + UNSCOPED_INFO("no fix among several: " + said(problems)); + REQUIRE(several->fixes.empty()); } - template<> template<> - void object::test<4>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an unused SLua local is marked unused safely", "[ALScriptFixes]") { - set_test_name("an unused SLua local is marked unused, safely"); - ensure("definitions: " + error, luauLoaded); - const std::string made = fixed("local unused = 1\n", true, "LuauLintLocalUnused", "Rename to '_unused'"); - ensure_equals("marked", made, std::string("local _unused = 1\n")); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + const std::string made = require_fixed("local unused = 1\n", true, "LuauLintLocalUnused", "Rename to '_unused'"); + CHECK(made == std::string("local _unused = 1\n")); const ALScriptProblems problems = check("local function helper() end\n", true); const ALScriptProblem* problem = keyed(problems, "LuauLintFunctionUnused"); - ensure("said: " + said(problems), problem != nullptr && !problem->fixes.empty()); - ensure("safe", problem->fixes.front().safe); + { + INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + REQUIRE_FALSE(problem->fixes.empty()); + } + CHECK(problem->fixes.front().safe); } - template<> template<> - void object::test<5>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an SLua key spelt wrong is changed to the nearest there is", "[ALScriptFixes]") { - set_test_name("an SLua key spelt wrong is changed to the nearest there is"); - ensure("definitions: " + error, luauLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); if (newSolver) { // Whose nonstrict mode says nothing of a key not there. @@ -254,36 +265,14 @@ namespace tut config.mode = "strict"; luau.setConfig(config); } - const std::string made = fixed("ll.OwnerSya(\"hi\")\n", true, "LuauKeyNotFoundDidYouMean", "Change 'OwnerSya' to 'OwnerSay'"); - ensure_equals("the call", made, std::string("ll.OwnerSay(\"hi\")\n")); - } - - template<> template<> - void object::test<10>() - { - set_test_name("an SLua global spelt wrong is changed to the nearest name in scope"); - ensure("definitions: " + error, luauLoaded); - const ALScriptProblems problems = check("pirnt(\"hi\")\n", true); - const ALScriptProblem* problem = nullptr; - for (const ALScriptProblem& each : problems) - { - if (each.key.find("UnknownGlobal") != std::string::npos) - { - problem = &each; - } - } - ensure("said: " + said(problems), problem != nullptr); - const std::string made = fixed("pirnt(\"hi\")\n", true, problem->key, "Change 'pirnt' to 'print'"); - ensure_equals("the call", made, std::string("print(\"hi\")\n")); - const std::string local = fixed("local count = 1\nprint(coutn)\n", true, problem->key, "Change 'coutn' to 'count'"); - ensure_equals("a local", local, std::string("local count = 1\nprint(count)\n")); + const std::string made = require_fixed("ll.OwnerSya(\"hi\")\n", true, "LuauKeyNotFoundDidYouMean", "Change 'OwnerSya' to 'OwnerSay'"); + CHECK(made == std::string("ll.OwnerSay(\"hi\")\n")); } - template<> template<> - void object::test<6>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a deprecated SLua call is replaced as the definitions say; never safe", "[ALScriptFixes]") { - set_test_name("a deprecated SLua call is replaced by what the definitions say to use, never as safe"); - ensure("definitions: " + error, luauLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); const ALScriptProblems problems = check("local n = ll.Abs(-1)\nprint(n)\n", true); const ALScriptProblem* problem = nullptr; for (const ALScriptProblem& each : problems) @@ -293,186 +282,258 @@ namespace tut problem = &each; } } - ensure("said: " + said(problems), problem != nullptr); - ensure("a fix: " + said(problems), !problem->fixes.empty()); - ensure("not safe", !problem->fixes.front().safe); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + UNSCOPED_INFO("a fix: " + said(problems)); + REQUIRE_FALSE(problem->fixes.empty()); + CHECK_FALSE(problem->fixes.front().safe); const std::optional made = ALScriptFixes::apply("local n = ll.Abs(-1)\nprint(n)\n", problem->fixes.front()); - ensure("applies", made.has_value()); + REQUIRE(made.has_value()); const ALScriptProblems after = check(*made, true); + INFO("made:\n" + *made + "\nsaid then:\n" + said(after)); for (const ALScriptProblem& each : after) { - ensure("no longer deprecated:\n" + *made + "\n" + said(after), each.code != "DeprecatedApi"); + REQUIRE(each.code != "DeprecatedApi"); } - ensure_equals("no errors:\n" + *made + "\n" + said(after), errors(after), static_cast(0)); + CHECK(errors(after) == static_cast(0)); } - template<> template<> - void object::test<7>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: edits are made in the text's order and overlapping ones are refused", "[ALScriptFixes]") { - set_test_name("edits are made in the text's order, and overlapping ones are refused"); ALScriptFix fix; fix.edits.push_back({ 1, 0, 1, 3, "BAR" }); fix.edits.push_back({ 0, 0, 0, 0, ">" }); - ensure_equals("both", ALScriptFixes::apply("foo\nbar\n", fix).value_or("refused"), std::string(">foo\nBAR\n")); + CHECK(ALScriptFixes::apply("foo\nbar\n", fix).value_or("refused") == std::string(">foo\nBAR\n")); fix.edits.push_back({ 1, 2, 1, 3, "x" }); - ensure("overlap refused", !ALScriptFixes::apply("foo\nbar\n", fix).has_value()); + UNSCOPED_INFO("overlap refused"); + REQUIRE_FALSE(ALScriptFixes::apply("foo\nbar\n", fix).has_value()); ALScriptFix past; past.edits.push_back({ 0, 4, 0, 4, "!" }); - ensure("past the line's end refused", !ALScriptFixes::apply("foo\nbar\n", past).has_value()); + UNSCOPED_INFO("past the line's end refused"); + REQUIRE_FALSE(ALScriptFixes::apply("foo\nbar\n", past).has_value()); ALScriptFix at_end; at_end.edits.push_back({ 0, 3, 0, 3, ";" }); - ensure_equals("at the line's end", ALScriptFixes::apply("foo\nbar\n", at_end).value_or("refused"), std::string("foo;\nbar\n")); + UNSCOPED_INFO("at the line's end"); + CHECK(ALScriptFixes::apply("foo\nbar\n", at_end).value_or("refused") == std::string("foo;\nbar\n")); } - template<> template<> - void object::test<8>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: NOLINT and NOLINTNEXTLINE suppress named or all lints; never a type error", "[ALScriptFixes]") { - set_test_name("NOLINT and NOLINTNEXTLINE suppress lints by name, bare or listed, and never an error"); ALScriptProblem lint; lint.severity = ALScriptProblem::Severity::Warning; lint.source = ALScriptProblem::Source::Lint; lint.code = "LocalUnused"; - ensure("named", ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT(LocalUnused)", "", true)); - ensure("among others", ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT(ShadowLocal, LocalUnused)", "", true)); - ensure("bare", ALScriptFixes::suppressed(lint, "local x = 1 --[[ NOLINT ]]", "", true)); - ensure("another name", !ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT(ShadowLocal)", "", true)); - ensure("the line before", ALScriptFixes::suppressed(lint, "local x = 1", "-- NOLINTNEXTLINE(LocalUnused)", true)); - ensure("NOLINT on the line before is not about this one", !ALScriptFixes::suppressed(lint, "local x = 1", "-- NOLINT(LocalUnused)", true)); - ensure("not in a string", !ALScriptFixes::suppressed(lint, "local x = \"-- NOLINT\"", "", true)); - ensure("not in a long string", !ALScriptFixes::suppressed(lint, "local x = [[ -- NOLINT ]]", "", true)); - ensure("not a longer word", !ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINTING", "", true)); + UNSCOPED_INFO("named"); + CHECK(ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT(LocalUnused)", "", true)); + UNSCOPED_INFO("among others"); + CHECK(ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT(ShadowLocal, LocalUnused)", "", true)); + UNSCOPED_INFO("bare"); + CHECK(ALScriptFixes::suppressed(lint, "local x = 1 --[[ NOLINT ]]", "", true)); + UNSCOPED_INFO("another name"); + CHECK_FALSE(ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT(ShadowLocal)", "", true)); + UNSCOPED_INFO("the line before"); + CHECK(ALScriptFixes::suppressed(lint, "local x = 1", "-- NOLINTNEXTLINE(LocalUnused)", true)); + UNSCOPED_INFO("NOLINT on the line before is not about this one"); + CHECK_FALSE(ALScriptFixes::suppressed(lint, "local x = 1", "-- NOLINT(LocalUnused)", true)); + UNSCOPED_INFO("not in a string"); + CHECK_FALSE(ALScriptFixes::suppressed(lint, "local x = \"-- NOLINT\"", "", true)); + UNSCOPED_INFO("not in a long string"); + CHECK_FALSE(ALScriptFixes::suppressed(lint, "local x = [[ -- NOLINT ]]", "", true)); + UNSCOPED_INFO("not a longer word"); + CHECK_FALSE(ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINTING", "", true)); lint.severity = ALScriptProblem::Severity::Error; - ensure("a lint made an error, still", ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT", "", true)); + UNSCOPED_INFO("a lint made an error, still"); + CHECK(ALScriptFixes::suppressed(lint, "local x = 1 -- NOLINT", "", true)); ALScriptProblem type; type.severity = ALScriptProblem::Severity::Error; type.source = ALScriptProblem::Source::Types; - ensure("a type error, never", !ALScriptFixes::suppressed(type, "local x: number = \"a\" -- NOLINT", "", true)); + UNSCOPED_INFO("a type error, never"); + CHECK_FALSE(ALScriptFixes::suppressed(type, "local x: number = \"a\" -- NOLINT", "", true)); ALScriptProblem lsl; lsl.severity = ALScriptProblem::Severity::Warning; lsl.source = ALScriptProblem::Source::Lint; lsl.code = "20009"; lsl.key = "LSLDeclaredButNotUsed"; - ensure_equals("its name", ALScriptFixes::lintName(lsl, false), std::string("DeclaredButNotUsed")); - ensure("by name", ALScriptFixes::suppressed(lsl, "integer x; // NOLINT(DeclaredButNotUsed)", "", false)); - ensure("by number", ALScriptFixes::suppressed(lsl, "integer x; /* NOLINT(20009) */", "", false)); - ensure("not in a string", !ALScriptFixes::suppressed(lsl, "string s = \"// NOLINT\";", "", false)); + UNSCOPED_INFO("its name"); + CHECK(ALScriptFixes::lintName(lsl, false) == std::string("DeclaredButNotUsed")); + UNSCOPED_INFO("by name"); + CHECK(ALScriptFixes::suppressed(lsl, "integer x; // NOLINT(DeclaredButNotUsed)", "", false)); + UNSCOPED_INFO("by number"); + CHECK(ALScriptFixes::suppressed(lsl, "integer x; /* NOLINT(20009) */", "", false)); + UNSCOPED_INFO("not in a string"); + CHECK_FALSE(ALScriptFixes::suppressed(lsl, "string s = \"// NOLINT\";", "", false)); } - template<> template<> - void object::test<9>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a suppression joins a NOLINT or the line's comment or makes its own", "[ALScriptFixes]") { - set_test_name("the suppression goes in a NOLINT already there, the comment the line ends in, or one of its own"); ALScriptProblem lint; lint.severity = ALScriptProblem::Severity::Warning; lint.source = ALScriptProblem::Source::Lint; lint.code = "LocalUnused"; - auto made = [&lint](const std::string& line, bool lua) { + auto require_suppression = [&lint](const std::string& line, bool lua) { const std::optional fix = ALScriptFixes::suppression(lint, line, lua); - ensure("a fix for: " + line, fix.has_value()); - ensure("a suppression", fix->kind == ALScriptFix::Kind::Suppress && !fix->preferred && fix->safe); + CAPTURE(line); + REQUIRE(fix.has_value()); + REQUIRE(fix->kind == ALScriptFix::Kind::Suppress); + REQUIRE_FALSE(fix->preferred); + REQUIRE(fix->safe); const std::string after = ALScriptFixes::apply(line, *fix).value_or("refused"); - ensure("suppresses: " + after, ALScriptFixes::suppressed(lint, after, "", lua)); + UNSCOPED_INFO("suppresses: " + after); + REQUIRE(ALScriptFixes::suppressed(lint, after, "", lua)); return after; }; - ensure_equals("its own", made("local x = 1", true), std::string("local x = 1 -- NOLINT(LocalUnused)")); - ensure_equals("the line's comment", made("local x = 1 -- why", true), std::string("local x = 1 -- why NOLINT(LocalUnused)")); - ensure_equals("a NOLINT's list", made("local x = 1 -- NOLINT(ShadowLocal)", true), std::string("local x = 1 -- NOLINT(ShadowLocal, LocalUnused)")); - ensure_equals("after a block comment", made("local x = 1 --[[ why ]]", true), std::string("local x = 1 --[[ why ]] -- NOLINT(LocalUnused)")); - ensure("nothing where a bare NOLINT says it", !ALScriptFixes::suppression(lint, "local x = 1 -- NOLINT", true).has_value()); + UNSCOPED_INFO("its own"); + CHECK(require_suppression("local x = 1", true) == std::string("local x = 1 -- NOLINT(LocalUnused)")); + UNSCOPED_INFO("the line's comment"); + CHECK(require_suppression("local x = 1 -- why", true) == std::string("local x = 1 -- why NOLINT(LocalUnused)")); + UNSCOPED_INFO("a NOLINT's list"); + CHECK(require_suppression("local x = 1 -- NOLINT(ShadowLocal)", true) == std::string("local x = 1 -- NOLINT(ShadowLocal, LocalUnused)")); + UNSCOPED_INFO("after a block comment"); + CHECK(require_suppression("local x = 1 --[[ why ]]", true) == std::string("local x = 1 --[[ why ]] -- NOLINT(LocalUnused)")); + UNSCOPED_INFO("nothing where a bare NOLINT says it"); + REQUIRE_FALSE(ALScriptFixes::suppression(lint, "local x = 1 -- NOLINT", true).has_value()); lint.code = "20009"; lint.key = "LSLDeclaredButNotUsed"; - ensure_equals("LSL", made(" integer x;", false), std::string(" integer x; // NOLINT(DeclaredButNotUsed)")); + UNSCOPED_INFO("LSL"); + CHECK(require_suppression(" integer x;", false) == std::string(" integer x; // NOLINT(DeclaredButNotUsed)")); + } + + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an SLua global spelt wrong is changed to the nearest name in scope", "[ALScriptFixes]") + { + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + const ALScriptProblems problems = check("pirnt(\"hi\")\n", true); + const ALScriptProblem* problem = nullptr; + for (const ALScriptProblem& each : problems) + { + if (each.key.find("UnknownGlobal") != std::string::npos) + { + problem = &each; + } + } + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + const std::string made = require_fixed("pirnt(\"hi\")\n", true, problem->key, "Change 'pirnt' to 'print'"); + CHECK(made == std::string("print(\"hi\")\n")); + const std::string local = require_fixed("local count = 1\nprint(coutn)\n", true, problem->key, "Change 'coutn' to 'count'"); + CHECK(local == std::string("local count = 1\nprint(count)\n")); } - template<> template<> - void object::test<11>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an LSL value of the wrong type is cast where LSL has the cast", "[ALScriptFixes]") { - set_test_name("an LSL value of the wrong type is cast to the right one, where LSL casts it"); - ensure("builtins: " + error, lslLoaded); - std::string made = fixed("default\n{\n state_entry()\n {\n llOwnerSay(5 + 1);\n }\n}\n", false, "LSLArgumentWrongType", + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + std::string made = require_fixed("default\n{\n state_entry()\n {\n llOwnerSay(5 + 1);\n }\n}\n", false, "LSLArgumentWrongType", "Cast to string"); - ensure("the argument: " + made, made.find("llOwnerSay((string)(5 + 1));") != std::string::npos); - made = fixed("default\n{\n state_entry()\n {\n llSetText(\"a\", <1, 1, 1>, \"1\");\n }\n}\n", false, "LSLArgumentWrongType", + UNSCOPED_INFO("the argument: " + made); + REQUIRE(made.find("llOwnerSay((string)(5 + 1));") != std::string::npos); + made = require_fixed("default\n{\n state_entry()\n {\n llSetText(\"a\", <1, 1, 1>, \"1\");\n }\n}\n", false, "LSLArgumentWrongType", "Cast to float"); - ensure("the third, past a vector's commas: " + made, made.find("<1, 1, 1>, (float)\"1\")") != std::string::npos); - made = fixed("default\n{\n state_entry()\n {\n string s = 5;\n llOwnerSay(s);\n }\n}\n", false, + UNSCOPED_INFO("the third, past a vector's commas: " + made); + REQUIRE(made.find("<1, 1, 1>, (float)\"1\")") != std::string::npos); + made = require_fixed("default\n{\n state_entry()\n {\n string s = 5;\n llOwnerSay(s);\n }\n}\n", false, "LSLWrongTypeInAssignment", "Cast to string"); - ensure("the declaration: " + made, made.find("string s = (string)5;") != std::string::npos); - made = fixed("default\n{\n state_entry()\n {\n string s;\n s = 6;\n llOwnerSay(s);\n }\n}\n", false, + UNSCOPED_INFO("the declaration: " + made); + REQUIRE(made.find("string s = (string)5;") != std::string::npos); + made = require_fixed("default\n{\n state_entry()\n {\n string s;\n s = 6;\n llOwnerSay(s);\n }\n}\n", false, "LSLInvalidOperator", "Cast to string"); - ensure("the assignment: " + made, made.find("s = (string)6;") != std::string::npos); - made = fixed("string f()\n{\n return 5;\n}\ndefault\n{\n state_entry()\n {\n llOwnerSay(f());\n }\n}\n", false, + UNSCOPED_INFO("the assignment: " + made); + REQUIRE(made.find("s = (string)6;") != std::string::npos); + made = require_fixed("string f()\n{\n return 5;\n}\ndefault\n{\n state_entry()\n {\n llOwnerSay(f());\n }\n}\n", false, "LSLBadReturnType", "Cast to string"); - ensure("the return: " + made, made.find("return (string)5;") != std::string::npos); + UNSCOPED_INFO("the return: " + made); + REQUIRE(made.find("return (string)5;") != std::string::npos); // No cast LSL has: a list is no vector. const ALScriptProblems problems = check("default\n{\n state_entry()\n {\n llSetPos([1]);\n }\n}\n", false); const ALScriptProblem* none = keyed(problems, "LSLArgumentWrongType"); - ensure("said: " + said(problems), none != nullptr && none->fixes.empty()); + { + INFO("said: " + said(problems)); + REQUIRE(none != nullptr); + REQUIRE(none->fixes.empty()); + } } - template<> template<> - void object::test<12>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: the LSL warnings that say what to write are written so", "[ALScriptFixes]") { - set_test_name("the LSL warnings that say what to write are written so"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string head = "default\n{\n state_entry()\n {\n integer a = 2;\n"; const std::string tail = " llOwnerSay((string)a);\n }\n}\n"; - std::string made = fixed(head + " a == 3;\n" + tail, false, "LSLEqAsStatement", "Assign with '='"); - ensure("assigned: " + made, made.find(" a = 3;\n") != std::string::npos); - made = fixed(head + " a *= 1.5;\n" + tail, false, "LSLIntFloatMulAssign", "Multiply as a float, then cast to integer"); - ensure("rewritten: " + made, made.find("a = (integer)(a * 1.5);") != std::string::npos); - made = fixed(head + " state default;\n" + tail, false, "LSLChangeToCurrentState", "Write 'return' instead"); - ensure("returned: " + made, made.find(" return;\n") != std::string::npos); + std::string made = require_fixed(head + " a == 3;\n" + tail, false, "LSLEqAsStatement", "Assign with '='"); + UNSCOPED_INFO("assigned: " + made); + REQUIRE(made.find(" a = 3;\n") != std::string::npos); + made = require_fixed(head + " a *= 1.5;\n" + tail, false, "LSLIntFloatMulAssign", "Multiply as a float, then cast to integer"); + UNSCOPED_INFO("rewritten: " + made); + REQUIRE(made.find("a = (integer)(a * 1.5);") != std::string::npos); + made = require_fixed(head + " state default;\n" + tail, false, "LSLChangeToCurrentState", "Write 'return' instead"); + UNSCOPED_INFO("returned: " + made); + REQUIRE(made.find(" return;\n") != std::string::npos); // An assignment used as a condition: either was meant, so neither // is preferred, and the brackets change nothing. const std::string script = head + " if (a = 1) llOwnerSay(\"one\");\n" + tail; const ALScriptProblems problems = check(script, false); const ALScriptProblem* problem = keyed(problems, "LSLAssignmentInComparison"); - ensure("said: " + said(problems), problem != nullptr && problem->fixes.size() >= 2); - ensure("neither preferred", !problem->fixes[0].preferred && !problem->fixes[1].preferred); - ensure_equals("compared", ALScriptFixes::apply(script, problem->fixes[0]).value_or("").find("if (a == 1)") != std::string::npos, true); - ensure("bracketed, safely", problem->fixes[1].safe && ALScriptFixes::apply(script, problem->fixes[1]).value_or("").find("if ((a = 1))") != std::string::npos); + { + INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + REQUIRE(problem->fixes.size() >= 2); + } + REQUIRE_FALSE(problem->fixes[0].preferred); + CHECK_FALSE(problem->fixes[1].preferred); + REQUIRE(ALScriptFixes::apply(script, problem->fixes[0]).value_or("").find("if (a == 1)") != std::string::npos); + REQUIRE(problem->fixes[1].safe); + REQUIRE(ALScriptFixes::apply(script, problem->fixes[1]).value_or("").find("if ((a = 1))") != std::string::npos); const std::optional quiet = ALScriptFixes::apply(script, problem->fixes[1]); - ensure("and quiet after", quiet && keyed(check(*quiet, false), "LSLAssignmentInComparison") == nullptr); + { + INFO("and quiet after"); + REQUIRE(quiet); + REQUIRE(keyed(check(*quiet, false), "LSLAssignmentInComparison") == nullptr); + } } - template<> template<> - void object::test<13>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: the SLua fixes read off what the checker marks", "[ALScriptFixes]") { - set_test_name("the SLua fixes read off what the checker marks"); - ensure("definitions: " + error, luauLoaded); - std::string made = fixed("--!strict\nlocal n = 5\nlocal s: string = n\nprint(s)\n", true, "LuauTypeMismatch", "Wrap in tostring()"); - ensure("the value a declaration is given: " + made, made.find("local s: string = tostring(n)") != std::string::npos); - made = fixed("--!strict\nlocal t = {}\nfunction t:m() return self end\nprint(t.m())\n", true, "LuauRequiresSelf", "Call with ':'"); - ensure("with a colon: " + made, made.find("print(t:m())") != std::string::npos); - made = fixed("local function f()\n counter = 1\n return counter\nend\nprint(f())\n", true, "LuauLintGlobalUsedAsLocalFunction", + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + std::string made = require_fixed("--!strict\nlocal n = 5\nlocal s: string = n\nprint(s)\n", true, "LuauTypeMismatch", "Wrap in tostring()"); + UNSCOPED_INFO("the value a declaration is given: " + made); + REQUIRE(made.find("local s: string = tostring(n)") != std::string::npos); + made = require_fixed("--!strict\nlocal t = {}\nfunction t:m() return self end\nprint(t.m())\n", true, "LuauRequiresSelf", "Call with ':'"); + UNSCOPED_INFO("with a colon: " + made); + REQUIRE(made.find("print(t:m())") != std::string::npos); + made = require_fixed("local function f()\n counter = 1\n return counter\nend\nprint(f())\n", true, "LuauLintGlobalUsedAsLocalFunction", "Make 'counter' local"); - ensure("local: " + made, made.find(" local counter = 1\n") != std::string::npos); - made = fixed("--!strict\nlocal x\nprint(x)\n", true, "LuauLintUninitializedLocal", "Initialize 'x' with nil"); - ensure("given nil: " + made, made.find("local x = nil\n") != std::string::npos); - made = fixed("--!nonstrickt\nprint(1)\n", true, "LuauLintDirectiveUnknownDidYouMean", "Change 'nonstrickt' to 'nonstrict'"); - ensure("spelt: " + made, made.compare(0, 12, "--!nonstrict") == 0); + UNSCOPED_INFO("local: " + made); + REQUIRE(made.find(" local counter = 1\n") != std::string::npos); + made = require_fixed("--!strict\nlocal x\nprint(x)\n", true, "LuauLintUninitializedLocal", "Initialize 'x' with nil"); + UNSCOPED_INFO("given nil: " + made); + REQUIRE(made.find("local x = nil\n") != std::string::npos); + made = require_fixed("--!nonstrickt\nprint(1)\n", true, "LuauLintDirectiveUnknownDidYouMean", "Change 'nonstrickt' to 'nonstrict'"); + UNSCOPED_INFO("spelt: " + made); + CHECK(made.compare(0, 12, "--!nonstrict") == 0); } - template<> template<> - void object::test<14>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an unused LSL declaration is removed unless its value does something", "[ALScriptFixes]") { - set_test_name("an LSL declaration nothing uses is taken out whole, and a local only where what it is given does nothing"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string state = "default\n{\n state_entry()\n {\n llOwnerSay(\"x\");\n }\n}\n"; - std::string made = fixed("integer unused = 5;\n" + state, false, "LSLDeclaredButNotUsed", "Remove 'unused'"); - ensure_equals("the global's line gone", made, state); - made = fixed("integer helper(integer x)\n{\n return x;\n}\n" + state, false, "LSLDeclaredButNotUsed", "Remove 'helper'"); - ensure_equals("the function's lines gone", made, state); - made = fixed("default\n{\n state_entry()\n {\n integer a = 1 + 2;\n llOwnerSay(\"x\");\n }\n}\n", false, + std::string made = require_fixed("integer unused = 5;\n" + state, false, "LSLDeclaredButNotUsed", "Remove 'unused'"); + UNSCOPED_INFO("the global's line gone"); + CHECK(made == state); + made = require_fixed("integer helper(integer x)\n{\n return x;\n}\n" + state, false, "LSLDeclaredButNotUsed", "Remove 'helper'"); + UNSCOPED_INFO("the function's lines gone"); + CHECK(made == state); + made = require_fixed("default\n{\n state_entry()\n {\n integer a = 1 + 2;\n llOwnerSay(\"x\");\n }\n}\n", false, "LSLDeclaredButNotUsed", "Remove 'a'"); - ensure_equals("the local's line gone", made, state); + UNSCOPED_INFO("the local's line gone"); + CHECK(made == state); const ALScriptProblem* problem = nullptr; const ALScriptProblems problems = check("default\n{\n state_entry()\n {\n integer h = llListen(0, \"\", NULL_KEY, \"\");\n }\n}\n", false); @@ -480,26 +541,29 @@ namespace tut { problem = each.key == "LSLDeclaredButNotUsed" ? &each : problem; } - ensure("said: " + said(problems), problem != nullptr); - ensure("kept where what it is given does something", problem->fixes.empty()); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + UNSCOPED_INFO("kept where what it is given does something"); + REQUIRE(problem->fixes.empty()); // One that shares its line keeps the line. ALScriptProblem shared; shared.line = 0; ALScriptFixes::offerRemoval(shared, "integer a = 1; integer b = 2;\n", 0, 8, 0, 13, "a"); - ensure_equals("only itself, and the space after it", ALScriptFixes::apply("integer a = 1; integer b = 2;\n", shared.fixes.front()).value_or(""), + UNSCOPED_INFO("only itself, and the space after it"); + CHECK(ALScriptFixes::apply("integer a = 1; integer b = 2;\n", shared.fixes.front()).value_or("") == std::string("integer b = 2;\n")); // The last on its line takes the space before it instead. ALScriptProblem last; ALScriptFixes::offerRemoval(last, "integer a = 1; integer b = 2;\n", 0, 23, 0, 28, "b"); - ensure_equals("and the space before it", ALScriptFixes::apply("integer a = 1; integer b = 2;\n", last.fixes.front()).value_or(""), + UNSCOPED_INFO("and the space before it"); + CHECK(ALScriptFixes::apply("integer a = 1; integer b = 2;\n", last.fixes.front()).value_or("") == std::string("integer a = 1;\n")); } - template<> template<> - void object::test<15>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: what the optimizer did is offered as a change to the source", "[ALScriptFixes]") { - set_test_name("what the optimizer did is offered as a change to the source, where the source says what it read"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "default\n{\n state_entry()\n {\n llOwnerSay((string)(1 + 2));\n return;\n" " llOwnerSay(\"never\");\n }\n}\n"; ALPreprocessor::Options options; @@ -516,159 +580,196 @@ namespace tut folded = p.key == "OptimizerFolded" && !p.fixes.empty() ? &p : folded; unreachable = p.key == "OptimizerRemovedUnreachable" && !p.fixes.empty() ? &p : unreachable; } - ensure("a fold offered: " + notes, folded != nullptr); - ensure("never preferred: the source is the scripter's", !folded->fixes.front().preferred); + UNSCOPED_INFO("a fold offered: " + notes); + REQUIRE(folded != nullptr); + UNSCOPED_INFO("never preferred: the source is the scripter's"); + CHECK_FALSE(folded->fixes.front().preferred); std::string made = ALScriptFixes::apply(script, folded->fixes.front()).value_or("refused"); - ensure("written in: " + made, made.find("llOwnerSay(\"3\");") != std::string::npos); - ensure("what can never run offered: " + notes, unreachable != nullptr && unreachable->fixes.front().preferred && unreachable->fixes.front().safe); + UNSCOPED_INFO("written in: " + made); + REQUIRE(made.find("llOwnerSay(\"3\");") != std::string::npos); + { + INFO("what can never run offered: " + notes); + REQUIRE(unreachable != nullptr); + REQUIRE(unreachable->fixes.front().preferred); + CHECK(unreachable->fixes.front().safe); + } made = ALScriptFixes::apply(script, unreachable->fixes.front()).value_or("refused"); - ensure("gone: " + made, made.find("never") == std::string::npos && made.find(" return;\n }\n") != std::string::npos); + { + INFO("gone: " + made); + REQUIRE(made.find("never") == std::string::npos); + REQUIRE(made.find(" return;\n }\n") != std::string::npos); + } } - template<> template<> - void object::test<16>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an LSL expression chosen goes in a local before its statement where safe", "[ALScriptFixes]") { - set_test_name("an LSL expression chosen goes into a local just before its statement, where that runs it as it ran"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string head = "default\n{\n state_entry()\n {\n"; const std::string tail = " }\n}\n"; - std::string made = actedOn(head + " llOwnerSay((string)(llGetUnixTime() + 5));\n" + tail, false, "llGetUnixTime() + 5", true, + std::string made = require_acted_on(head + " llOwnerSay((string)(llGetUnixTime() + 5));\n" + tail, false, "llGetUnixTime() + 5", true, "Put it in a local, 'value'"); - ensure_equals("on its own line", made, + UNSCOPED_INFO("on its own line"); + CHECK(made == head + " integer value = llGetUnixTime() + 5;\n llOwnerSay((string)(value));\n" + tail); // After another statement on the line, just before its own; and // named as nothing in the script is. - made = actedOn(head + " integer value = 1; llOwnerSay((string)(value * 2.5));\n" + tail, false, "value * 2.5", true, + made = require_acted_on(head + " integer value = 1; llOwnerSay((string)(value * 2.5));\n" + tail, false, "value * 2.5", true, "Put it in a local, 'value2'"); - ensure_equals("beside", made, head + " integer value = 1; float value2 = value * 2.5; llOwnerSay((string)(value2));\n" + tail); - made = actedOn(head + " llSetRot(llEuler2Rot(<0, 0, 1>));\n" + tail, false, "llEuler2Rot(<0, 0, 1>)", true, "Put it in a local, 'value'"); - ensure("a rotation as LSL says it: " + made, made.find("rotation value = llEuler2Rot(<0, 0, 1>);") != std::string::npos); + UNSCOPED_INFO("beside"); + CHECK(made == head + " integer value = 1; float value2 = value * 2.5; llOwnerSay((string)(value2));\n" + tail); + made = require_acted_on(head + " llSetRot(llEuler2Rot(<0, 0, 1>));\n" + tail, false, "llEuler2Rot(<0, 0, 1>)", true, "Put it in a local, 'value'"); + UNSCOPED_INFO("a rotation as LSL says it: " + made); + REQUIRE(made.find("rotation value = llEuler2Rot(<0, 0, 1>);") != std::string::npos); // Not out of a loop's condition, nor from under an if with no // braces, nor what is assigned to. std::string none = head + " integer i;\n while (i < 10) ++i;\n" + tail; - ensure("a condition: " + actedOn(none, false, "i < 10", true, "Put it in a local, 'value'"), - actedOn(none, false, "i < 10", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("a condition: " + require_acted_on(none, false, "i < 10", true, "Put it in a local, 'value'")); + CHECK(require_acted_on(none, false, "i < 10", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); none = head + " integer i;\n if (i) llOwnerSay((string)(i + 1));\n" + tail; - ensure("under an if", actedOn(none, false, "i + 1", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("under an if"); + CHECK(require_acted_on(none, false, "i + 1", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); none = head + " integer i;\n i = 4;\n llOwnerSay((string)i);\n" + tail; S32 line = 0, column = 0, end = 0; - place(none, "i = 4", line, column, end); - ensure("assigned to", acted(none, false, line, column, line, column + 1, "Put it in a local, 'value'").rfind("offered", 0) == 0); + require_place(none, "i = 4", line, column, end); + UNSCOPED_INFO("assigned to"); + CHECK(require_acted(none, false, line, column, line, column + 1, "Put it in a local, 'value'").rfind("offered", 0) == 0); // Nor the whole of a statement, which would leave `value;`. none = head + " llOwnerSay((string)llGetUnixTime());\n" + tail; - ensure("the whole statement", actedOn(none, false, "llOwnerSay((string)llGetUnixTime())", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("the whole statement"); + CHECK(require_acted_on(none, false, "llOwnerSay((string)llGetUnixTime())", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); } - template<> template<> - void object::test<17>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an LSL if-else is inverted: the condition turned and the branches swapped", "[ALScriptFixes]") { - set_test_name("an LSL if with an else is inverted: the condition turned round, the branches swapped"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string head = "default\n{\n touch_start(integer n)\n {\n"; const std::string tail = " }\n}\n"; - std::string made = actedOn(head + " if (n == 1)\n {\n llOwnerSay(\"one\");\n }\n else\n {\n" + std::string made = require_acted_on(head + " if (n == 1)\n {\n llOwnerSay(\"one\");\n }\n else\n {\n" " llOwnerSay(\"many\");\n }\n" + tail, false, "if", false, "Invert the if"); - ensure_equals("braces", made, + UNSCOPED_INFO("braces"); + CHECK(made == head + " if (n != 1)\n {\n llOwnerSay(\"many\");\n }\n else\n {\n" " llOwnerSay(\"one\");\n }\n" + tail); - made = actedOn(head + " if (!n) llOwnerSay(\"none\"); else llOwnerSay(\"some\");\n" + tail, false, "if", false, "Invert the if"); - ensure_equals("a not taken off", made, head + " if (n) llOwnerSay(\"some\"); else llOwnerSay(\"none\");\n" + tail); - made = actedOn(head + " if (n > 2) llOwnerSay(\"many\"); else llOwnerSay(\"few\");\n" + tail, false, "if", false, "Invert the if"); - ensure_equals("an order under a not", made, head + " if (!(n > 2)) llOwnerSay(\"few\"); else llOwnerSay(\"many\");\n" + tail); + made = require_acted_on(head + " if (!n) llOwnerSay(\"none\"); else llOwnerSay(\"some\");\n" + tail, false, "if", false, "Invert the if"); + UNSCOPED_INFO("a not taken off"); + CHECK(made == head + " if (n) llOwnerSay(\"some\"); else llOwnerSay(\"none\");\n" + tail); + made = require_acted_on(head + " if (n > 2) llOwnerSay(\"many\"); else llOwnerSay(\"few\");\n" + tail, false, "if", false, "Invert the if"); + UNSCOPED_INFO("an order under a not"); + CHECK(made == head + " if (!(n > 2)) llOwnerSay(\"few\"); else llOwnerSay(\"many\");\n" + tail); const std::string chain = head + " if (n == 1) llOwnerSay(\"one\"); else if (n == 2) llOwnerSay(\"two\");\n" + tail; - ensure("not with an else if", actedOn(chain, false, "if", false, "Invert the if").rfind("offered", 0) == 0); + UNSCOPED_INFO("not with an else if"); + CHECK(require_acted_on(chain, false, "if", false, "Invert the if").rfind("offered", 0) == 0); const std::string lone = head + " if (n == 1) llOwnerSay(\"one\");\n" + tail; - ensure("not without an else", actedOn(lone, false, "if", false, "Invert the if").rfind("offered", 0) == 0); + UNSCOPED_INFO("not without an else"); + CHECK(require_acted_on(lone, false, "if", false, "Invert the if").rfind("offered", 0) == 0); // An else branch ending in an if of its own goes first in braces, // or the else would come to be that if's. - made = actedOn(head + " if (n) llOwnerSay(\"a\"); else while (n--) if (n == 3) llOwnerSay(\"c\");\n" + tail, false, "if", false, + made = require_acted_on(head + " if (n) llOwnerSay(\"a\"); else while (n--) if (n == 3) llOwnerSay(\"c\");\n" + tail, false, "if", false, "Invert the if"); - ensure_equals("braced", made, head + " if (!n) { while (n--) if (n == 3) llOwnerSay(\"c\"); } else llOwnerSay(\"a\");\n" + tail); + UNSCOPED_INFO("braced"); + CHECK(made == head + " if (!n) { while (n--) if (n == 3) llOwnerSay(\"c\"); } else llOwnerSay(\"a\");\n" + tail); } - template<> template<> - void object::test<18>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an LSL state asking for an event it does not hear is offered a handler", "[ALScriptFixes]") { - set_test_name("a handler is offered for an event the LSL state asks for and does not hear, laid out as its others are"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "default\n{\n state_entry()\n {\n llListen(0, \"\", NULL_KEY, \"\");\n llSetTimerEvent(1.0);\n" " }\n}\n"; - std::string made = actedOn(script, false, "llListen", false, "Add a handler for 'listen'"); - ensure_equals("before the brace, after a blank line", made, + std::string made = require_acted_on(script, false, "llListen", false, "Add a handler for 'listen'"); + UNSCOPED_INFO("before the brace, after a blank line"); + CHECK(made == "default\n{\n state_entry()\n {\n llListen(0, \"\", NULL_KEY, \"\");\n llSetTimerEvent(1.0);\n }\n\n" " listen(integer channel, string name, key id, string msg)\n {\n }\n}\n"); - ensure("the timer too", actions(script, false, 4, 8, 4, 8).size() == 2); + UNSCOPED_INFO("the timer too"); + REQUIRE(actions(script, false, 4, 8, 4, 8).size() == 2); // Heard already: nothing. const std::string heard = "default\n{\n state_entry() {\n llSetTimerEvent(1.0);\n }\n\n timer() {\n }\n}\n"; - ensure("heard: " + titles(actions(heard, false, 3, 8, 3, 8)), actions(heard, false, 3, 8, 3, 8).empty()); + UNSCOPED_INFO("heard: " + titles(actions(heard, false, 3, 8, 3, 8))); + REQUIRE(actions(heard, false, 3, 8, 3, 8).empty()); // The brace where the state's first handler has it. const std::string same = "default\n{\n state_entry() {\n llSensorRepeat(\"\", NULL_KEY, AGENT, 10.0, PI, 5.0);\n }\n}\n"; - made = actedOn(same, false, "llSensorRepeat", false, "Add a handler for 'sensor'"); - ensure("on its line: " + made, made.find("\n\n sensor(integer num_detected) {\n }\n}\n") != std::string::npos); + made = require_acted_on(same, false, "llSensorRepeat", false, "Add a handler for 'sensor'"); + UNSCOPED_INFO("on its line: " + made); + REQUIRE(made.find("\n\n sensor(integer num_detected) {\n }\n}\n") != std::string::npos); // Asked for from a function a handler calls, and from one that one // calls: offered all the same. const std::string helper = "arm()\n{\n llSetTimerEvent(1.0);\n}\nstart()\n{\n arm();\n}\n" "default\n{\n state_entry()\n {\n start();\n }\n}\n"; - made = actedOn(helper, false, "start();\n }", false, "Add a handler for 'timer'"); - ensure("through two calls: " + made, made.find(" timer()\n {\n }\n") != std::string::npos); + made = require_acted_on(helper, false, "start();\n }", false, "Add a handler for 'timer'"); + UNSCOPED_INFO("through two calls: " + made); + REQUIRE(made.find(" timer()\n {\n }\n") != std::string::npos); } - template<> template<> - void object::test<19>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a SLua expression chosen goes in a local where it surely runs as before", "[ALScriptFixes]") { - set_test_name("a SLua expression chosen goes into a local, where that runs it as surely as it ran"); - ensure("definitions: " + error, luauLoaded); - std::string made = actedOn("local function f(n: number)\n print(n * 2 + 1)\nend\nf(1)\n", true, "n * 2", true, "Put it in a local, 'value'"); - ensure_equals("before the statement", made, "local function f(n: number)\n local value = n * 2\n print(value + 1)\nend\nf(1)\n"); - made = actedOn("local x = 1; print(x + 1)\n", true, "x + 1", true, "Put it in a local, 'value'"); - ensure_equals("beside", made, "local x = 1; local value = x + 1; print(value)\n"); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + std::string made = require_acted_on("local function f(n: number)\n print(n * 2 + 1)\nend\nf(1)\n", true, "n * 2", true, "Put it in a local, 'value'"); + UNSCOPED_INFO("before the statement"); + CHECK(made == "local function f(n: number)\n local value = n * 2\n print(value + 1)\nend\nf(1)\n"); + made = require_acted_on("local x = 1; print(x + 1)\n", true, "x + 1", true, "Put it in a local, 'value'"); + UNSCOPED_INFO("beside"); + CHECK(made == "local x = 1; local value = x + 1; print(value)\n"); const std::string skipped = "local x = 1\nprint(x > 0 and tostring(x))\n"; - ensure("not what and may skip", actedOn(skipped, true, "tostring(x)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not what and may skip"); + CHECK(require_acted_on(skipped, true, "tostring(x)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); const std::string looped = "local x = 1\nwhile x < 10 do\n x += 1\nend\n"; - ensure("not a loop's condition", actedOn(looped, true, "x < 10", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not a loop's condition"); + CHECK(require_acted_on(looped, true, "x < 10", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); const std::string elseif = "local x = 1\nif x == 0 then\n print(0)\nelseif x + 1 == 2 then\n print(1)\nend\n"; - ensure("not an elseif's condition", actedOn(elseif, true, "x + 1", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not an elseif's condition"); + CHECK(require_acted_on(elseif, true, "x + 1", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); // Not a call that is the whole statement: `value` alone is no // statement. const std::string whole = "print(math.abs(-1))\n"; - ensure("not the whole statement", actedOn(whole, true, "print(math.abs(-1))", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); - ensure("its argument yes: " + actedOn(whole, true, "math.abs(-1)", true, "Put it in a local, 'value'"), - actedOn(whole, true, "math.abs(-1)", true, "Put it in a local, 'value'") == "local value = math.abs(-1)\nprint(value)\n"); + UNSCOPED_INFO("not the whole statement"); + CHECK(require_acted_on(whole, true, "print(math.abs(-1))", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("its argument yes: " + require_acted_on(whole, true, "math.abs(-1)", true, "Put it in a local, 'value'")); + CHECK(require_acted_on(whole, true, "math.abs(-1)", true, "Put it in a local, 'value'") == "local value = math.abs(-1)\nprint(value)\n"); // Not a call where all it gives is kept, since a local keeps one. const std::string two = "local function g(n: number) return n, 2 end\n"; - ensure("not a last argument", actedOn(two + "print(g(1))\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); - ensure("not a last return", actedOn(two + "local function h() return 0, g(1) end\nh()\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); - ensure("not a table's last item", actedOn(two + "local t = { g(1) }\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); - ensure("not to two names", actedOn(two + "local a, b = g(1)\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); - ensure("not a for-in's values", actedOn("for k, v in pairs({}) do end\n", true, "pairs({})", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not a last argument"); + CHECK(require_acted_on(two + "print(g(1))\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not a last return"); + CHECK(require_acted_on(two + "local function h() return 0, g(1) end\nh()\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not a table's last item"); + CHECK(require_acted_on(two + "local t = { g(1) }\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not to two names"); + CHECK(require_acted_on(two + "local a, b = g(1)\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); + UNSCOPED_INFO("not a for-in's values"); + CHECK(require_acted_on("for k, v in pairs({}) do end\n", true, "pairs({})", true, "Put it in a local, 'value'").rfind("offered", 0) == 0); // Where only the first is kept anyway, yes. - ensure("an argument before the last: " + actedOn(two + "print(g(1), 3)\n", true, "g(1)", true, "Put it in a local, 'value'"), - actedOn(two + "print(g(1), 3)\n", true, "g(1)", true, "Put it in a local, 'value'") == two + "local value = g(1)\nprint(value, 3)\n"); - ensure("to one name: " + actedOn(two + "local a = g(1)\n", true, "g(1)", true, "Put it in a local, 'value'"), - actedOn(two + "local a = g(1)\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) != 0); + UNSCOPED_INFO("an argument before the last: " + require_acted_on(two + "print(g(1), 3)\n", true, "g(1)", true, "Put it in a local, 'value'")); + CHECK(require_acted_on(two + "print(g(1), 3)\n", true, "g(1)", true, "Put it in a local, 'value'") == two + "local value = g(1)\nprint(value, 3)\n"); + UNSCOPED_INFO("to one name: " + require_acted_on(two + "local a = g(1)\n", true, "g(1)", true, "Put it in a local, 'value'")); + CHECK(require_acted_on(two + "local a = g(1)\n", true, "g(1)", true, "Put it in a local, 'value'").rfind("offered", 0) != 0); } - template<> template<> - void object::test<20>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a SLua if is inverted; a join interpolated; a local given its type", "[ALScriptFixes]") { - set_test_name("a SLua if is inverted, a concatenation interpolated, a local given the type it has"); - ensure("definitions: " + error, luauLoaded); - std::string made = actedOn("local x = 1\nif x == 1 then\n print(\"one\")\nelse\n print(\"other\")\nend\n", true, "if", false, "Invert the if"); - ensure_equals("inverted", made, "local x = 1\nif x ~= 1 then\n print(\"other\")\nelse\n print(\"one\")\nend\n"); - made = actedOn("local x = 1\nif not (x > 1) then\n print(\"few\")\nelse\n print(\"many\")\nend\n", true, "if", false, "Invert the if"); - ensure_equals("a not taken off", made, "local x = 1\nif x > 1 then\n print(\"many\")\nelse\n print(\"few\")\nend\n"); - made = actedOn("local x = 1\nprint(\"n = \" .. x .. \"!\")\n", true, "..", false, "Write it as an interpolated string"); - ensure_equals("interpolated", made, "local x = 1\nprint(`n = {x}!`)\n"); - made = actedOn("local count = 5\nprint(count)\n", true, "count", false, "Declare it as 'number'"); - ensure_equals("annotated", made, "local count: number = 5\nprint(count)\n"); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + std::string made = require_acted_on("local x = 1\nif x == 1 then\n print(\"one\")\nelse\n print(\"other\")\nend\n", true, "if", false, "Invert the if"); + CHECK(made == "local x = 1\nif x ~= 1 then\n print(\"other\")\nelse\n print(\"one\")\nend\n"); + made = require_acted_on("local x = 1\nif not (x > 1) then\n print(\"few\")\nelse\n print(\"many\")\nend\n", true, "if", false, "Invert the if"); + UNSCOPED_INFO("a not taken off"); + CHECK(made == "local x = 1\nif x > 1 then\n print(\"many\")\nelse\n print(\"few\")\nend\n"); + made = require_acted_on("local x = 1\nprint(\"n = \" .. x .. \"!\")\n", true, "..", false, "Write it as an interpolated string"); + UNSCOPED_INFO("interpolated"); + CHECK(made == "local x = 1\nprint(`n = {x}!`)\n"); + made = require_acted_on("local count = 5\nprint(count)\n", true, "count", false, "Declare it as 'number'"); + UNSCOPED_INFO("annotated"); + CHECK(made == "local count: number = 5\nprint(count)\n"); } - template<> template<> - void object::test<21>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a SLua script asking for an event it does not hear is offered a handler", "[ALScriptFixes]") { - set_test_name("a handler is offered for an event a SLua script asks for and does not hear, with the parameters the definitions give"); - ensure("definitions: " + error, luauLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); // Nonstrict, as the grid compiles: the new solver checking strict // cannot yet push an overloaded function's parameter types into a // function given to it, so every LLEvents:on there is an error it @@ -677,24 +778,27 @@ namespace tut grid.mode = "nonstrict"; luau.setConfig(grid); const std::string script = "ll.Listen(0, \"\", ll.GetOwner(), \"\")\n"; - std::string made = actedOn(script, true, "Listen", false, "Add a handler for 'listen'"); - ensure_equals("at the end", made, + std::string made = require_acted_on(script, true, "Listen", false, "Add a handler for 'listen'"); + UNSCOPED_INFO("at the end"); + CHECK(made == script + "\nLLEvents:on(\"listen\", function(channel: number, name: string, id: uuid, msg: string)\nend)\n"); const std::string heard = script + "LLEvents:on(\"listen\", function(channel, name, id, text) end)\n"; - ensure("heard: " + titles(actions(heard, true, 0, 3, 0, 3)), actedOn(heard, true, "Listen", false, "Add a handler for 'listen'").rfind("offered", 0) == 0); + UNSCOPED_INFO("heard: " + titles(actions(heard, true, 0, 3, 0, 3))); + CHECK(require_acted_on(heard, true, "Listen", false, "Add a handler for 'listen'").rfind("offered", 0) == 0); // A handler taken off is no handler put on. const std::string off = script + "local function said(channel, name, id, text) end\nLLEvents:off(\"listen\", said)\n"; - ensure("offered where only taken off: " + titles(actions(off, true, 0, 3, 0, 3)), - actedOn(off, true, "Listen", false, "Add a handler for 'listen'").rfind("offered", 0) != 0); + UNSCOPED_INFO("offered where only taken off: " + titles(actions(off, true, 0, 3, 0, 3))); + CHECK(require_acted_on(off, true, "Listen", false, "Add a handler for 'listen'").rfind("offered", 0) != 0); } - template<> template<> - void object::test<22>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a refactor's name is fresh and a line put in maps to its source line", "[ALScriptFixes]") { - set_test_name("a refactor's name is one the text has nowhere, and one put in at a line's start is mapped to the source line it began as"); - ensure_equals("free", ALScriptFixes::freshName("local x = 1", "value"), std::string("value")); - ensure_equals("taken", ALScriptFixes::freshName("local value = 1 -- value2", "value"), std::string("value3")); - ensure_equals("a part is not a word", ALScriptFixes::freshName("local values = 1", "value"), std::string("value")); + UNSCOPED_INFO("free"); + CHECK(ALScriptFixes::freshName("local x = 1", "value") == std::string("value")); + UNSCOPED_INFO("taken"); + CHECK(ALScriptFixes::freshName("local value = 1 -- value2", "value") == std::string("value3")); + UNSCOPED_INFO("a part is not a word"); + CHECK(ALScriptFixes::freshName("local values = 1", "value") == std::string("value")); // A line the preprocessor carries as it stands, a line down for the // directive it drops. ALPreprocessor::Options options; @@ -704,19 +808,19 @@ namespace tut const ALPreprocessor::Result result = ALPreprocessor::run(script, options); ALScriptProblem held; const ALSourceMap::Loc carried = result.map.toExpanded(0, 5, 8); - ensure("carried", carried.found()); + REQUIRE(carried.found()); held.fixes.push_back(ALScriptFixes::titled("ScriptActionExtract", "Put it in a local, '[1]'", { "value" })); held.fixes.back().edits.push_back({ carried.line, 0, carried.line, 0, " integer value = 1;\n" }); ALScriptFixes::mapThrough(result.map, held); - ensure_equals("kept", held.fixes.size(), static_cast(1)); - ensure_equals("the source's line", held.fixes.front().edits.front().line, 5); - ensure_equals("its start", held.fixes.front().edits.front().column, 0); + UNSCOPED_INFO("kept"); + REQUIRE(held.fixes.size() == static_cast(1)); + UNSCOPED_INFO("the source's line"); + CHECK(held.fixes.front().edits.front().line == 5); + CHECK(held.fixes.front().edits.front().column == 0); } - template<> template<> - void object::test<23>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a module's exports are the names of the table it returns however built", "[ALScriptFixes]") { - set_test_name("a module's exports are the names of the table it returns, however it builds it"); const auto listed = [](const std::string& source) { std::string out; for (const std::string& name : ALLuauExports::of(source)) @@ -725,24 +829,29 @@ namespace tut } return out; }; - ensure_equals("a table returned", listed("local function greet() end\nreturn { greet = greet, count = 3, [\"spaced out\"] = 1, 4 }\n"), + UNSCOPED_INFO("a table returned"); + CHECK(listed("local function greet() end\nreturn { greet = greet, count = 3, [\"spaced out\"] = 1, 4 }\n") == std::string("greet count")); - ensure_equals("a local table filled in", - listed("local M = { count = 3 }\nfunction M.greet() end\nfunction M:reset() end\nM.name = \"util\"\nM[\"size\"] = 2\n" - "local other = {}\nother.hidden = 1\nreturn M\n"), + UNSCOPED_INFO("a local table filled in"); + CHECK(listed("local M = { count = 3 }\nfunction M.greet() end\nfunction M:reset() end\nM.name = \"util\"\nM[\"size\"] = 2\n" + "local other = {}\nother.hidden = 1\nreturn M\n") == std::string("count greet reset name size")); - ensure_equals("through setmetatable", listed("local M = {}\nM.x = 1\nreturn setmetatable(M, { __index = M })\n"), std::string("x")); - ensure_equals("each once", listed("local M = {}\nM.x = 1\nM.x = 2\nreturn M\n"), std::string("x")); - ensure_equals("a function: no names", listed("return function() end\n"), std::string()); - ensure_equals("nothing returned", listed("local M = {}\nM.x = 1\n"), std::string()); - ensure_equals("a module that does not parse", listed("local M = {\nreturn M\n"), std::string()); - ensure_equals("not a keyword", listed("return { [\"end\"] = 1, ok = 2 }\n"), std::string("ok")); + UNSCOPED_INFO("through setmetatable"); + CHECK(listed("local M = {}\nM.x = 1\nreturn setmetatable(M, { __index = M })\n") == std::string("x")); + UNSCOPED_INFO("each once"); + CHECK(listed("local M = {}\nM.x = 1\nM.x = 2\nreturn M\n") == std::string("x")); + UNSCOPED_INFO("a function: no names"); + CHECK(listed("return function() end\n") == std::string()); + UNSCOPED_INFO("nothing returned"); + CHECK(listed("local M = {}\nM.x = 1\n") == std::string()); + UNSCOPED_INFO("a module that does not parse"); + CHECK(listed("local M = {\nreturn M\n") == std::string()); + UNSCOPED_INFO("not a keyword"); + CHECK(listed("return { [\"end\"] = 1, ok = 2 }\n") == std::string("ok")); } - template<> template<> - void object::test<24>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a require goes after the opening requires or comments apart from code", "[ALScriptFixes]") { - set_test_name("a require goes after the requires a script opens with, else after its comments, apart from the code"); ALScriptProblem problem; problem.key = "LuauUnknownGlobal"; problem.args = { "util" }; @@ -751,29 +860,33 @@ namespace tut ALScriptFixes::offerRequire(problem, text, module, field); return problem.fixes.empty() ? std::string("none") : ALScriptFixes::apply(text, problem.fixes.front()).value_or("refused"); }; - ensure_equals("after the last require", made("--!strict\nlocal a = require(\"a\")\nlocal b = require(\"b\").b\n\nprint(util.x)\n", "util", false), + UNSCOPED_INFO("after the last require"); + CHECK(made("--!strict\nlocal a = require(\"a\")\nlocal b = require(\"b\").b\n\nprint(util.x)\n", "util", false) == std::string("--!strict\nlocal a = require(\"a\")\nlocal b = require(\"b\").b\nlocal util = require(\"util\")\n\nprint(util.x)\n")); - ensure_equals("its words", problem.fixes.front().title, std::string("Require 'util'")); - ensure_equals("after the comments, the blank line kept", - made("--!strict\n--[[ A header\n over lines ]]\n\nprint(util.x)\n", "@lib/util", false), + CHECK(problem.fixes.front().title == std::string("Require 'util'")); + UNSCOPED_INFO("after the comments, the blank line kept"); + CHECK(made("--!strict\n--[[ A header\n over lines ]]\n\nprint(util.x)\n", "@lib/util", false) == std::string("--!strict\n--[[ A header\n over lines ]]\nlocal util = require(\"@lib/util\")\n\nprint(util.x)\n")); - ensure_equals("apart from code that follows at once", made("print(util)\n", "helpers", true), + UNSCOPED_INFO("apart from code that follows at once"); + CHECK(made("print(util)\n", "helpers", true) == std::string("local util = require(\"helpers\").util\n\nprint(util)\n")); - ensure_equals("a field's words", problem.fixes.front().title, std::string("Take 'util' from 'helpers'")); - ensure_equals("past a last line with no break", made("-- only a comment", "util", false), + CHECK(problem.fixes.front().title == std::string("Take 'util' from 'helpers'")); + UNSCOPED_INFO("past a last line with no break"); + CHECK(made("-- only a comment", "util", false) == std::string("-- only a comment\nlocal util = require(\"util\")")); - ensure_equals("a name quoted", made("print(util)\n", "a\"b", false), std::string("local util = require(\"a\\\"b\")\n\nprint(util)\n")); + UNSCOPED_INFO("a name quoted"); + CHECK(made("print(util)\n", "a\"b", false) == std::string("local util = require(\"a\\\"b\")\n\nprint(util)\n")); problem.fixes.clear(); ALScriptFixes::offerRequire(problem, "print(util)\n", "util", false); ALScriptFixes::offerRequire(problem, "print(util)\n", "util", false); - ensure_equals("offered once", problem.fixes.size(), static_cast(1)); - ensure("neither preferred nor safe", !problem.fixes.front().preferred && !problem.fixes.front().safe); + UNSCOPED_INFO("offered once"); + REQUIRE(problem.fixes.size() == static_cast(1)); + CHECK_FALSE(problem.fixes.front().preferred); + CHECK_FALSE(problem.fixes.front().safe); } - template<> template<> - void object::test<25>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an LSL include exports its macros and top-level functions and globals", "[ALScriptFixes]") { - set_test_name("an LSL include declares its macros and the functions and globals at its top, a fragment and all"); const auto listed = [](const std::string& source) { std::string out; for (const std::string& name : ALLSLExports::of(source)) @@ -782,23 +895,23 @@ namespace tut } return out; }; - ensure_equals("everything at the top", - listed("// helpers\n#define CHANNEL -42\n#define say(x) \\\n llOwnerSay(x)\n#ifdef DEBUG\n#include \"more.lsl\"\n#endif\n" + UNSCOPED_INFO("everything at the top"); + CHECK(listed("// helpers\n#define CHANNEL -42\n#define say(x) \\\n llOwnerSay(x)\n#ifdef DEBUG\n#include \"more.lsl\"\n#endif\n" "integer gCount = 0;\nlist gSeen;\nvector gAt = <1, 2, 3>;\n" "string greet(string name)\n{\n integer inner = 1;\n return \"hi \" + name;\n}\n" - "reset() { gCount = 0; }\ninline integer twice(integer n) { return n * 2; }\n"), + "reset() { gCount = 0; }\ninline integer twice(integer n) { return n * 2; }\n") == std::string("CHANNEL say gCount gSeen gAt greet reset twice")); - ensure_equals("a state holds no declarations", - listed("integer shared;\ndefault\n{\n state_entry()\n {\n integer local = 1;\n }\n}\nstate other { touch_start(integer n) {} }\n"), + UNSCOPED_INFO("a state holds no declarations"); + CHECK(listed("integer shared;\ndefault\n{\n state_entry()\n {\n integer local = 1;\n }\n}\nstate other { touch_start(integer n) {} }\n") == std::string("shared")); - ensure_equals("each once", listed("#define A 1\n#undef A\n#define A 2\n"), std::string("A")); - ensure_equals("SLua is no LSL", listed("local function greet() end\nprint(greet())\nreturn { greet = greet }\n"), std::string()); + UNSCOPED_INFO("each once"); + CHECK(listed("#define A 1\n#undef A\n#define A 2\n") == std::string("A")); + UNSCOPED_INFO("SLua is no LSL"); + CHECK(listed("local function greet() end\nprint(greet())\nreturn { greet = greet }\n") == std::string()); } - template<> template<> - void object::test<26>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an LSL include goes after opening directives or comments apart from code", "[ALScriptFixes]") { - set_test_name("an include goes after the directives an LSL script opens with, else after its comments, apart from the code"); ALScriptProblem problem; problem.key = "LSLUndeclared"; problem.args = { "greet" }; @@ -807,17 +920,21 @@ namespace tut ALScriptFixes::offerInclude(problem, text, include); return problem.fixes.empty() ? std::string("none") : ALScriptFixes::apply(text, problem.fixes.front()).value_or("refused"); }; - ensure_equals("after the last directive", - made("// Hello\n#include \"a.lsl\"\n#define LONG \\\n 1\n\ndefault {}\n", "helpers.lsl"), + UNSCOPED_INFO("after the last directive"); + CHECK(made("// Hello\n#include \"a.lsl\"\n#define LONG \\\n 1\n\ndefault {}\n", "helpers.lsl") == std::string("// Hello\n#include \"a.lsl\"\n#define LONG \\\n 1\n#include \"helpers.lsl\"\n\ndefault {}\n")); - ensure_equals("its words", problem.fixes.front().title, std::string("Include 'helpers.lsl'")); - ensure_equals("after the comments, the blank line kept", made("/* A header\n over lines */\n\ndefault {}\n", "helpers"), + CHECK(problem.fixes.front().title == std::string("Include 'helpers.lsl'")); + UNSCOPED_INFO("after the comments, the blank line kept"); + CHECK(made("/* A header\n over lines */\n\ndefault {}\n", "helpers") == std::string("/* A header\n over lines */\n#include \"helpers\"\n\ndefault {}\n")); - ensure_equals("apart from code that follows at once", made("default {}\n", "helpers"), std::string("#include \"helpers\"\n\ndefault {}\n")); - ensure_equals("no name with a quote in it", made("default {}\n", "a\"b"), std::string("none")); + UNSCOPED_INFO("apart from code that follows at once"); + CHECK(made("default {}\n", "helpers") == std::string("#include \"helpers\"\n\ndefault {}\n")); + UNSCOPED_INFO("no name with a quote in it"); + CHECK(made("default {}\n", "a\"b") == std::string("none")); // What the studio offers them on: a function, a global and a macro // alike, each undeclared by its name. - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const ALScriptProblems problems = check("default\n{\n state_entry()\n {\n greet(\"x\");\n llOwnerSay((string)gCount);\n llSay(CHANNEL, \"\");\n }\n}\n", false); for (const char* name : { "greet", "gCount", "CHANNEL" }) @@ -827,14 +944,14 @@ namespace tut { said_so = said_so || (each.key == "LSLUndeclared" && each.args.size() == 1 && each.args[0] == name); } - ensure(std::string("undeclared: ") + name + "\n" + said(problems), said_so); + CAPTURE(name); + INFO("said:\n" + said(problems)); + REQUIRE(said_so); } } - template<> template<> - void object::test<27>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a source fix maps into the expansion where copied but not over a macro", "[ALScriptFixes]") { - set_test_name("a fix made over the source is taken into the expansion where it lands on what the expansion copied, and not where a macro stood"); ALPreprocessor::Options options; options.fileName = "main.lsl"; options.resolve = [](const ALPreprocessor::Ask&, ALPreprocessor::Include&) { return ALPreprocessor::Found::No; }; @@ -856,43 +973,59 @@ namespace tut return made.has_value(); }; std::string out; - ensure("a word the expansion copied", into(edited(5, 20, 5, 25, "hi"), out)); + UNSCOPED_INFO("a word the expansion copied"); + CHECK(into(edited(5, 20, 5, 25, "hi"), out)); std::string expected = result.text; expected.replace(expected.find("\"hello\""), 7, "\"hi\""); - ensure_equals("replaced there alone", out, expected); + UNSCOPED_INFO("replaced there alone"); + CHECK(out == expected); ALScriptFix over_macro = edited(6, 27, 6, 28, "3"); - ensure("not where a macro stood", !ALScriptFixes::intoExpansion(result.map, over_macro)); - ensure_equals("and the fix as it was", over_macro.edits.front().line, 6); - ensure("nor across lines unless whole", !into(edited(5, 8, 6, 8, ""), out)); - - ensure("a line put in at a line's start", into(edited(7, 0, 7, 0, " integer x;\n"), out)); + UNSCOPED_INFO("not where a macro stood"); + CHECK_FALSE(ALScriptFixes::intoExpansion(result.map, over_macro)); + UNSCOPED_INFO("and the fix as it was"); + CHECK(over_macro.edits.front().line == 6); + UNSCOPED_INFO("nor across lines unless whole"); + CHECK_FALSE(into(edited(5, 8, 6, 8, ""), out)); + + UNSCOPED_INFO("a line put in at a line's start"); + CHECK(into(edited(7, 0, 7, 0, " integer x;\n"), out)); const size_t put = out.find("integer x;"); - ensure("before the line it was put before", put != std::string::npos && put < out.find("\"bye\"") && put > out.find("(string)")); - ensure("whole lines taken out", into(edited(5, 0, 6, 0, ""), out)); - ensure("that line gone, the rest kept", out.find("hello") == std::string::npos && out.find("\"bye\"") != std::string::npos && - out.find("(string)") != std::string::npos); + { + INFO("before the line it was put before"); + REQUIRE(put != std::string::npos); + CHECK(put < out.find("\"bye\"")); + CHECK(put > out.find("(string)")); + } + UNSCOPED_INFO("whole lines taken out"); + CHECK(into(edited(5, 0, 6, 0, ""), out)); + { + INFO("that line gone, the rest kept"); + REQUIRE(out.find("hello") == std::string::npos); + REQUIRE(out.find("\"bye\"") != std::string::npos); + REQUIRE(out.find("(string)") != std::string::npos); + } } - template<> template<> - void object::test<28>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a repeated LSL string goes into a global after the others; uses named", "[ALScriptFixes]") { - set_test_name("a string written the same several times over goes into a global after the ones the script opens with, each use named"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "string gGreeting = \"hello there\";\n" "default\n{\n state_entry()\n {\n llOwnerSay(\"hello there\");\n" " llSetText(\"hello there\", <1.0, 1.0, 1.0>, 1.0);\n }\n" " touch_start(integer n)\n {\n llOwnerSay(\"hello there\");\n llOwnerSay(\"other\");\n }\n}\n"; - const std::string made = actedOn(script, false, "\"hello there\");", false, "Put the string in a global, 'gHelloThere', for its 3 uses"); - ensure_equals("declared after the globals, each use named, the global's own value kept", made, + const std::string made = require_acted_on(script, false, "\"hello there\");", false, "Put the string in a global, 'gHelloThere', for its 3 uses"); + UNSCOPED_INFO("declared after the globals, each use named, the global's own value kept"); + CHECK(made == "string gGreeting = \"hello there\";\nstring gHelloThere = \"hello there\";\n" "default\n{\n state_entry()\n {\n llOwnerSay(gHelloThere);\n" " llSetText(gHelloThere, <1.0, 1.0, 1.0>, 1.0);\n }\n" " touch_start(integer n)\n {\n llOwnerSay(gHelloThere);\n llOwnerSay(\"other\");\n }\n}\n"); // A short one costs more as a global than it saves: the list says // so, weighing it. A long one, used as often, is lighter. - ensure("heavier on LSO: " + std::to_string(ALScriptWeigh::lso(made).total) + " from " + std::to_string(ALScriptWeigh::lso(script).total), - ALScriptWeigh::lso(made).total >= ALScriptWeigh::lso(script).total); + UNSCOPED_INFO("heavier on LSO"); + CHECK(ALScriptWeigh::lso(made).total >= ALScriptWeigh::lso(script).total); std::string long_script = script; std::string long_made = made; for (std::string* text : { &long_script, &long_made }) @@ -902,67 +1035,79 @@ namespace tut text->replace(at, 11, "hello there, and a sentence of some length said to the owner"); } } - ensure("a long one lighter on LSO: " + std::to_string(ALScriptWeigh::lso(long_made).total) + " from " + - std::to_string(ALScriptWeigh::lso(long_script).total), - ALScriptWeigh::lso(long_made).total < ALScriptWeigh::lso(long_script).total); - ensure("not for a string used once", actedOn(script, false, "\"other\"", false, "Put the string in a global, 'gOther', for its 1 uses").rfind("offered", 0) == 0); + UNSCOPED_INFO("a long one lighter on LSO"); + CHECK(ALScriptWeigh::lso(long_made).total < ALScriptWeigh::lso(long_script).total); + UNSCOPED_INFO("not for a string used once"); + CHECK(require_acted_on(script, false, "\"other\"", false, "Put the string in a global, 'gOther', for its 1 uses").rfind("offered", 0) == 0); // No globals to follow: ahead of what the script opens with. const std::string bare = "default\n{\n state_entry()\n {\n llOwnerSay(\"hi\\n\");\n llOwnerSay(\"hi\\n\");\n }\n}\n"; - const std::string placed = actedOn(bare, false, "\"hi", false, "Put the string in a global, 'gHi', for its 2 uses"); - ensure("ahead of the state, a blank line after: " + placed, placed.rfind("string gHi = \"hi\\n\";\n\ndefault\n", 0) == 0); + const std::string placed = require_acted_on(bare, false, "\"hi", false, "Put the string in a global, 'gHi', for its 2 uses"); + UNSCOPED_INFO("ahead of the state, a blank line after: " + placed); + CHECK(placed.rfind("string gHi = \"hi\\n\";\n\ndefault\n", 0) == 0); } - template<> template<> - void object::test<29>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a list literal is written as a sum bracketed where part of more", "[ALScriptFixes]") { - set_test_name("a list written out is written as a sum, each element meaning what it meant, and the sum bracketed where it is part of more"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "list gFixed = [1, 2];\n" "default\n{\n state_entry()\n {\n integer n = 5;\n" " list l = [n, \"two\", <3.0, 3.0, 3.0>, -4, n + 3, llGetListLength(gFixed)];\n" " llOwnerSay((string)[1, 2]);\n }\n}\n"; - std::string made = actedOn(script, false, "[n,", false, "Write the list as a sum"); - ensure_equals("each where it would mean what it meant", made, + std::string made = require_acted_on(script, false, "[n,", false, "Write the list as a sum"); + UNSCOPED_INFO("each where it would mean what it meant"); + CHECK(made == "list gFixed = [1, 2];\n" "default\n{\n state_entry()\n {\n integer n = 5;\n" " list l = (list)n + \"two\" + <3.0, 3.0, 3.0> + (-4) + (n + 3) + llGetListLength(gFixed);\n" " llOwnerSay((string)[1, 2]);\n }\n}\n"); - made = actedOn(script, false, "[1, 2]);", false, "Write the list as a sum"); - ensure("bracketed under a cast: " + made, made.find("llOwnerSay((string)((list)1 + 2));") != std::string::npos); - ensure("not a global's value, which must be written out", actedOn(script, false, "[1, 2];", false, "Write the list as a sum").rfind("offered", 0) == 0); + made = require_acted_on(script, false, "[1, 2]);", false, "Write the list as a sum"); + UNSCOPED_INFO("bracketed under a cast: " + made); + REQUIRE(made.find("llOwnerSay((string)((list)1 + 2));") != std::string::npos); + UNSCOPED_INFO("not a global's value, which must be written out"); + CHECK(require_acted_on(script, false, "[1, 2];", false, "Write the list as a sum").rfind("offered", 0) == 0); } - template<> template<> - void object::test<30>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a fix that takes code out says so and one that only renames does not", "[ALScriptFixes]") { - set_test_name("a fix that takes code out says so, and one that only renames does not, so that a save makes the one and not the other"); - ensure("builtins: " + error, lslLoaded); - ensure("definitions: " + error, luauLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); const std::string state = "default\n{\n state_entry()\n {\n llOwnerSay(\"x\");\n }\n}\n"; const ALScriptProblems lsl = check("integer helper(integer x)\n{\n return x;\n}\n" + state, false); const ALScriptProblem* unused = keyed(lsl, "LSLDeclaredButNotUsed"); - ensure("said", unused != nullptr && !unused->fixes.empty()); - ensure("a removal is safe, and removes", unused->fixes.front().safe && unused->fixes.front().removes); + REQUIRE(unused != nullptr); + REQUIRE_FALSE(unused->fixes.empty()); + REQUIRE(unused->fixes.front().safe); + CHECK(unused->fixes.front().removes); const ALScriptProblems lua = check("local function f()\n local unused = 1\nend\nf()\n", true); const ALScriptProblem* local = keyed(lua, "LuauLintLocalUnused"); - ensure("said: " + said(lua), local != nullptr && !local->fixes.empty()); - ensure("a rename is safe, and removes nothing", local->fixes.front().safe && !local->fixes.front().removes); + { + INFO("said: " + said(lua)); + REQUIRE(local != nullptr); + REQUIRE_FALSE(local->fixes.empty()); + } + REQUIRE(local->fixes.front().safe); + CHECK_FALSE(local->fixes.front().removes); } - template<> template<> - void object::test<31>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: short names guess one edit away; a swap is one edit; ties not preferred", "[ALScriptFixes]") { - set_test_name("a short name gets a guess only one edit away, a swap is one edit, and names as near as each other are all offered and none preferred"); - ensure("builtins: " + error, lslLoaded); - ensure("definitions: " + error, luauLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); const std::string head = "default\n{\n state_entry()\n {\n"; const std::string tail = " }\n}\n"; // One character: nothing but itself in another case; two edits // made PI of it once. ALScriptProblems problems = check(head + " llOwnerSay((string)n);\n" + tail, false); const ALScriptProblem* problem = keyed(problems, "LSLUndeclared"); - ensure("said: " + said(problems), problem != nullptr); - ensure("no guess at n: " + titles(problem->fixes), problem->fixes.empty()); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + UNSCOPED_INFO("no guess at n: " + titles(problem->fixes)); + REQUIRE(problem->fixes.empty()); const auto unknown = [](const ALScriptProblems& all) -> const ALScriptProblem* { for (const ALScriptProblem& each : all) { @@ -975,58 +1120,80 @@ namespace tut }; problems = check("print(xy)\n", true); problem = unknown(problems); - ensure("said: " + said(problems), problem != nullptr); - ensure("no guess at xy two edits from ll: " + titles(problem->fixes), problem->fixes.empty()); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + UNSCOPED_INFO("no guess at xy two edits from ll: " + titles(problem->fixes)); + REQUIRE(problem->fixes.empty()); // Two as near: both, neither preferred. problems = check(head + " integer cat = 1;\n integer car = 2;\n llOwnerSay((string)(cat + car + cax));\n" + tail, false); problem = keyed(problems, "LSLUndeclared"); - ensure("said: " + said(problems), problem != nullptr); - ensure_equals("both: " + titles(problem->fixes), problem->fixes.size(), size_t(2)); - ensure("neither preferred", !problem->fixes[0].preferred && !problem->fixes[1].preferred); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + UNSCOPED_INFO("both: " + titles(problem->fixes)); + REQUIRE(problem->fixes.size() == size_t(2)); + REQUIRE_FALSE(problem->fixes[0].preferred); + CHECK_FALSE(problem->fixes[1].preferred); problems = check("local cat, car = 1, 2\nprint(cat, car, cax)\n", true); problem = unknown(problems); - ensure("said: " + said(problems), problem != nullptr); - ensure("both in SLua: " + titles(problem->fixes), problem->fixes.size() == 2 && !problem->fixes[0].preferred && !problem->fixes[1].preferred); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + { + INFO("both in SLua: " + titles(problem->fixes)); + REQUIRE(problem->fixes.size() == 2); + REQUIRE_FALSE(problem->fixes[0].preferred); + CHECK_FALSE(problem->fixes[1].preferred); + } // One near a short name: offered, not preferred; one near a longer // name, preferred, a swap being one edit. problems = check(head + " integer cat = 1;\n llOwnerSay((string)(cat + cta));\n" + tail, false); problem = keyed(problems, "LSLUndeclared"); - ensure("said: " + said(problems), problem != nullptr && problem->fixes.size() == 1); - ensure("a guess at three characters is not preferred", !problem->fixes.front().preferred); + { + INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + REQUIRE(problem->fixes.size() == 1); + } + UNSCOPED_INFO("a guess at three characters is not preferred"); + CHECK_FALSE(problem->fixes.front().preferred); problems = check(head + " integer total = 1;\n llOwnerSay((string)(total + totla));\n" + tail, false); problem = keyed(problems, "LSLUndeclared"); - ensure("said: " + said(problems), problem != nullptr && problem->fixes.size() == 1 && problem->fixes.front().preferred); - ensure_equals("swapped back", ALScriptFixes::editDistance("totla", "total"), size_t(1)); + { + INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + REQUIRE(problem->fixes.size() == 1); + CHECK(problem->fixes.front().preferred); + } + UNSCOPED_INFO("swapped back"); + CHECK(ALScriptFixes::editDistance("totla", "total") == size_t(1)); // A name one edit past how far a guess may be is no guess, even // where it is the only one that near. problems = check(head + " integer count = 1;\n llOwnerSay((string)(count + cqunx));\n" + tail, false); problem = keyed(problems, "LSLUndeclared"); - ensure("said: " + said(problems), problem != nullptr); - ensure("two edits from a five-letter name: no guess: " + titles(problem->fixes), problem->fixes.empty()); + UNSCOPED_INFO("said: " + said(problems)); + REQUIRE(problem != nullptr); + UNSCOPED_INFO("two edits from a five-letter name: no guess: " + titles(problem->fixes)); + REQUIRE(problem->fixes.empty()); } - template<> template<> - void object::test<32>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a fix's edits are applied in text order: insertions before replacements", "[ALScriptFixes]") { - set_test_name("a fix's edits are made in the text's order, something put in at a place before a stretch replaced from there, whatever order it gives them"); for (const bool insertion_first : { true, false }) { ALScriptFix fix; const ALScriptEdit put{ 0, 0, 0, 0, "local v = print(x)\n" }; const ALScriptEdit swap{ 0, 0, 0, 8, "v" }; fix.edits = insertion_first ? std::vector{ put, swap } : std::vector{ swap, put }; - ensure_equals(insertion_first ? "put in first" : "replaced first", ALScriptFixes::apply("print(x)\n", fix).value_or("refused"), + CAPTURE(insertion_first); + REQUIRE(ALScriptFixes::apply("print(x)\n", fix).value_or("refused") == std::string("local v = print(x)\nv\n")); } ALScriptFix two; two.edits = { { 0, 0, 0, 0, "a" }, { 0, 0, 0, 0, "b" } }; - ensure_equals("two at one place in the order given", ALScriptFixes::apply("x", two).value_or("refused"), std::string("abx")); + UNSCOPED_INFO("two at one place in the order given"); + CHECK(ALScriptFixes::apply("x", two).value_or("refused") == std::string("abx")); } - template<> template<> - void object::test<33>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: the optimizer's removal is added unpreferred; other fixes keep their mark", "[ALScriptFixes]") { - set_test_name("the optimizer's removal, offered beside the analyzer's, takes the preferred mark off its own fix only"); ALScriptProblem problem; problem.key = "OptimizerRemovedLocal"; problem.args = { "x" }; @@ -1040,112 +1207,144 @@ namespace tut problem.column = 0; problem.endColumn = 1; ALScriptFixes::attachOptimizer(problem, "integer x;\n"); - ensure_equals("nothing added", problem.fixes.size(), size_t(1)); - ensure("still preferred", problem.fixes.front().preferred); + REQUIRE(problem.fixes.size() == size_t(1)); + CHECK(problem.fixes.front().preferred); // Where it can, its own is added unpreferred. problem.line = problem.endLine = 0; problem.column = 8; problem.endColumn = 9; ALScriptFixes::attachOptimizer(problem, "integer x;\n"); - ensure("added: " + std::to_string(problem.fixes.size()), problem.fixes.size() == 2 && !problem.fixes.back().preferred && problem.fixes.front().preferred); + REQUIRE(problem.fixes.size() == 2); + CHECK_FALSE(problem.fixes.back().preferred); + CHECK(problem.fixes.front().preferred); } - template<> template<> - void object::test<34>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: LSL's operators and zero indexes in SLua fixed; only ~= is safe", "[ALScriptFixes]") { - set_test_name("SLua written with LSL's habits -- != && || ! -- changed to SLua's words, only ~= safe; an index or a loop from 0 counted from 1"); - ensure_equals("~=", fixed("local a, b = 1, 2\nif a != b then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '!=' to '~='"), + UNSCOPED_INFO("~="); + CHECK(require_fixed("local a, b = 1, 2\nif a != b then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '!=' to '~='") == std::string("local a, b = 1, 2\nif a ~= b then print(1) end\n")); - ensure_equals("and, apart from what it joins", fixed("local a, b = true, false\nif a&&b then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '&&' to 'and'"), + UNSCOPED_INFO("and, apart from what it joins"); + CHECK(require_fixed("local a, b = true, false\nif a&&b then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '&&' to 'and'") == std::string("local a, b = true, false\nif a and b then print(1) end\n")); - ensure_equals("or", fixed("local a, b = true, false\nif a || b then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '||' to 'or'"), + UNSCOPED_INFO("or"); + CHECK(require_fixed("local a, b = true, false\nif a || b then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '||' to 'or'") == std::string("local a, b = true, false\nif a or b then print(1) end\n")); - ensure_equals("not, apart from its operand", fixed("local a = true\nif(!a) then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '!' to 'not'"), + UNSCOPED_INFO("not, apart from its operand"); + CHECK(require_fixed("local a = true\nif(!a) then print(1) end\n", true, "LuauUnexpectedDidYouMean", "Change '!' to 'not'") == std::string("local a = true\nif(not a) then print(1) end\n")); const ALScriptProblems compared = check("local a, b = 1, 2\nif a != b then print(1) end\n", true); const ALScriptProblems denied = check("local a = true\nif !a then print(1) end\n", true); const ALScriptProblem* unequal = keyed(compared, "LuauUnexpectedDidYouMean"); const ALScriptProblem* negated = keyed(denied, "LuauUnexpectedDidYouMean"); - ensure("~= safe, the same whatever it compares", unequal && unequal->fixes.front().safe); - ensure("not unsafe: 0 is true in SLua", negated && !negated->fixes.front().safe); + { + INFO("~= safe, the same whatever it compares"); + REQUIRE(unequal); + CHECK(unequal->fixes.front().safe); + } + { + INFO("not unsafe: 0 is true in SLua"); + REQUIRE(negated); + CHECK_FALSE(negated->fixes.front().safe); + } - ensure_equals("an index from 0", fixed("local t = {}\ntable.insert(t, 0, 5)\n", true, "LuauLintTableInsertZero", "Change '0' to '1'"), + UNSCOPED_INFO("an index from 0"); + CHECK(require_fixed("local t = {}\ntable.insert(t, 0, 5)\n", true, "LuauLintTableInsertZero", "Change '0' to '1'") == std::string("local t = {}\ntable.insert(t, 1, 5)\n")); - ensure_equals("a loop from 0", fixed("local t = {1, 2}\nfor i = 0, #t do print(t[i]) end\n", true, "LuauLintForRangeZero", "Change '0' to '1'"), + UNSCOPED_INFO("a loop from 0"); + CHECK(require_fixed("local t = {1, 2}\nfor i = 0, #t do print(t[i]) end\n", true, "LuauLintForRangeZero", "Change '0' to '1'") == std::string("local t = {1, 2}\nfor i = 1, #t do print(t[i]) end\n")); } - template<> template<> - void object::test<35>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: LSL fixes: an if's ';' and event parameters and untyped ones and braces", "[ALScriptFixes]") { - set_test_name("LSL: an if ended by its ';', an event's parameters as it takes them, a parameter with no type, a declaration braced, a missing ';' safe"); - ensure_equals("the if's ';' out", fixed("default { state_entry() { integer x = llGetUnixTime(); if (x); llOwnerSay(\"a\"); } }", false, "LSLEmptyIf", - "Take out the ';' that ends the if"), + UNSCOPED_INFO("the if's ';' out"); + CHECK(require_fixed("default { state_entry() { integer x = llGetUnixTime(); if (x); llOwnerSay(\"a\"); } }", false, "LSLEmptyIf", + "Take out the ';' that ends the if") == std::string("default { state_entry() { integer x = llGetUnixTime(); if (x) llOwnerSay(\"a\"); } }")); - ensure_equals("a wrong type, the name kept", fixed("default { touch_start(string n) { llOwnerSay((string)n); } }", false, "LSLArgumentWrongTypeEvent", - "Write the parameters 'touch_start' takes"), + UNSCOPED_INFO("a wrong type, the name kept"); + CHECK(require_fixed("default { touch_start(string n) { llOwnerSay((string)n); } }", false, "LSLArgumentWrongTypeEvent", + "Write the parameters 'touch_start' takes") == std::string("default { touch_start(integer n) { llOwnerSay((string)n); } }")); - ensure_equals("too few, the builtins' names", fixed("default { touch_start() { } }", false, "LSLTooFewArgumentsEvent", "Write the parameters 'touch_start' takes"), + UNSCOPED_INFO("too few, the builtins' names"); + CHECK(require_fixed("default { touch_start() { } }", false, "LSLTooFewArgumentsEvent", "Write the parameters 'touch_start' takes") == std::string("default { touch_start(integer num_detected) { } }")); - ensure_equals("too many", fixed("default { touch_start(integer n, integer m) { } }", false, "LSLTooManyArgumentsEvent", "Write the parameters 'touch_start' takes"), + UNSCOPED_INFO("too many"); + CHECK(require_fixed("default { touch_start(integer n, integer m) { } }", false, "LSLTooManyArgumentsEvent", "Write the parameters 'touch_start' takes") == std::string("default { touch_start(integer n) { } }")); - ensure_equals("an event's with no type", fixed("default { touch_start(n) { } }", false, "LSLParameterUntyped", "Write the parameters 'touch_start' takes"), + UNSCOPED_INFO("an event's with no type"); + CHECK(require_fixed("default { touch_start(n) { } }", false, "LSLParameterUntyped", "Write the parameters 'touch_start' takes") == std::string("default { touch_start(integer n) { } }")); - ensure_equals("a declaration as an if's body, braced", fixed("default { state_entry() { if (llGetUnixTime()) integer i = 1; } }", false, "LSLDeclarationInvalidHere", - "Put it in braces of its own"), + UNSCOPED_INFO("a declaration as an if's body, braced"); + CHECK(require_fixed("default { state_entry() { if (llGetUnixTime()) integer i = 1; } }", false, "LSLDeclarationInvalidHere", + "Put it in braces of its own") == std::string("default { state_entry() { if (llGetUnixTime()) { integer i = 1; } } }")); const std::string function = "say(x) { llOwnerSay((string)x); }\ndefault { state_entry() { say(1); } }"; const ALScriptProblems untyped = check(function, false); const ALScriptProblem* bare = keyed(untyped, "LSLParameterUntyped"); - ensure("a function's: every type, none preferred", bare && bare->fixes.size() == 7 && - std::none_of(bare->fixes.begin(), bare->fixes.end(), [](const ALScriptFix& fix) { return fix.preferred; })); + { + INFO("a function's: every type, none preferred"); + REQUIRE(bare); + REQUIRE(bare->fixes.size() == 7); + CHECK(std::none_of(bare->fixes.begin(), bare->fixes.end(), [](const ALScriptFix& fix) { return fix.preferred; })); + } const std::optional typed = ALScriptFixes::apply(function, bare->fixes.front()); - ensure("declared as the first", typed && typed->rfind("say(integer x)", 0) == 0 && errors(check(*typed, false)) == 0); + { + INFO("declared as the first"); + REQUIRE(typed); + REQUIRE(typed->rfind("say(integer x)", 0) == 0); + CHECK(errors(check(*typed, false)) == 0); + } const ALScriptProblems missing = check("default { state_entry() { integer x = 1 llOwnerSay(\"a\"); } }", false); const ALScriptProblem* ended = keyed(missing, "LSLSyntaxMissing"); - ensure("a missing ';' put in safely", ended && !ended->fixes.empty() && ended->fixes.front().safe); + { + INFO("a missing ';' put in safely"); + REQUIRE(ended); + REQUIRE_FALSE(ended->fixes.empty()); + CHECK(ended->fixes.front().safe); + } } - template<> template<> - void object::test<36>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a handler's missing brace goes on its own line level with the next one", "[ALScriptFixes]") { - set_test_name("a handler's missing brace is put on a line of its own, level with the handler after it, and the script checks clean"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "default\n{\n state_entry()\n {\n llSay(0, \"Hello, Avatar!\");\n\n" " touch_start(integer total_number)\n {\n llSay(0, \"Touched.\");\n }\n}\n"; - const std::string made = fixed(script, false, "LSLSyntaxMissing", "Insert '}'"); - ensure("closed after the call, its own line: " + made, made.find("\"Hello, Avatar!\");\n }\n\n touch_start(") != std::string::npos); - ensure("no error left", errors(check(made, false)) == 0); + const std::string made = require_fixed(script, false, "LSLSyntaxMissing", "Insert '}'"); + UNSCOPED_INFO("closed after the call, its own line: " + made); + REQUIRE(made.find("\"Hello, Avatar!\");\n }\n\n touch_start(") != std::string::npos); + CHECK(errors(check(made, false)) == 0); } - template<> template<> - void object::test<37>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: SlCompoundAssign's fix writes x op= y safely; a field and a join too", "[ALScriptFixes]") { - set_test_name("SlCompoundAssign's fix: x = x op y written x op= y, safe; a field and a join too"); - ensure("definitions: " + error, luauLoaded); - const std::string made = fixed("local x = 1\nx = x + 2 * x\nprint(x)\n", true, "LuauLintSlCompoundAssign", "Write it x += ..."); - ensure_equals("the text", made, std::string("local x = 1\nx += 2 * x\nprint(x)\n")); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + const std::string made = require_fixed("local x = 1\nx = x + 2 * x\nprint(x)\n", true, "LuauLintSlCompoundAssign", "Write it x += ..."); + CHECK(made == std::string("local x = 1\nx += 2 * x\nprint(x)\n")); const ALScriptProblems problems = check("local x = 1\nx = x + 2 * x\nprint(x)\n", true); const ALScriptProblem* problem = keyed(problems, "LuauLintSlCompoundAssign"); - ensure("safe", problem && problem->fixes.front().safe); - ensure_equals("a field, joined", fixed("local t = { n = \"a\" }\nt.n = t.n ..\"b\" .. \"c\"\nprint(t.n)\n", true, "LuauLintSlCompoundAssign", - "Write it t.n ..= ..."), + REQUIRE(problem); + CHECK(problem->fixes.front().safe); + UNSCOPED_INFO("a field, joined"); + CHECK(require_fixed("local t = { n = \"a\" }\nt.n = t.n ..\"b\" .. \"c\"\nprint(t.n)\n", true, "LuauLintSlCompoundAssign", + "Write it t.n ..= ...") == std::string("local t = { n = \"a\" }\nt.n ..= \"b\" .. \"c\"\nprint(t.n)\n")); } - template<> template<> - void object::test<38>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: SlLoopInvariantCall's fix: the call in a fresh local before the loop", "[ALScriptFixes]") { - set_test_name("SlLoopInvariantCall's fix: the call in a local before the loop, named as nothing else is, and read in the check; not safe"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "default { state_entry() {\n" " list l = [1, 2]; integer length = 3; integer i;\n" " for (i = 0; i < llGetListLength(l); ++i) llOwnerSay((string)(i + length));\n" "} }\n"; - const std::string made = fixed(script, false, "LSLSlLoopInvariantCall", "Work out llGetListLength once, before the loop"); - ensure_equals("the text", made, + const std::string made = require_fixed(script, false, "LSLSlLoopInvariantCall", "Work out llGetListLength once, before the loop"); + CHECK(made == std::string("default { state_entry() {\n" " list l = [1, 2]; integer length = 3; integer i;\n" " integer length2 = llGetListLength(l);\n" @@ -1153,7 +1352,8 @@ namespace tut "} }\n")); const ALScriptProblems problems = check(script, false); const ALScriptProblem* problem = keyed(problems, "LSLSlLoopInvariantCall"); - ensure("not safe", problem && !problem->fixes.front().safe); + REQUIRE(problem); + CHECK_FALSE(problem->fixes.front().safe); // Not where the loop is all its if has: a declaration may not be. const ALScriptProblems in_if = check("default { state_entry() {\n" " list l; integer i;\n" @@ -1161,58 +1361,67 @@ namespace tut "} }\n", false); const ALScriptProblem* alone = keyed(in_if, "LSLSlLoopInvariantCall"); - ensure("said, with no fix", alone && alone->fixes.empty()); + REQUIRE(alone); + REQUIRE(alone->fixes.empty()); } - template<> template<> - void object::test<39>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: SlNumberTruth's fixes: n ~= 0 bracketed where needed and not n as n == 0", "[ALScriptFixes]") { - set_test_name("SlNumberTruth's fixes: n ~= 0, an if-then-else bracketed first, and not n as n == 0; not safe"); - ensure("definitions: " + error, luauLoaded); - ensure_equals("compared", fixed("local n = 0\nif n then print(n) end\n", true, "LuauLintSlNumberTruth", "Compare it: n ~= 0"), + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + UNSCOPED_INFO("compared"); + CHECK(require_fixed("local n = 0\nif n then print(n) end\n", true, "LuauLintSlNumberTruth", "Compare it: n ~= 0") == std::string("local n = 0\nif n ~= 0 then print(n) end\n")); - ensure_equals("bracketed", fixed("local c = true\nif (if c then 1 else 2) then print(c) end\n", true, "LuauLintSlNumberTruth", - "Compare it: if c then 1 else 2 ~= 0"), + UNSCOPED_INFO("bracketed"); + CHECK(require_fixed("local c = true\nif (if c then 1 else 2) then print(c) end\n", true, "LuauLintSlNumberTruth", + "Compare it: if c then 1 else 2 ~= 0") == std::string("local c = true\nif ((if c then 1 else 2) ~= 0) then print(c) end\n")); - ensure_equals("not", fixed("local n = 0\nprint(not n)\n", true, "LuauLintSlNumberTruthNot", "Ask whether it is 0: n == 0"), + UNSCOPED_INFO("not"); + CHECK(require_fixed("local n = 0\nprint(not n)\n", true, "LuauLintSlNumberTruthNot", "Ask whether it is 0: n == 0") == std::string("local n = 0\nprint(n == 0)\n")); const ALScriptProblems problems = check("local n = 0\nif n then print(n) end\n", true); const ALScriptProblem* problem = keyed(problems, "LuauLintSlNumberTruth"); - ensure("not safe", problem && !problem->fixes.front().safe); + REQUIRE(problem); + CHECK_FALSE(problem->fixes.front().safe); } - template<> template<> - void object::test<40>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: SlNilSentinel's fixes: nil for SLua's finds and -1 for llcompat's; safe", "[ALScriptFixes]") { - set_test_name("SlNilSentinel's fixes: nil where SLua's find answers it, -1 where llcompat's does; a find on the right put on the left; safe"); - ensure("definitions: " + error, luauLoaded); - ensure_equals("never", fixed("local l = {1}\nif ll.ListFindList(l, {2}) == -1 then print(l) end\n", true, "LuauLintSlNilSentinel", - "Write it ll.ListFindList(...) == nil"), + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + UNSCOPED_INFO("never"); + CHECK(require_fixed("local l = {1}\nif ll.ListFindList(l, {2}) == -1 then print(l) end\n", true, "LuauLintSlNilSentinel", + "Write it ll.ListFindList(...) == nil") == std::string("local l = {1}\nif ll.ListFindList(l, {2}) == nil then print(l) end\n")); - ensure_equals("on the right", fixed("local i = table.find({1}, 1)\nprint(-1 ~= i)\n", true, "LuauLintSlNilSentinelAlways", "Write it i ~= nil"), + UNSCOPED_INFO("on the right"); + CHECK(require_fixed("local i = table.find({1}, 1)\nprint(-1 ~= i)\n", true, "LuauLintSlNilSentinelAlways", "Write it i ~= nil") == std::string("local i = table.find({1}, 1)\nprint(i ~= nil)\n")); - ensure_equals("ordered, bracketed", fixed("local i = ll.SubStringIndex(\"ab\", \"b\")\nprint((i) < 0)\n", true, "LuauLintSlNilSentinelOrder", - "Write it i == nil"), + UNSCOPED_INFO("ordered, bracketed"); + CHECK(require_fixed("local i = ll.SubStringIndex(\"ab\", \"b\")\nprint((i) < 0)\n", true, "LuauLintSlNilSentinelOrder", + "Write it i == nil") == std::string("local i = ll.SubStringIndex(\"ab\", \"b\")\nprint((i) == nil)\n")); - ensure_equals("ordered, on the right", fixed("local i = ll.SubStringIndex(\"ab\", \"b\")\nprint(0 <= i)\n", true, - "LuauLintSlNilSentinelOrder", "Write it i ~= nil"), + UNSCOPED_INFO("ordered, on the right"); + CHECK(require_fixed("local i = ll.SubStringIndex(\"ab\", \"b\")\nprint(0 <= i)\n", true, + "LuauLintSlNilSentinelOrder", "Write it i ~= nil") == std::string("local i = ll.SubStringIndex(\"ab\", \"b\")\nprint(i ~= nil)\n")); - ensure_equals("llcompat's", fixed("print(llcompat.ListFindList({1}, {1}) ~= nil)\n", true, "LuauLintSlNilSentinelAlways", - "Write it llcompat.ListFindList(...) ~= -1"), + UNSCOPED_INFO("llcompat's"); + CHECK(require_fixed("print(llcompat.ListFindList({1}, {1}) ~= nil)\n", true, "LuauLintSlNilSentinelAlways", + "Write it llcompat.ListFindList(...) ~= -1") == std::string("print(llcompat.ListFindList({1}, {1}) ~= -1)\n")); const ALScriptProblems problems = check("print(table.find({1}, 1) == -1)\n", true); const ALScriptProblem* problem = keyed(problems, "LuauLintSlNilSentinel"); - ensure("safe", problem && !problem->fixes.empty() && problem->fixes.front().safe); + REQUIRE(problem); + REQUIRE_FALSE(problem->fixes.empty()); + CHECK(problem->fixes.front().safe); } - template<> template<> - void object::test<41>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a deprecated ll function is renamed only where the arguments match", "[ALScriptFixes]") { - set_test_name("a deprecated ll function's replacement put in by name only where it takes the same arguments: not " - "ll.SetPrimitiveParams', which wants a link first"); - ensure("definitions: " + error, luauLoaded); - ensure_equals("the same arguments", fixed("ll.SetLinkPrimitiveParams(LINK_THIS, {})\n", true, "LuauLintDeprecatedMemberUse", - "Use 'll.SetLinkPrimitiveParamsFast' instead of 'll.SetLinkPrimitiveParams'"), + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); + UNSCOPED_INFO("the same arguments"); + CHECK(require_fixed("ll.SetLinkPrimitiveParams(LINK_THIS, {})\n", true, "LuauLintDeprecatedMemberUse", + "Use 'll.SetLinkPrimitiveParamsFast' instead of 'll.SetLinkPrimitiveParams'") == std::string("ll.SetLinkPrimitiveParamsFast(LINK_THIS, {})\n")); for (const char* call : { "ll.SetPrimitiveParams({})\n", "ll.MakeFire(1, 1, 1, 1, 1, \"\", vector(0, 0, 0))\n" }) { @@ -1223,41 +1432,49 @@ namespace tut { for (const ALScriptFix& fix : problem.fixes) { - ensure("no replacement by name: " + std::string(call) + fix.title, fix.key != "ScriptFixUseInstead"); + CAPTURE(call, fix.title); + REQUIRE(fix.key != "ScriptFixUseInstead"); } } } } } - template<> template<> - void object::test<42>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: making a script --!strict changes its head's mode or puts it first", "[ALScriptFixes]") { - set_test_name("a script made --!strict: its head's --!nonstrict or --!nocheck changed, else it put first; nothing where it is already"); const auto made = [](const std::string& text) { const std::optional fix = ALScriptFixes::strictFix(text); return fix ? ALScriptFixes::apply(text, *fix).value_or("(not made)") : std::string("(none)"); }; - ensure_equals("put first", made("-- a door\nlocal x = 1\n"), std::string("--!strict\n-- a door\nlocal x = 1\n")); - ensure_equals("changed", made("-- a door\n--!nonstrict\nlocal x = 1\n"), std::string("-- a door\n--!strict\nlocal x = 1\n")); - ensure_equals("from nocheck", made("--!nocheck\nprint(1)\n"), std::string("--!strict\nprint(1)\n")); - ensure_equals("already", made("--!strict\nprint(1)\n"), std::string("(none)")); - ensure_equals("past the head, not read", made("print(1)\n--!nonstrict\n"), std::string("--!strict\nprint(1)\n--!nonstrict\n")); + UNSCOPED_INFO("put first"); + CHECK(made("-- a door\nlocal x = 1\n") == std::string("--!strict\n-- a door\nlocal x = 1\n")); + UNSCOPED_INFO("changed"); + CHECK(made("-- a door\n--!nonstrict\nlocal x = 1\n") == std::string("-- a door\n--!strict\nlocal x = 1\n")); + UNSCOPED_INFO("from nocheck"); + CHECK(made("--!nocheck\nprint(1)\n") == std::string("--!strict\nprint(1)\n")); + UNSCOPED_INFO("already"); + CHECK(made("--!strict\nprint(1)\n") == std::string("(none)")); + UNSCOPED_INFO("past the head, not read"); + CHECK(made("print(1)\n--!nonstrict\n") == std::string("--!strict\nprint(1)\n--!nonstrict\n")); } - template<> template<> - void object::test<43>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: an unread require is offered for removal neither safe nor preferred", "[ALScriptFixes]") { - set_test_name("a require nothing reads may be taken out, its line with it: offered after marking it unused, neither safe nor preferred, since a require runs its module"); - ensure("definitions: " + error, luauLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); // The fixture's statics named by its class: MSVC finds none of them // from a lambda, even one holding `this`. - const auto removal = [this](const std::string& script) -> const ALScriptFix* { + const auto require_removal = [this](const std::string& script) -> const ALScriptFix* { static ALScriptProblems problems; problems = check(script, true); const ALScriptProblem* problem = alscriptfixes_data::keyed(problems, "LuauLintImportUnused"); - ensure("said: " + alscriptfixes_data::said(problems), problem != nullptr && !problem->fixes.empty()); - ensure_equals("marking it unused first", problem->fixes.front().title.substr(0, 9), std::string("Rename to")); + { + INFO("said: " + alscriptfixes_data::said(problems)); + REQUIRE(problem != nullptr); + REQUIRE_FALSE(problem->fixes.empty()); + } + UNSCOPED_INFO("marking it unused first"); + REQUIRE(problem->fixes.front().title.substr(0, 9) == std::string("Rename to")); for (const ALScriptFix& fix : problem->fixes) { if (fix.title.rfind("Remove the require", 0) == 0) @@ -1268,31 +1485,32 @@ namespace tut return nullptr; }; const std::string script = "local util = require(\"util\")\nprint(1)\n"; - const ALScriptFix* fix = removal(script); - ensure("offered", fix != nullptr); - ensure_equals("its words", fix->title, std::string("Remove the require of \"util\"")); - ensure("neither preferred nor safe", !fix->preferred && !fix->safe); - ensure_equals("its line gone", ALScriptFixes::apply(script, *fix).value_or("(not made)"), std::string("print(1)\n")); + const ALScriptFix* fix = require_removal(script); + REQUIRE(fix != nullptr); + CHECK(fix->title == std::string("Remove the require of \"util\"")); + REQUIRE_FALSE(fix->preferred); + CHECK_FALSE(fix->safe); + UNSCOPED_INFO("its line gone"); + CHECK(ALScriptFixes::apply(script, *fix).value_or("(not made)") == std::string("print(1)\n")); const std::string bare = "local util = require 'util'; print(1)\n"; - fix = removal(bare); - ensure("without brackets, and its ;", fix != nullptr); - ensure_equals("the statement alone gone", ALScriptFixes::apply(bare, *fix).value_or("(not made)"), std::string("print(1)\n")); - - ensure("a module named by an expression: nothing to name", removal("local name = 'util'\nlocal util = require(name)\n") == nullptr); + fix = require_removal(bare); + UNSCOPED_INFO("without brackets, and its ;"); + REQUIRE(fix != nullptr); + UNSCOPED_INFO("the statement alone gone"); + CHECK(ALScriptFixes::apply(bare, *fix).value_or("(not made)") == std::string("print(1)\n")); + + UNSCOPED_INFO("a module named by an expression: nothing to name"); + REQUIRE(require_removal("local name = 'util'\nlocal util = require(name)\n") == nullptr); } - template<> template<> - void object::test<44>() + TEST_CASE_METHOD(alscriptfixes_data, "ALScriptFixes: a deprecated call with other arguments is rewritten as a call that works", "[ALScriptFixes]") { - set_test_name("a deprecated call whose replacement takes other arguments is written as a call that works -- ll.VecDist's " - "vector.magnitude(a - b), ll.Round's half up, ll.SetPrimitiveParams' link, table.getn's # -- an argument " - "bracketed where it does not bind as one; none where the event's table is wanted, the call does not close " - "on its line or is not made; vector.normalize not preferred; LSL's llParticleSystem not put for llMakeFire"); - ensure("definitions: " + error, luauLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(luauLoaded); // The fixture's statics named by its class: MSVC finds none of them // from a lambda, even one holding `this`. - const auto deprecated = [this](const std::string& script) -> const ALScriptProblem* { + const auto require_deprecated = [this](const std::string& script) -> const ALScriptProblem* { static ALScriptProblems problems; problems = check(script, true); for (const ALScriptProblem& problem : problems) @@ -1302,52 +1520,72 @@ namespace tut return &problem; } } - ensure("said: " + alscriptfixes_data::said(problems), false); + FAIL("no deprecation said:\n" + alscriptfixes_data::said(problems)); return nullptr; }; - const auto written = [&](const std::string& script) { - const ALScriptProblem* problem = deprecated(script); - ensure("a fix: " + script, !problem->fixes.empty()); + const auto require_written = [&](const std::string& script) { + CAPTURE(script); + const ALScriptProblem* problem = require_deprecated(script); + REQUIRE_FALSE(problem->fixes.empty()); const ALScriptFix& fix = problem->fixes.front(); - ensure("preferred, not safe: " + fix.title, fix.preferred && !fix.safe); + CAPTURE(fix.title); + REQUIRE(fix.preferred); + REQUIRE_FALSE(fix.safe); const std::string made = ALScriptFixes::apply(script, fix).value_or("(not made)"); const ALScriptProblems after = check(made, true); + INFO("made:\n" + made + "\nsaid then:\n" + alscriptfixes_data::said(after)); for (const ALScriptProblem& each : after) { - ensure("no longer deprecated:\n" + made, each.key.rfind("LuauLintDeprecated", 0) != 0); + REQUIRE(each.key.rfind("LuauLintDeprecated", 0) != 0); } - ensure_equals("no errors:\n" + made + "\n" + alscriptfixes_data::said(after), alscriptfixes_data::errors(after), static_cast(0)); + REQUIRE(alscriptfixes_data::errors(after) == static_cast(0)); return fix.title + "\n" + made; }; const std::string vectors = "local a, b = vector(1, 0, 0), vector(0, 1, 0)\n"; - ensure_equals("a distance", written(vectors + "print(ll.VecDist(a, ll.GetPos()))\n"), + UNSCOPED_INFO("a distance"); + CHECK(require_written(vectors + "print(ll.VecDist(a, ll.GetPos()))\n") == "Write it vector.magnitude(a - ll.GetPos())\n" + vectors + "print(vector.magnitude(a - ll.GetPos()))\n"); - ensure_equals("bracketed", written(vectors + "print(ll.VecDist(a + b, b))\n"), + UNSCOPED_INFO("bracketed"); + CHECK(require_written(vectors + "print(ll.VecDist(a + b, b))\n") == "Write it vector.magnitude((a + b) - b)\n" + vectors + "print(vector.magnitude((a + b) - b))\n"); - ensure_equals("half up", written("local n = 2\nprint(ll.Round(n * 0.25))\n"), + UNSCOPED_INFO("half up"); + CHECK(require_written("local n = 2\nprint(ll.Round(n * 0.25))\n") == std::string("Write it math.floor((n * 0.25) + 0.5)\nlocal n = 2\nprint(math.floor((n * 0.25) + 0.5))\n")); - ensure_equals("a link first", written("ll.SetPrimitiveParams({PRIM_SIZE, vector(1, 1, 1)})\n"), + UNSCOPED_INFO("a link first"); + CHECK(require_written("ll.SetPrimitiveParams({PRIM_SIZE, vector(1, 1, 1)})\n") == std::string("Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_SIZE, vector(1, 1, 1)})\n" "ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_SIZE, vector(1, 1, 1)})\n")); - ensure_equals("a length", written("local t = {1, 2}\nprint(table.getn(t))\n"), + UNSCOPED_INFO("a length"); + CHECK(require_written("local t = {1, 2}\nprint(table.getn(t))\n") == std::string("Write it #t\nlocal t = {1, 2}\nprint(#t)\n")); - ensure_equals("a name", written("print(ll.Abs(-1))\n"), std::string("Use 'math.abs' instead of 'll.Abs'\nprint(math.abs(-1))\n")); + UNSCOPED_INFO("a name"); + CHECK(require_written("print(ll.Abs(-1))\n") == std::string("Use 'math.abs' instead of 'll.Abs'\nprint(math.abs(-1))\n")); - const ALScriptProblem* norm = deprecated("print(ll.VecNorm(vector(0, 0, 0)))\n"); - ensure("normalize offered: " + alscriptfixes_data::said({ *norm }), !norm->fixes.empty() && norm->fixes.front().title == "Use 'vector.normalize' instead of 'll.VecNorm'"); - ensure("not preferred, NaN at zero", !norm->fixes.front().preferred); + const ALScriptProblem* norm = require_deprecated("print(ll.VecNorm(vector(0, 0, 0)))\n"); + { + INFO("normalize offered: " + alscriptfixes_data::said({ *norm })); + REQUIRE_FALSE(norm->fixes.empty()); + CHECK(norm->fixes.front().title == "Use 'vector.normalize' instead of 'll.VecNorm'"); + } + UNSCOPED_INFO("not preferred, NaN at zero"); + CHECK_FALSE(norm->fixes.front().preferred); for (const char* script : { "LLEvents:on(\"touch_start\", function(detected) print(ll.DetectedKey(0)) end)\n", "local a = vector(1, 0, 0)\nprint(ll.VecDist(a,\n a))\n", "local f = ll.VecDist\nprint(f)\n", "print(ll.GetListEntryType({1}, 0))\n" }) { - const ALScriptProblem* problem = deprecated(script); - ensure("no fix: " + std::string(script) + alscriptfixes_data::said({ *problem }), problem->fixes.empty()); + CAPTURE(script); + const ALScriptProblem* problem = require_deprecated(script); + INFO(alscriptfixes_data::said({ *problem })); + REQUIRE(problem->fixes.empty()); } - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const ALScriptProblems lsl = check("default\n{\n state_entry()\n {\n llMakeFire(1, 1.0, 1.0, 1.0, 1.0, \"\", ZERO_VECTOR);\n }\n}\n", false); const ALScriptProblem* fire = alscriptfixes_data::keyed(lsl, "LSLDeprecatedWithReplacement"); - ensure("said: " + alscriptfixes_data::said(lsl), fire != nullptr); - ensure("no llParticleSystem by name: " + alscriptfixes_data::said(lsl), fire->fixes.empty()); + UNSCOPED_INFO("said: " + alscriptfixes_data::said(lsl)); + REQUIRE(fire != nullptr); + UNSCOPED_INFO("no llParticleSystem by name: " + alscriptfixes_data::said(lsl)); + REQUIRE(fire->fixes.empty()); } } diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index f74d8ef06e..9b2153db23 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -26,9 +26,9 @@ #include "../core/alscriptformatter.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { struct alscriptformatter_data { @@ -44,13 +44,9 @@ namespace tut return ALScriptFormatter::format(text, options); } }; - typedef test_group alscriptformatter_group; - typedef alscriptformatter_group::object alscriptformatter_object; - tut::alscriptformatter_group alscriptformatter_instance("alscriptformatter"); // LSL: braces, a hanging statement, a list across lines, a directive. - template<> template<> - void alscriptformatter_object::test<1>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: LSL: braces; a hanging statement; a list across lines; a directive", "[alscriptformatter]") { const std::string in = "#define X 1\n" @@ -86,31 +82,34 @@ namespace tut " }\n" " }\n" "}\n"; - ensure_equals("lsl structure", lsl(in), want); + CHECK(lsl(in) == want); } // LSL spacing: operators, unary signs, vectors, casts, comments. - template<> template<> - void alscriptformatter_object::test<2>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: LSL spacing: operators; unary signs; vectors; casts; comments", "[alscriptformatter]") { - ensure_equals("binary", lsl("x=a+b*c;\n"), std::string("x = a + b * c;\n")); - ensure_equals("unary", lsl("x = -a * -b;\n"), std::string("x = -a * -b;\n")); - ensure_equals("not", lsl("if(!x)y=~z;\n"), std::string("if (!x) y = ~z;\n")); - ensure_equals("vector", lsl("v=<1,-2,3>*<0,0,1>;\n"), std::string("v = <1, -2, 3> * <0, 0, 1>;\n")); - ensure_equals("comparison", lsl("if(ad)x++;\n"), std::string("if (a < b && c > d) x++;\n")); - ensure_equals("cast kept", lsl("x=(integer)y;\n"), std::string("x = (integer)y;\n")); - ensure_equals("cast spaced kept", lsl("x=(integer) y;\n"), std::string("x = (integer) y;\n")); - ensure_equals("for", lsl("for(i=0;i*<0,0,1>;\n") == std::string("v = <1, -2, 3> * <0, 0, 1>;\n")); + CHECK(lsl("if(ad)x++;\n") == std::string("if (a < b && c > d) x++;\n")); + UNSCOPED_INFO("cast kept"); + CHECK(lsl("x=(integer)y;\n") == std::string("x = (integer)y;\n")); + UNSCOPED_INFO("cast spaced kept"); + CHECK(lsl("x=(integer) y;\n") == std::string("x = (integer) y;\n")); + CHECK(lsl("for(i=0;i template<> - void alscriptformatter_object::test<3>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: Luau: blocks by keyword; a function in a call; a table; its own spacing", "[alscriptformatter]") { const std::string in = "local function f(a,b)\n" @@ -150,62 +149,73 @@ namespace tut " a = 1,\n" " b = { 1, 2 },\n" "}\n"; - ensure_equals("luau structure", lua(in), want); - ensure_equals("generics kept", lua("local x: Array = {}\n"), std::string("local x: Array = {}\n")); - ensure_equals("method kept", lua("obj:method(1)\n"), std::string("obj:method(1)\n")); - ensure_equals("not", lua("if not(x)then end\n"), std::string("if not (x) then end\n")); - ensure_equals("long string kept", lua("s = [[\n a\n]]\nx=1\n"), std::string("s = [[\n a\n]]\nx = 1\n")); + CHECK(lua(in) == want); + UNSCOPED_INFO("generics kept"); + CHECK(lua("local x: Array = {}\n") == std::string("local x: Array = {}\n")); + UNSCOPED_INFO("method kept"); + CHECK(lua("obj:method(1)\n") == std::string("obj:method(1)\n")); + CHECK(lua("if not(x)then end\n") == std::string("if not (x) then end\n")); + UNSCOPED_INFO("long string kept"); + CHECK(lua("s = [[\n a\n]]\nx=1\n") == std::string("s = [[\n a\n]]\nx = 1\n")); } // Blank lines, trailing whitespace, and the end of the file. - template<> template<> - void alscriptformatter_object::test<4>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: blank lines; trailing whitespace; the end of the file", "[alscriptformatter]") { - ensure_equals("blanks shortened", lsl("\n\nx = 1; \n\n\n\n\ny = 2;\n\n\n"), std::string("x = 1;\n\n\ny = 2;\n")); - ensure_equals("a newline added", lsl("x = 1;"), std::string("x = 1;\n")); - ensure_equals("tabs", [] { + UNSCOPED_INFO("blanks shortened"); + CHECK(lsl("\n\nx = 1; \n\n\n\n\ny = 2;\n\n\n") == std::string("x = 1;\n\n\ny = 2;\n")); + UNSCOPED_INFO("a newline added"); + CHECK(lsl("x = 1;") == std::string("x = 1;\n")); + UNSCOPED_INFO("tabs"); + CHECK([] { ALScriptFormatter::Options options; options.tabs = true; return ALScriptFormatter::format("{\nx;\n}\n", options); - }(), std::string("{\n\tx;\n}\n")); + }() == std::string("{\n\tx;\n}\n")); } // Only some lines, the rest as written and every line kept. - template<> template<> - void alscriptformatter_object::test<5>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: only some lines formatted with the rest as written and every line kept", "[alscriptformatter]") { const std::string in = "default\n{\nstate_entry()\n{\n\n\nx=1;\n}\n}\n"; ALScriptFormatter::Options options; - ensure_equals("one line", ALScriptFormatter::formatLines(in, options, 6, 6), std::string("default\n{\nstate_entry()\n{\n\n\n x = 1;\n}\n}\n")); - ensure_equals("blanks kept", ALScriptFormatter::formatLines(in, options, 4, 7), std::string("default\n{\nstate_entry()\n{\n\n\n x = 1;\n }\n}\n")); + UNSCOPED_INFO("one line"); + CHECK(ALScriptFormatter::formatLines(in, options, 6, 6) == std::string("default\n{\nstate_entry()\n{\n\n\n x = 1;\n}\n}\n")); + UNSCOPED_INFO("blanks kept"); + CHECK(ALScriptFormatter::formatLines(in, options, 4, 7) == std::string("default\n{\nstate_entry()\n{\n\n\n x = 1;\n }\n}\n")); // A string across lines is one line to the formatter, left alone // unless the whole of it is asked for. const std::string across = "x = \"a\nb\";\ny=2;\n"; - ensure_equals("across, partly", ALScriptFormatter::formatLines(across, options, 1, 2), std::string("x = \"a\nb\";\ny = 2;\n")); - ensure_equals("across, whole", ALScriptFormatter::formatLines(across, options, 0, 1), std::string("x = \"a\nb\";\ny=2;\n")); + UNSCOPED_INFO("across, partly"); + CHECK(ALScriptFormatter::formatLines(across, options, 1, 2) == std::string("x = \"a\nb\";\ny = 2;\n")); + UNSCOPED_INFO("across, whole"); + CHECK(ALScriptFormatter::formatLines(across, options, 0, 1) == std::string("x = \"a\nb\";\ny=2;\n")); } // Luau's own: an if-expression opens no block, `const` and `export` // read as the keywords they are, and a function's body starts afresh. - template<> template<> - void alscriptformatter_object::test<6>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: Luau: an if-expression opens no block and `const` and `export` are keywords", "[alscriptformatter]") { - ensure_equals("an if-expression", lua("local x = if a then 1 else 2\ny = 3\n"), std::string("local x = if a then 1 else 2\ny = 3\n")); - ensure_equals("chained", lua("local x = if a then 1 elseif b then 2 else 3\ny = 3\n"), std::string("local x = if a then 1 elseif b then 2 else 3\ny = 3\n")); - ensure_equals("else if, as one expression", lua("return if a then 1 else if b then 2 else 3\n"), std::string("return if a then 1 else if b then 2 else 3\n")); - ensure_equals("across lines", lua("local x =\nif a then 1\nelse 2\ny = 3\n"), std::string("local x =\nif a then 1\nelse 2\ny = 3\n")); - ensure_equals("a statement if still opens its block", lua("if a then\nx = 1\nelse\nx = 2\nend\ny = 3\n"), + UNSCOPED_INFO("an if-expression"); + CHECK(lua("local x = if a then 1 else 2\ny = 3\n") == std::string("local x = if a then 1 else 2\ny = 3\n")); + UNSCOPED_INFO("chained"); + CHECK(lua("local x = if a then 1 elseif b then 2 else 3\ny = 3\n") == std::string("local x = if a then 1 elseif b then 2 else 3\ny = 3\n")); + UNSCOPED_INFO("else if, as one expression"); + CHECK(lua("return if a then 1 else if b then 2 else 3\n") == std::string("return if a then 1 else if b then 2 else 3\n")); + UNSCOPED_INFO("across lines"); + CHECK(lua("local x =\nif a then 1\nelse 2\ny = 3\n") == std::string("local x =\nif a then 1\nelse 2\ny = 3\n")); + UNSCOPED_INFO("a statement if still opens its block"); + CHECK(lua("if a then\nx = 1\nelse\nx = 2\nend\ny = 3\n") == std::string("if a then\n x = 1\nelse\n x = 2\nend\ny = 3\n")); - ensure_equals("a statement if inside a function inside an expression", - lua("local f = if a then function()\nif b then\nreturn 1\nend\nend else nil\ny = 3\n"), + UNSCOPED_INFO("a statement if inside a function inside an expression"); + CHECK(lua("local f = if a then function()\nif b then\nreturn 1\nend\nend else nil\ny = 3\n") == std::string("local f = if a then function()\n if b then\n return 1\n end\nend else nil\ny = 3\n")); - ensure_equals("const and export", lua("export const n=1\nconst function f(a,b)\nreturn a+b\nend\n"), + UNSCOPED_INFO("const and export"); + CHECK(lua("export const n=1\nconst function f(a,b)\nreturn a+b\nend\n") == std::string("export const n = 1\nconst function f(a, b)\n return a + b\nend\n")); } - template<> template<> - void alscriptformatter_object::test<7>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: a continued directive's braces open and close nothing of the script's", "[alscriptformatter]") { - set_test_name("a directive continued over several lines is the directive's: its braces open and close nothing of the script's"); const std::string text = "#define BLOCK(x) { \\\n" " llSay(0, x); \\\n" @@ -218,8 +228,8 @@ namespace tut "}\n" "}\n"; const std::string out = lsl(text); - ensure_equals("the macro as written, the script as it should be", - out, + UNSCOPED_INFO("the macro as written, the script as it should be"); + CHECK(out == std::string("#define BLOCK(x) { \\\n" " llSay(0, x); \\\n" "}\n" @@ -232,14 +242,12 @@ namespace tut "}\n")); // SLua's are written --#, and are as written too: a function-like // macro's ( stays against its name, and a # is no operator's. - ensure_equals("SLua's directives as written, the script as it should be", - lua("--#define SQUARE(x) ((x)*(x))\n--#define LEN(t) #t\nlocal function f(a)\nreturn SQUARE(a)+LEN(a)\nend\n"), + UNSCOPED_INFO("SLua's directives as written, the script as it should be"); + CHECK(lua("--#define SQUARE(x) ((x)*(x))\n--#define LEN(t) #t\nlocal function f(a)\nreturn SQUARE(a)+LEN(a)\nend\n") == std::string("--#define SQUARE(x) ((x)*(x))\n--#define LEN(t) #t\nlocal function f(a)\n return SQUARE(a) + LEN(a)\nend\n")); } - template<> template<> - void alscriptformatter_object::test<8>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: breaksInStrings marks line breaks inside strings and not inside comments", "[alscriptformatter]") { - set_test_name("the line breaks that stand inside a string are known, so that tidying lines leaves them and the blanks before them alone"); const auto marked = [](const std::vector& breaks) { std::string out; for (const bool in : breaks) @@ -250,41 +258,40 @@ namespace tut }; // A Luau long string over five lines, with blank lines in it; a long // comment is not a string. - ensure_equals("a long string", marked(ALScriptFormatter::breaksInStrings("local s = [[\n a \n\n\n\nb]]\nprint(s)\n", true)), std::string("11111000")); - ensure_equals("a long comment", marked(ALScriptFormatter::breaksInStrings("--[[\n\n]]\nprint(1)\n", true)), std::string("00000")); - ensure_equals("a string continued by a backslash", marked(ALScriptFormatter::breaksInStrings("print(\"a \\\nb\")\n", true)), std::string("100")); + UNSCOPED_INFO("a long string"); + CHECK(marked(ALScriptFormatter::breaksInStrings("local s = [[\n a \n\n\n\nb]]\nprint(s)\n", true)) == std::string("11111000")); + UNSCOPED_INFO("a long comment"); + CHECK(marked(ALScriptFormatter::breaksInStrings("--[[\n\n]]\nprint(1)\n", true)) == std::string("00000")); + UNSCOPED_INFO("a string continued by a backslash"); + CHECK(marked(ALScriptFormatter::breaksInStrings("print(\"a \\\nb\")\n", true)) == std::string("100")); // LSL: a break written into a string, an escaped quote that does not // end it, and quotes in comments that begin nothing. - ensure_equals("an LSL string", marked(ALScriptFormatter::breaksInStrings("string s = \"a \n\n\nb\";\n// \"not\n/* \" */ integer i;\n", false)), + UNSCOPED_INFO("an LSL string"); + CHECK(marked(ALScriptFormatter::breaksInStrings("string s = \"a \n\n\nb\";\n// \"not\n/* \" */ integer i;\n", false)) == std::string("1110000")); - ensure_equals("an escaped quote", marked(ALScriptFormatter::breaksInStrings("string s = \"a\\\"\nb\";\n", false)), std::string("100")); + UNSCOPED_INFO("an escaped quote"); + CHECK(marked(ALScriptFormatter::breaksInStrings("string s = \"a\\\"\nb\";\n", false)) == std::string("100")); } - template<> template<> - void alscriptformatter_object::test<9>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: a comment-only line is indented as a statement there would be", "[alscriptformatter]") { - set_test_name("a line holding nothing but a comment keeps it, indented as a statement there would be"); - ensure_equals("LSL", - lsl("// first\ndefault\n{\n// a note\nstate_entry()\n{\n/* over\n lines */\nif (x)\n// why\nllSay(0, \"a\");\n}\n}\n"), + CHECK(lsl("// first\ndefault\n{\n// a note\nstate_entry()\n{\n/* over\n lines */\nif (x)\n// why\nllSay(0, \"a\");\n}\n}\n") == std::string("// first\ndefault\n{\n // a note\n state_entry()\n {\n /* over\n lines */\n if (x)\n" " // why\n llSay(0, \"a\");\n }\n}\n")); - ensure_equals("Luau", - lua("-- first\nlocal function f()\n-- a note\nreturn 1\nend\n--[[ over\n lines ]]\n"), + CHECK(lua("-- first\nlocal function f()\n-- a note\nreturn 1\nend\n--[[ over\n lines ]]\n") == std::string("-- first\nlocal function f()\n -- a note\n return 1\nend\n--[[ over\n lines ]]\n")); // Some lines asked for, as the studio asks for all of a script's: // every line kept, the comments too. ALScriptFormatter::Options options; - ensure_equals("LSL, by lines", ALScriptFormatter::formatLines("{\n// note\n/* a\nb */\n}\n", options, 0, 5), + UNSCOPED_INFO("LSL, by lines"); + CHECK(ALScriptFormatter::formatLines("{\n// note\n/* a\nb */\n}\n", options, 0, 5) == std::string("{\n // note\n /* a\nb */\n}\n")); options.lua = true; - ensure_equals("Luau, by lines", ALScriptFormatter::formatLines("do\n-- note\nend\n", options, 0, 3), std::string("do\n -- note\nend\n")); + UNSCOPED_INFO("Luau, by lines"); + CHECK(ALScriptFormatter::formatLines("do\n-- note\nend\n", options, 0, 3) == std::string("do\n -- note\nend\n")); } - template<> template<> - void alscriptformatter_object::test<10>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: a line past the width breaks at the commas of its widest bracket", "[alscriptformatter]") { - set_test_name("a line past the width broken at the commas of its widest bracket, or of the table a call ends with on the call's " - "line, each part a level further in and broken again where still too long, its comment after the last; formatted " - "again, the same; not where no width is set, nor by formatLines; formatEach gives it as one line's"); ALScriptFormatter::Options options; options.lua = true; options.width = 100; @@ -318,31 +325,37 @@ namespace tut " print(\"a string with no comma in it that is longer than the width of one hundred columns, and so on\")\n" "end\n"; const std::string out = ALScriptFormatter::format(in, options); - ensure_equals("broken", out, want); - ensure_equals("the same again", ALScriptFormatter::format(out, options), want); + UNSCOPED_INFO("broken"); + CHECK(out == want); + UNSCOPED_INFO("the same again"); + CHECK(ALScriptFormatter::format(out, options) == want); ALScriptFormatter::Options none = options; none.width = 0; - ensure("no width, no break", ALScriptFormatter::format(in, none).find("{\n") == std::string::npos); + UNSCOPED_INFO("no width, no break"); + REQUIRE(ALScriptFormatter::format(in, none).find("{\n") == std::string::npos); const std::string lines = ALScriptFormatter::formatLines(in, options, 0, 5); - ensure_equals("formatLines keeps every line's number", std::count(lines.begin(), lines.end(), '\n'), std::count(in.begin(), in.end(), '\n')); + UNSCOPED_INFO("formatLines keeps every line's number"); + CHECK(std::count(lines.begin(), lines.end(), '\n') == std::count(in.begin(), in.end(), '\n')); const std::vector each = ALScriptFormatter::formatEach(in, options, 0, 5); - ensure_equals("one a line, and the empty one after the last break", each.size(), size_t(6)); - ensure_equals("the broken line's, breaks and all", each[1] + "\n", want.substr(want.find(" local r"), want.find(" print(string") - want.find(" local r"))); - ensure_equals("a line asked for", ALScriptFormatter::formatEach(in, options, 2, 2)[1], in.substr(in.find(" local r"), in.find(" -- sent") + 8 - in.find(" local r"))); + UNSCOPED_INFO("one a line, and the empty one after the last break"); + REQUIRE(each.size() == size_t(6)); + UNSCOPED_INFO("the broken line's, breaks and all"); + CHECK(each[1] + "\n" == want.substr(want.find(" local r"), want.find(" print(string") - want.find(" local r"))); + UNSCOPED_INFO("a line asked for"); + CHECK(ALScriptFormatter::formatEach(in, options, 2, 2)[1] == in.substr(in.find(" local r"), in.find(" -- sent") + 8 - in.find(" local r"))); // From below the first line: every line still one of its own, the // blank ones that the whole text's runs would take out too. const std::vector blanks = ALScriptFormatter::formatEach("\n\n\n\n\nprint(1)\n", options, -1, 5); - ensure_equals("a line each, blank or not", blanks.size(), size_t(7)); - ensure_equals("the code where it was", blanks[5], std::string("print(1)")); + UNSCOPED_INFO("a line each, blank or not"); + REQUIRE(blanks.size() == size_t(7)); + UNSCOPED_INFO("the code where it was"); + CHECK(blanks[5] == std::string("print(1)")); } - template<> template<> - void alscriptformatter_object::test<11>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: LSL past the width: vectors stay whole and tabs count a level wide", "[alscriptformatter]") { - set_test_name("LSL past the width: a vector or rotation never broken, nor spaced inside where a line begins with one; a call's list " - "on the call's line; tabs a level wide; not a line with a comment inside it"); ALScriptFormatter::Options options; options.width = 100; const std::string in = "default\n{\n state_entry()\n {\n" @@ -368,26 +381,27 @@ namespace tut "<1, 1, 1>, 1.0);\n" " }\n}\n"; const std::string out = ALScriptFormatter::format(in, options); - ensure_equals("broken", out, want); - ensure_equals("the same again", ALScriptFormatter::format(out, options), want); + UNSCOPED_INFO("broken"); + CHECK(out == want); + UNSCOPED_INFO("the same again"); + CHECK(ALScriptFormatter::format(out, options) == want); // With tabs, each a level's width: 2 levels of 4 and the call's 92 // are 100, which fits; at 99 it does not. ALScriptFormatter::Options tabs = options; tabs.tabs = true; const std::string call = "llSay(0, \"" + std::string(92 - 13, 'x') + "\");"; - ensure_equals("a call of 92", call.size(), size_t(92)); + REQUIRE(call.size() == size_t(92)); const std::string tabbed = "default\n{\n state_entry()\n {\n " + call + "\n }\n}\n"; - ensure("fits", ALScriptFormatter::format(tabbed, tabs).find("\t\t" + call + "\n") != std::string::npos); + UNSCOPED_INFO("fits"); + REQUIRE(ALScriptFormatter::format(tabbed, tabs).find("\t\t" + call + "\n") != std::string::npos); tabs.width = 99; - ensure("does not", ALScriptFormatter::format(tabbed, tabs).find("\t\tllSay(\n\t\t\t0,\n") != std::string::npos); + UNSCOPED_INFO("does not"); + REQUIRE(ALScriptFormatter::format(tabbed, tabs).find("\t\tllSay(\n\t\t\t0,\n") != std::string::npos); } - template<> template<> - void alscriptformatter_object::test<12>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: braceSpaces spaces Luau's braces inside but not {} or interpolation or LSL's", "[alscriptformatter]") { - set_test_name("Luau's braces spaced inside as asked, whatever stands next to them, a table type's too; never {} nor an " - "interpolated string's; a type's colon before a bracket spaced as written; LSL's braces as they were"); const std::string in = "local states: {[string]: {[string]:(...any) -> ()}} = {}\n" "local t = {{1}, {2, 3}, [k] = v, -1, #t}\n" "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})\n" @@ -398,68 +412,72 @@ namespace tut "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})\n" "print(`a {b} c {{d}}`, `{ {1} }`)\n" "local f: (number) -> () = obj:method(1)\n"; - ensure_equals("StyLua's, by default", lua(in), want); + UNSCOPED_INFO("StyLua's, by default"); + CHECK(lua(in) == want); ALScriptFormatter::Options none; none.lua = true; none.braceSpaces = false; - ensure_equals("none", ALScriptFormatter::format(in, none), + UNSCOPED_INFO("none"); + CHECK(ALScriptFormatter::format(in, none) == std::string("local states: {[string]: {[string]:(...any) -> ()}} = {}\n" "local t = {{1}, {2, 3}, [k] = v, -1, #t}\n" "local params: {any} = ll.ParseStringKeepNulls(msg, {\"\\n\"}, {})\n" "print(`a {b} c {{d}}`, `{ {1} }`)\n" "local f: (number) -> () = obj:method(1)\n")); - ensure_equals("a type's colon, spaced", lua("local states: {[string]: (...any) -> ()} = {}\n"), + UNSCOPED_INFO("a type's colon, spaced"); + CHECK(lua("local states: {[string]: (...any) -> ()} = {}\n") == std::string("local states: { [string]: (...any) -> () } = {}\n")); ALScriptFormatter::Options lsl_none; lsl_none.braceSpaces = false; - ensure_equals("LSL's braces", ALScriptFormatter::format("default { state_entry() { llSay(0, \"a\"); } }\n", lsl_none), + UNSCOPED_INFO("LSL's braces"); + CHECK(ALScriptFormatter::format("default { state_entry() { llSay(0, \"a\"); } }\n", lsl_none) == std::string("default { state_entry() { llSay(0, \"a\"); } }\n")); } - template<> template<> - void alscriptformatter_object::test<13>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: a sign hugs the bracket after it and is spaced from a < or minus before it", "[alscriptformatter]") { - set_test_name("a sign against the bracket or brace after it, and after a < that compares or a minus that subtracts, spaced " - "from it: -(a), #{ 1 }, i < -1, x - #t, in Luau and in LSL; LSL's vector's first sign still against its <"); - ensure_equals("Luau", lua("local x = - (a)\nlocal y = # (t)\nlocal z = # {1}\nif i <#t and i <-1 and i > - (2) then end\n" - "local w = not (a)\nlocal q = a - (b)\nreturn -(x)\nlocal d = #{1} -#{2} - -#t + -#t\n"), + CHECK(lua("local x = - (a)\nlocal y = # (t)\nlocal z = # {1}\nif i <#t and i <-1 and i > - (2) then end\n" + "local w = not (a)\nlocal q = a - (b)\nreturn -(x)\nlocal d = #{1} -#{2} - -#t + -#t\n") == std::string("local x = -(a)\nlocal y = #(t)\nlocal z = #{ 1 }\nif i < #t and i < -1 and i > -(2) then end\n" "local w = not (a)\nlocal q = a - (b)\nreturn -(x)\nlocal d = #{ 1 } - #{ 2 } - -#t + -#t\n")); - ensure_equals("LSL", lsl("default { state_entry() { integer x = - (a); vector v = < -1, 0, 0 >; if (i <-1) x = ! (b); } }\n"), + CHECK(lsl("default { state_entry() { integer x = - (a); vector v = < -1, 0, 0 >; if (i <-1) x = ! (b); } }\n") == std::string("default { state_entry() { integer x = -(a); vector v = <-1, 0, 0>; if (i < -1) x = !(b); } }\n")); - ensure_equals("a list of vectors", lsl("list l = [<-1, 0, 0>, < -2, 0, 0 >];\n"), std::string("list l = [<-1, 0, 0>, <-2, 0, 0>];\n")); + UNSCOPED_INFO("a list of vectors"); + CHECK(lsl("list l = [<-1, 0, 0>, < -2, 0, 0 >];\n") == std::string("list l = [<-1, 0, 0>, <-2, 0, 0>];\n")); } - template<> template<> - void alscriptformatter_object::test<14>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: what the author lined up stays lined up: runs of = and trailing comments", "[alscriptformatter]") { - set_test_name("what the author lined up stays lined up: a run's =, at the author's column or past the longest, a comment on a line " - "of its own going on past; a run's trailing comments where its code changed; not equal columns by chance; tabs to " - "their stops"); const std::string lined = "local CHANNEL = 1 -- channel\n" "local OWNER_ONLY = true -- owner\n" "-- a note over the next\n" "local USE_OWNER_SAY = true -- say\n"; - ensure_equals("as it was", lua(lined), lined); - ensure_equals("spaced afresh, lined up again", lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n"), + UNSCOPED_INFO("as it was"); + CHECK(lua(lined) == lined); + UNSCOPED_INFO("spaced afresh, lined up again"); + CHECK(lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n") == std::string("local a = 1 -- one\nlocal bbb = { 2, 3 } -- two\n")); - ensure_equals("the author's column", lua("local a = 1\nlocal bb = 2\n"), std::string("local a = 1\nlocal bb = 2\n")); - ensure_equals("past the longest", lua("t[i+1] = 1\nt[ii] = 2\n"), std::string("t[i + 1] = 1\nt[ii] = 2\n")); - ensure_equals("not by chance", lua("ab=1\nc = 2\n"), std::string("ab = 1\nc = 2\n")); - ensure_equals("not across a blank line", lua("local a = 1\n\nlocal bb = 2\n"), std::string("local a = 1\n\nlocal bb = 2\n")); + UNSCOPED_INFO("the author's column"); + CHECK(lua("local a = 1\nlocal bb = 2\n") == std::string("local a = 1\nlocal bb = 2\n")); + UNSCOPED_INFO("past the longest"); + CHECK(lua("t[i+1] = 1\nt[ii] = 2\n") == std::string("t[i + 1] = 1\nt[ii] = 2\n")); + UNSCOPED_INFO("not by chance"); + CHECK(lua("ab=1\nc = 2\n") == std::string("ab = 1\nc = 2\n")); + UNSCOPED_INFO("not across a blank line"); + CHECK(lua("local a = 1\n\nlocal bb = 2\n") == std::string("local a = 1\n\nlocal bb = 2\n")); const std::string fields = "local t = {\n alpha = 1,\n b = 2,\n}\n"; - ensure_equals("a table's fields", lua(fields), fields); - ensure_equals("LSL, by tabs", lsl("integer a\t= 1;\ninteger bb\t= 2;\n"), std::string("integer a = 1;\ninteger bb = 2;\n")); + UNSCOPED_INFO("a table's fields"); + CHECK(lua(fields) == fields); + UNSCOPED_INFO("LSL, by tabs"); + CHECK(lsl("integer a\t= 1;\ninteger bb\t= 2;\n") == std::string("integer a = 1;\ninteger bb = 2;\n")); // The formatter's own, again: the same. - ensure_equals("again", lua(lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n")), + UNSCOPED_INFO("again"); + CHECK(lua(lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n")) == std::string("local a = 1 -- one\nlocal bbb = { 2, 3 } -- two\n")); } - template<> template<> - void alscriptformatter_object::test<15>() + TEST_CASE_METHOD(alscriptformatter_data, "alscriptformatter: a comment before elseif or else or until or end keeps the author's depth", "[alscriptformatter]") { - set_test_name("a comment the author wrote out of the block over an elseif, else, until or end stays as far in as that line; one " - "written in the block stays in it; in an empty block, as written where the text is indented; flat text, in"); const std::string chain = "if a then\n" " x()\n" "-- about b\n" @@ -469,15 +487,18 @@ namespace tut "else\n" " z()\n" "end\n"; - ensure_equals("as written", lua(chain), chain); + UNSCOPED_INFO("as written"); + CHECK(lua(chain) == chain); const std::string repeat = "local function f()\n" " repeat\n" " -- why\n" " until x\n" "end\n"; - ensure_equals("an empty block, indented", lua(repeat), repeat); - ensure_equals("flat", lua("if a then\nx()\n-- note\nend\n"), std::string("if a then\n x()\n -- note\nend\n")); - ensure_equals("LSL", lsl("default\n{\n touch_start(integer n)\n {\n x();\n // about the end\n }\n}\n"), + UNSCOPED_INFO("an empty block, indented"); + CHECK(lua(repeat) == repeat); + UNSCOPED_INFO("flat"); + CHECK(lua("if a then\nx()\n-- note\nend\n") == std::string("if a then\n x()\n -- note\nend\n")); + CHECK(lsl("default\n{\n touch_start(integer n)\n {\n x();\n // about the end\n }\n}\n") == std::string("default\n{\n touch_start(integer n)\n {\n x();\n // about the end\n }\n}\n")); } } diff --git a/indra/alscript/tests/alscriptjoblane_test.cpp b/indra/alscript/tests/alscriptjoblane_test.cpp index cc19f76b35..b3b4b4cc56 100644 --- a/indra/alscript/tests/alscriptjoblane_test.cpp +++ b/indra/alscript/tests/alscriptjoblane_test.cpp @@ -26,7 +26,7 @@ #include "../preprocessor/alscriptjoblane.h" -#include "../test/lltut.h" +#include #include #include @@ -43,7 +43,7 @@ namespace typedef ALScriptJobLane Lane; } -namespace tut +namespace { struct alscriptjoblane_data { @@ -71,55 +71,72 @@ namespace tut } }; - typedef test_group alscriptjoblane_group; - typedef alscriptjoblane_group::object alscriptjoblane_object; - alscriptjoblane_group alscriptjoblane_instance("alscriptjoblane"); - - template<> template<> - void alscriptjoblane_object::test<1>() + TEST_CASE_METHOD(alscriptjoblane_data, "alscriptjoblane: runs go before checks in order and the worker starts once per drain", "[alscriptjoblane]") { - set_test_name("runs before checks, each in the order put in; the worker started once while it drains"); - ensure("the first starts the worker", lane.put(queued("check a", "a", true), take)); - ensure("the rest do not", !lane.put(queued("run b", "b", false), take) && !lane.put(queued("check c", "c", true), take) && - !lane.put(queued("run d", "d", false), take)); + UNSCOPED_INFO("the first starts the worker"); + CHECK(lane.put(queued("check a", "a", true), take)); + { + INFO("the rest do not"); + CHECK_FALSE(lane.put(queued("run b", "b", false), take)); + CHECK_FALSE(lane.put(queued("check c", "c", true), take)); + CHECK_FALSE(lane.put(queued("run d", "d", false), take)); + } lane.drain(); - ensure("runs first", ran == std::vector({ "run b", "run d", "check a", "check c" })); - ensure("drained: the next starts it again", lane.put(queued("run e", "e", false), take)); + UNSCOPED_INFO("runs first"); + CHECK(ran == std::vector({ "run b", "run d", "check a", "check c" })); + UNSCOPED_INFO("drained: the next starts it again"); + CHECK(lane.put(queued("run e", "e", false), take)); } - template<> template<> - void alscriptjoblane_object::test<2>() + TEST_CASE_METHOD(alscriptjoblane_data, "alscriptjoblane: a later check replaces a waiting one and stops a running one; runs never do", "[alscriptjoblane]") { - set_test_name("a later check of a script stands for the one waiting, and tells the one running; a run is no check's to stand for"); lane.put(queued("check a1", "a", true), take); lane.put(queued("check b", "b", true), take); lane.put(queued("check a2", "a", true), take); - ensure("the waiting one taken", jobs[0]->taken && !jobs[0]->stopped && !jobs[1]->taken); - ensure_equals("and let go of", lane.waiting(), size_t(2)); + { + INFO("the waiting one taken"); + CHECK(jobs[0]->taken); + CHECK_FALSE(jobs[0]->stopped); + CHECK_FALSE(jobs[1]->taken); + } + UNSCOPED_INFO("and let go of"); + CHECK(lane.waiting() == size_t(2)); lane.put(queued("run a", "a", false), take); - ensure("a run takes nothing", !jobs[2]->taken); + UNSCOPED_INFO("a run takes nothing"); + CHECK_FALSE(jobs[2]->taken); // Put in while the first is running: the running one told. Lane lane2; lane2.put(queued("check x1", "x", true, [this, &lane2]() { lane2.put(queued("check x2", "x", true), &alscriptjoblane_data::take); }), take); lane2.drain(); - ensure("the running one told to stop", jobs.back()->name == "check x2" && jobs[jobs.size() - 2]->taken && jobs[jobs.size() - 2]->stopped); - ensure("and the later one ran after", ran.back() == "check x2"); + { + INFO("the running one told to stop"); + REQUIRE(jobs.back()->name == "check x2"); + CHECK(jobs[jobs.size() - 2]->taken); + CHECK(jobs[jobs.size() - 2]->stopped); + } + UNSCOPED_INFO("and the later one ran after"); + CHECK(ran.back() == "check x2"); } - template<> template<> - void alscriptjoblane_object::test<3>() + TEST_CASE_METHOD(alscriptjoblane_data, "alscriptjoblane: a throwing job is logged and the next goes on; a closed lane takes nothing", "[alscriptjoblane]") { - set_test_name("a job that throws is said, and the next goes on; closed, nothing more is taken"); lane.put(queued("throws", "a", false, []() { throw std::runtime_error("nope"); }), take); lane.put(queued("next", "b", false), take); lane.drain(); - ensure("the next ran", ran == std::vector({ "throws", "next" })); + UNSCOPED_INFO("the next ran"); + CHECK(ran == std::vector({ "throws", "next" })); lane.put(queued("left", "c", false), take); lane.close(); - ensure("let go of", lane.waiting() == 0); - ensure("nothing taken once closed", !lane.put(queued("late", "d", false), take) && lane.waiting() == 0); + UNSCOPED_INFO("let go of"); + CHECK(lane.waiting() == 0); + { + INFO("nothing taken once closed"); + CHECK_FALSE(lane.put(queued("late", "d", false), take)); + CHECK(lane.waiting() == 0); + } lane.drain(); - ensure("and nothing run", ran.size() == 2); + UNSCOPED_INFO("and nothing run"); + REQUIRE(ran.size() == 2); } } diff --git a/indra/alscript/tests/alscriptlexicon_test.cpp b/indra/alscript/tests/alscriptlexicon_test.cpp index 4e422abd7f..68459e2cf6 100644 --- a/indra/alscript/tests/alscriptlexicon_test.cpp +++ b/indra/alscript/tests/alscriptlexicon_test.cpp @@ -27,127 +27,206 @@ #include "../core/alscriptlexicon.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { - struct alscriptlexicon_data + TEST_CASE("alscriptlexicon: each LSL word has one kind; keywords are control words or types or reserved", "[alscriptlexicon]") { - }; - typedef test_group alscriptlexicon_group; - typedef alscriptlexicon_group::object alscriptlexicon_object; - tut::alscriptlexicon_group alscriptlexicon_instance("alscriptlexicon"); - - template<> template<> - void alscriptlexicon_object::test<1>() - { - set_test_name("each LSL word is of one kind; a keyword is a control word, a type or a reserved word"); using namespace ALScriptLexicon; for (const char* word : { "default", "state", "jump", "return", "if", "else", "for", "do", "while" }) { - ensure(std::string("control: ") + word, lslWord(word) == LSL_CONTROL && isLslKeyword(word)); + CAPTURE(word); + REQUIRE(lslWord(word) == LSL_CONTROL); + REQUIRE(isLslKeyword(word)); } for (const char* word : { "integer", "float", "string", "key", "vector", "rotation", "quaternion", "list" }) { - ensure(std::string("type: ") + word, lslWord(word) == LSL_TYPE && isLslType(word) && isLslKeyword(word)); + CAPTURE(word); + REQUIRE(lslWord(word) == LSL_TYPE); + REQUIRE(isLslType(word)); + REQUIRE(isLslKeyword(word)); } - ensure("event and print are reserved", lslWord("event") == LSL_RESERVED && lslWord("print") == LSL_RESERVED && isLslKeyword("print")); - ensure("TRUE and FALSE are constants, not keywords", lslWord("TRUE") == LSL_CONSTANT && lslWord("FALSE") == LSL_CONSTANT && !isLslKeyword("TRUE")); + CHECK(lslWord("event") == LSL_RESERVED); + CHECK(lslWord("print") == LSL_RESERVED); + CHECK(isLslKeyword("print")); + CHECK(lslWord("TRUE") == LSL_CONSTANT); + CHECK(lslWord("FALSE") == LSL_CONSTANT); + CHECK_FALSE(isLslKeyword("TRUE")); for (const char* word : { "inline", "const", "break", "continue", "switch", "case" }) { - ensure(std::string("extension, not a keyword: ") + word, lslWord(word) == LSL_EXTENSION && !isLslKeyword(word)); + CAPTURE(word); + REQUIRE(lslWord(word) == LSL_EXTENSION); + REQUIRE_FALSE(isLslKeyword(word)); + } + CHECK(lslWord("count") == LSL_NAME); + CHECK(lslWord("llSay") == LSL_NAME); + CHECK(lslWord("") == LSL_NAME); + { + INFO("case matters"); + CHECK(lslWord("Integer") == LSL_NAME); + CHECK(lslWord("true") == LSL_NAME); } - ensure("a name is none of them", lslWord("count") == LSL_NAME && lslWord("llSay") == LSL_NAME && lslWord("") == LSL_NAME); - ensure("case matters", lslWord("Integer") == LSL_NAME && lslWord("true") == LSL_NAME); } - template<> template<> - void alscriptlexicon_object::test<2>() + TEST_CASE("alscriptlexicon: Luau's reserved and statement words are keywords; type and typeof are not", "[alscriptlexicon]") { - set_test_name("Luau's reserved words and its statement words are keywords; type and typeof are not"); using namespace ALScriptLexicon; for (const char* word : { "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while", "continue", "export", "const" }) { - ensure(std::string("keyword: ") + word, isLuauKeyword(word) && isKeyword(true, word)); + CAPTURE(word); + REQUIRE(isLuauKeyword(word)); + REQUIRE(isKeyword(true, word)); + } + { + INFO("type and typeof are called"); + CHECK_FALSE(isLuauKeyword("type")); + CHECK_FALSE(isLuauKeyword("typeof")); + } + CHECK_FALSE(isLuauKeyword("print")); + CHECK_FALSE(isLuauKeyword("ll")); + { + INFO("the language decides"); + CHECK(isKeyword(false, "state")); + CHECK_FALSE(isKeyword(true, "state")); + CHECK(isKeyword(true, "local")); + CHECK_FALSE(isKeyword(false, "local")); } - ensure("type and typeof are called", !isLuauKeyword("type") && !isLuauKeyword("typeof")); - ensure("a name is not", !isLuauKeyword("print") && !isLuauKeyword("ll")); - ensure("the language decides", isKeyword(false, "state") && !isKeyword(true, "state") && isKeyword(true, "local") && !isKeyword(false, "local")); } - template<> template<> - void alscriptlexicon_object::test<3>() + TEST_CASE("alscriptlexicon: a name is ASCII letters and digits and underscores; no digit first", "[alscriptlexicon]") { - set_test_name("a name is ASCII letters, digits and underscores, not beginning with a digit"); using namespace ALScriptLexicon; - ensure("letters and an underscore", isName("_count2") && isName("x") && isName("llSay")); - ensure("not a digit first", !isName("2x")); - ensure("not empty, nor with other bytes", !isName("") && !isName("a-b") && !isName("a.b") && !isName("caf\xc3\xa9")); - ensure("the bytes", isNameStart('_') && !isNameStart('7') && isNameByte('7') && !isNameByte('$')); + CHECK(isName("_count2")); + CHECK(isName("x")); + CHECK(isName("llSay")); + CHECK_FALSE(isName("2x")); + CHECK_FALSE(isName("")); + CHECK_FALSE(isName("a-b")); + CHECK_FALSE(isName("a.b")); + CHECK_FALSE(isName("caf\xc3\xa9")); + CHECK(isNameStart('_')); + CHECK_FALSE(isNameStart('7')); + CHECK(isNameByte('7')); + CHECK_FALSE(isNameByte('$')); } - template<> template<> - void alscriptlexicon_object::test<4>() + TEST_CASE("alscriptlexicon: a long bracket's level and where its closer of that level ends", "[alscriptlexicon]") { - set_test_name("a long bracket's level, and where its closer of that level ends"); using namespace ALScriptLexicon; - ensure_equals("[[", longBracketLevel("x = [[a]]", 4), 0); - ensure_equals("[==[", longBracketLevel("[==[a]==]", 0), 2); - ensure_equals("not one", longBracketLevel("[=a", 0), -1); - ensure_equals("not a bracket", longBracketLevel("a[[", 0), -1); - ensure_equals("past the end", longBracketLevel("[[", 5), -1); - ensure_equals("the closer of its level, not another's", longBracketClose("a]=]b]]c", 0, 0), size_t(7)); - ensure_equals("level two", longBracketClose("a]]]=]]==]x", 0, 2), size_t(10)); - ensure("none", longBracketClose("a]=]", 0, 0) == std::string_view::npos); + CHECK(longBracketLevel("x = [[a]]", 4) == 0); + CHECK(longBracketLevel("[==[a]==]", 0) == 2); + UNSCOPED_INFO("not one"); + CHECK(longBracketLevel("[=a", 0) == -1); + UNSCOPED_INFO("not a bracket"); + CHECK(longBracketLevel("a[[", 0) == -1); + UNSCOPED_INFO("past the end"); + CHECK(longBracketLevel("[[", 5) == -1); + UNSCOPED_INFO("the closer of its level, not another's"); + CHECK(longBracketClose("a]=]b]]c", 0, 0) == size_t(7)); + CHECK(longBracketClose("a]]]=]]==]x", 0, 2) == size_t(10)); + CHECK(longBracketClose("a]=]", 0, 0) == std::string_view::npos); } - template<> template<> - void alscriptlexicon_object::test<5>() + TEST_CASE("alscriptlexicon: LSL: strings run past escapes and breaks; comments to the break or closer", "[alscriptlexicon]") { - set_test_name("LSL: a string to its quote past escapes and breaks; a line comment to the break; a block comment to its closer"); using namespace ALScriptLexicon; const std::string_view text = "a \"b\\\"c\nd\" // e\n/* f */ g /* h"; Stretch s = stretchAt(text, 0, false); - ensure("code, one byte", s.kind == Kind::Code && s.end == 1); + CHECK(s.kind == Kind::Code); + CHECK(s.end == 1); s = stretchAt(text, 2, false); - ensure("the string, across the break", s.kind == Kind::String && s.closed && s.open == 1 && s.close == 1 && text.substr(2, s.end - 2) == "\"b\\\"c\nd\""); + { + INFO("the string, across the break"); + CHECK(s.kind == Kind::String); + CHECK(s.closed); + CHECK(s.open == 1); + CHECK(s.close == 1); + CHECK(text.substr(2, s.end - 2) == "\"b\\\"c\nd\""); + } const size_t slash = text.find("//"); s = stretchAt(text, slash, false); - ensure("the line comment, to the break", s.kind == Kind::Comment && s.closed && s.close == 0 && text[s.end] == '\n' && s.open == 2); + { + INFO("the line comment, to the break"); + REQUIRE(s.kind == Kind::Comment); + REQUIRE(s.closed); + REQUIRE(s.close == 0); + CHECK(text[s.end] == '\n'); + CHECK(s.open == 2); + } const size_t block = text.find("/*"); s = stretchAt(text, block, false); - ensure("the block comment", s.kind == Kind::Comment && s.closed && s.close == 2 && text.substr(block, s.end - block) == "/* f */"); + CHECK(s.kind == Kind::Comment); + CHECK(s.closed); + CHECK(s.close == 2); + CHECK(text.substr(block, s.end - block) == "/* f */"); s = stretchAt(text, text.rfind("/*"), false); - ensure("a block comment the text runs out in", s.kind == Kind::Comment && !s.closed && s.end == text.size() && s.close == 0); - ensure("no char constants", stretchAt("'x'", 0, false).kind == Kind::Code); - ensure("no -- comment", stretchAt("--x", 0, false).kind == Kind::Code); - ensure("a string not closed", !stretchAt("\"abc", 0, false).closed); + { + INFO("a block comment the text runs out in"); + CHECK(s.kind == Kind::Comment); + CHECK_FALSE(s.closed); + CHECK(s.end == text.size()); + CHECK(s.close == 0); + } + UNSCOPED_INFO("no char constants"); + CHECK(stretchAt("'x'", 0, false).kind == Kind::Code); + UNSCOPED_INFO("no -- comment"); + CHECK(stretchAt("--x", 0, false).kind == Kind::Code); + CHECK_FALSE(stretchAt("\"abc", 0, false).closed); } - template<> template<> - void alscriptlexicon_object::test<6>() + TEST_CASE("alscriptlexicon: SLua: quoted strings and line comments end at the line; long ones at closers", "[alscriptlexicon]") { - set_test_name("SLua: quoted strings end at their line, long strings and comments at their closer, a line comment at its break"); using namespace ALScriptLexicon; for (const char* quoted : { "\"a\\\"b\"", "'a\\'b'", "`a{b}c`" }) { const Stretch s = stretchAt(quoted, 0, true); - ensure(std::string("closed: ") + quoted, s.kind == Kind::String && s.closed && s.end == std::string_view(quoted).size()); + CAPTURE(quoted); + REQUIRE(s.kind == Kind::String); + REQUIRE(s.closed); + REQUIRE(s.end == std::string_view(quoted).size()); } Stretch s = stretchAt("'abc\nd'", 0, true); - ensure("a quoted string ends at its break", s.kind == Kind::String && !s.closed && s.end == 4); + { + INFO("a quoted string ends at its break"); + CHECK(s.kind == Kind::String); + CHECK_FALSE(s.closed); + CHECK(s.end == 4); + } s = stretchAt("'a\\\nb'", 0, true); - ensure("but for an escaped one", s.closed && s.end == 6); + { + INFO("but for an escaped one"); + CHECK(s.closed); + CHECK(s.end == 6); + } s = stretchAt("[==[a]]b]==]c", 0, true); - ensure("a long string", s.kind == Kind::String && s.closed && s.open == 4 && s.close == 4 && s.end == 12); + CHECK(s.kind == Kind::String); + CHECK(s.closed); + CHECK(s.open == 4); + CHECK(s.close == 4); + CHECK(s.end == 12); s = stretchAt("--[[a\nb]] c", 0, true); - ensure("a long comment", s.kind == Kind::Comment && s.closed && s.open == 4 && s.close == 2 && s.end == 9); + CHECK(s.kind == Kind::Comment); + CHECK(s.closed); + CHECK(s.open == 4); + CHECK(s.close == 2); + CHECK(s.end == 9); s = stretchAt("-- a\nb", 0, true); - ensure("a line comment", s.kind == Kind::Comment && s.closed && s.close == 0 && s.end == 4); + CHECK(s.kind == Kind::Comment); + CHECK(s.closed); + CHECK(s.close == 0); + CHECK(s.end == 4); s = stretchAt("--[[ a", 0, true); - ensure("a long comment the text runs out in", s.kind == Kind::Comment && !s.closed && s.end == 6); - ensure("[ alone is code", stretchAt("[1]", 0, true).kind == Kind::Code); - ensure("no // comment", stretchAt("//x", 0, true).kind == Kind::Code); + { + INFO("a long comment the text runs out in"); + CHECK(s.kind == Kind::Comment); + CHECK_FALSE(s.closed); + CHECK(s.end == 6); + } + UNSCOPED_INFO("[ alone is code"); + CHECK(stretchAt("[1]", 0, true).kind == Kind::Code); + UNSCOPED_INFO("no // comment"); + CHECK(stretchAt("//x", 0, true).kind == Kind::Code); } } diff --git a/indra/alscript/tests/alscriptlintpass_test.cpp b/indra/alscript/tests/alscriptlintpass_test.cpp index 8dc9a1060f..3fd5938ac1 100644 --- a/indra/alscript/tests/alscriptlintpass_test.cpp +++ b/indra/alscript/tests/alscriptlintpass_test.cpp @@ -30,13 +30,13 @@ #include "../lint/alscriptfixes.h" #include "../lint/alscriptlintpass.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { struct alscriptlintpass_data { @@ -114,45 +114,46 @@ namespace tut // The problem keyed so, its preferred fix -- titled so, and safe or // not as said -- the text with it made, and that text checked again: // the problem gone, and no error in its place. - std::string fixed(const std::string& script, const std::string& key, const std::string& title, bool safe, bool lua = true) + std::string require_fixed(const std::string& script, const std::string& key, const std::string& title, bool safe, bool lua = true) { const ALScriptProblems problems = check(script, lua); const ALScriptProblem* problem = keyed(problems, key); - ensure("said: " + key + "\n" + said(problems), problem != nullptr); - ensure("a fix: " + said(problems), !problem->fixes.empty()); + CAPTURE(key); + INFO("said:\n" + said(problems)); + REQUIRE(problem != nullptr); + REQUIRE_FALSE(problem->fixes.empty()); const ALScriptFix& fix = problem->fixes.front(); - ensure("preferred", fix.preferred); - ensure_equals("its words", fix.title, title); - ensure_equals("safe or not", fix.safe, safe); + REQUIRE(fix.preferred); + REQUIRE(fix.title == title); + REQUIRE(fix.safe == safe); const std::optional made = ALScriptFixes::apply(script, fix); - ensure("applies", made.has_value()); + REQUIRE(made.has_value()); const ALScriptProblems after = check(*made, lua); - ensure("gone from:\n" + *made + "\n" + said(after), keyed(after, key) == nullptr); - ensure("no error in its place:\n" + *made + "\n" + said(after), - errors(after) <= errors(problems) - (problem->severity == ALScriptProblem::Severity::Error ? 1 : 0)); + INFO("made:\n" + *made + "\nsaid then:\n" + said(after)); + REQUIRE(keyed(after, key) == nullptr); + UNSCOPED_INFO("no error in its place"); + REQUIRE(errors(after) <= errors(problems) - (problem->severity == ALScriptProblem::Severity::Error ? 1 : 0)); return *made; } // The problem keyed so is said, and offers no fix. - void unfixed(const std::string& script, const std::string& key, bool lua = true) + void require_unfixed(const std::string& script, const std::string& key, bool lua = true) { const ALScriptProblems problems = check(script, lua); const ALScriptProblem* problem = keyed(problems, key); - ensure("said: " + key + "\n" + said(problems), problem != nullptr); - ensure("no fix: " + said(problems), problem->fixes.empty()); + CAPTURE(key); + INFO("said:\n" + said(problems)); + REQUIRE(problem != nullptr); + REQUIRE(problem->fixes.empty()); } }; // Raised from TUT's fifty: stages C and D of the SLua help plan add a // test for each rule. - typedef test_group alscriptlintpass_group; - typedef alscriptlintpass_group::object object; - tut::alscriptlintpass_group alscriptlintpass_test("ALScriptLintPass"); - template<> template<> - void object::test<1>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlTableCompare: a table compared with {}; with items or a string no fix", "[ALScriptLintPass]") { - set_test_name("SlTableCompare: a table compared with {} on either side, by length where it is a list; with items, or a string, no fix"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local t = {1}\n" "if t == {} then print(1) end\n" "if {} ~= t then print(2) end\n" @@ -164,26 +165,26 @@ namespace tut "local function f(l) return l ~= {} end\n" "if t == nil or t == d then print(f(t)) end\n", "SlTableCompare", ALScriptProblem::Severity::Error); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlTableCompare|t|#t == 0\n" "2 LuauLintSlTableCompareAlways|t|#t > 0\n" "4 LuauLintSlTableCompare|d|next(d) == nil\n" "5 LuauLintSlTableCompareItems|t\n" "7 LuauLintSlTableCompareItems|s\n" "8 LuauLintSlTableCompareAlways|l|next(l) ~= nil\n")); - ensure_equals("a list", fixed("local t = {1}\nif t == {} then print(t) end\n", "LuauLintSlTableCompare", "Write it #t == 0", false), + UNSCOPED_INFO("a list"); + CHECK(require_fixed("local t = {1}\nif t == {} then print(t) end\n", "LuauLintSlTableCompare", "Write it #t == 0", false) == std::string("local t = {1}\nif #t == 0 then print(t) end\n")); - ensure_equals("a table, on the right, bracketed", fixed("local d = {a = 1}\nprint({} ~= (d))\n", "LuauLintSlTableCompareAlways", "Write it next(d) ~= nil", false), + UNSCOPED_INFO("a table, on the right, bracketed"); + CHECK(require_fixed("local d = {a = 1}\nprint({} ~= (d))\n", "LuauLintSlTableCompareAlways", "Write it next(d) ~= nil", false) == std::string("local d = {a = 1}\nprint(next(d) ~= nil)\n")); - unfixed("local t = {1}\nprint(t == {1})\n", "LuauLintSlTableCompareItems"); + require_unfixed("local t = {1}\nprint(t == {1})\n", "LuauLintSlTableCompareItems"); } - template<> template<> - void object::test<2>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlZeroIndex: counting from 0 in lists and loops and subs and finds", "[ALScriptLintPass]") { - set_test_name("SlZeroIndex: a list at 0, a loop from 0 to a length less 1 that indexes, string.sub and ll from 0, a find against 0; " - "not a table keyed at 0, nor a loop given to llcompat"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local t = {1, 2}\n" "print(t[0])\n" "for i = 0, #t - 1 do print(t[i]) end\n" @@ -198,7 +199,7 @@ namespace tut "h[0] = 2\n" "print(d[0], h[0], llcompat.SubStringIndex(s, \"a\") == 0)\n", "SlZeroIndex", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlZeroIndex|t\n" "2 LuauLintSlZeroIndexLoop|#t|t[i]|i\n" "3 LuauLintSlZeroIndexLoop|#t|t[i]|i\n" @@ -210,31 +211,34 @@ namespace tut "7 LuauLintSlZeroIndexArg|ll.GetSubString\n" "8 LuauLintSlZeroIndexFound|ll.SubStringIndex(...)|ll.SubStringIndex\n" "8 LuauLintSlZeroIndexFound|table.find(...)|table.find\n")); - ensure_equals("a list", fixed("local t = {1}\nprint(t[0])\n", "LuauLintSlZeroIndex", "Write it t[1]", false), + UNSCOPED_INFO("a list"); + CHECK(require_fixed("local t = {1}\nprint(t[0])\n", "LuauLintSlZeroIndex", "Write it t[1]", false) == std::string("local t = {1}\nprint(t[1])\n")); - ensure_equals("a loop", fixed("local t = {1}\nfor i = 0, #t - 1 do print(t[i]) end\n", "LuauLintSlZeroIndexLoop", "Write it for i = 1, #t", false), + UNSCOPED_INFO("a loop"); + CHECK(require_fixed("local t = {1}\nfor i = 0, #t - 1 do print(t[i]) end\n", "LuauLintSlZeroIndexLoop", "Write it for i = 1, #t", false) == std::string("local t = {1}\nfor i = 1, #t do print(t[i]) end\n")); - ensure_equals("a method to a length less 1", fixed("local s = \"abc\"\nprint(s:sub(0, #s - 1))\n", "LuauLintSlZeroIndexSub", "Write it s:sub(1, #s)", false), + UNSCOPED_INFO("a method to a length less 1"); + CHECK(require_fixed("local s = \"abc\"\nprint(s:sub(0, #s - 1))\n", "LuauLintSlZeroIndexSub", "Write it s:sub(1, #s)", false) == std::string("local s = \"abc\"\nprint(s:sub(1, #s))\n")); - ensure_equals("string's, to a number", fixed("print(string.sub(\"abc\", 0, 2))\n", "LuauLintSlZeroIndexSub", "Write it string.sub(\"abc\", 1, 3)", false), + UNSCOPED_INFO("string's, to a number"); + CHECK(require_fixed("print(string.sub(\"abc\", 0, 2))\n", "LuauLintSlZeroIndexSub", "Write it string.sub(\"abc\", 1, 3)", false) == std::string("print(string.sub(\"abc\", 1, 3))\n")); - ensure_equals("ll's, one from the end kept", fixed("print(ll.GetSubString(\"abc\", 0, -1))\n", "LuauLintSlZeroIndexArg", - "Write it ll.GetSubString(\"abc\", 1, -1)", false), + UNSCOPED_INFO("ll's, one from the end kept"); + CHECK(require_fixed("print(ll.GetSubString(\"abc\", 0, -1))\n", "LuauLintSlZeroIndexArg", + "Write it ll.GetSubString(\"abc\", 1, -1)", false) == std::string("print(ll.GetSubString(\"abc\", 1, -1))\n")); - ensure_equals("a find", fixed("print(ll.SubStringIndex(\"ab\", \"a\") == 0)\n", "LuauLintSlZeroIndexFound", "Write it ll.SubStringIndex(...) == 1", false), + UNSCOPED_INFO("a find"); + CHECK(require_fixed("print(ll.SubStringIndex(\"ab\", \"a\") == 0)\n", "LuauLintSlZeroIndexFound", "Write it ll.SubStringIndex(...) == 1", false) == std::string("print(ll.SubStringIndex(\"ab\", \"a\") == 1)\n")); - unfixed("local t = {1}\nfor i = 0, #t - 1 do print(i, t[i]) end\n", "LuauLintSlZeroIndexLoop"); - unfixed("local s = \"abc\"\nlocal n = 2\nprint(s:sub(0, n))\n", "LuauLintSlZeroIndexSub"); - unfixed("local s = \"abc\"\nprint(ll.GetSubString(s, 0, #s))\n", "LuauLintSlZeroIndexArg"); + require_unfixed("local t = {1}\nfor i = 0, #t - 1 do print(i, t[i]) end\n", "LuauLintSlZeroIndexLoop"); + require_unfixed("local s = \"abc\"\nlocal n = 2\nprint(s:sub(0, n))\n", "LuauLintSlZeroIndexSub"); + require_unfixed("local s = \"abc\"\nprint(ll.GetSubString(s, 0, #s))\n", "LuauLintSlZeroIndexArg"); } - template<> template<> - void object::test<3>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlCompatCall: llcompat's where ll's means the same; not where it differs", "[ALScriptLintPass]") { - set_test_name("SlCompatCall: llcompat's where ll's means the same -- the same function, indexes that are numbers, a boolean against 1 " - "or 0, a find against -1 or 0, an answer unread, one ll's only deprecates; not where ll deprecates one that differs, nor an " - "answer read otherwise"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local s = \"abc\"\n" "llcompat.Say(0, s)\n" "print(llcompat.GetSubString(s, 0, -1))\n" @@ -249,7 +253,7 @@ namespace tut "print(n, g, llcompat.ListFindList({1}, {1}) == -1)\n" "print(llcompat.List2Json(JSON_ARRAY, {s}))\n", "SlCompatCall", ALScriptProblem::Severity::Note); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlCompatCall|llcompat.Say|ll.Say(...)\n" "2 LuauLintSlCompatCall|llcompat.GetSubString|ll.GetSubString(s, 1, -1)\n" "4 LuauLintSlCompatCall|llcompat.SameGroup|ll.SameGroup(...)\n" @@ -258,26 +262,30 @@ namespace tut "7 LuauLintSlCompatCall|llcompat.SubStringIndex|ll.SubStringIndex(...) == nil\n" "8 LuauLintSlCompatCall|llcompat.SameGroup|ll.SameGroup(...)\n" "12 LuauLintSlCompatCall|llcompat.List2Json|ll.List2Json(...)\n")); - ensure_equals("the same", fixed("llcompat.Say(0, \"hi\")\n", "LuauLintSlCompatCall", "Write it ll.Say(...)", true), + UNSCOPED_INFO("the same"); + CHECK(require_fixed("llcompat.Say(0, \"hi\")\n", "LuauLintSlCompatCall", "Write it ll.Say(...)", true) == std::string("ll.Say(0, \"hi\")\n")); - ensure_equals("indexes", fixed("print(llcompat.GetSubString(\"abc\", 0, 1))\n", "LuauLintSlCompatCall", "Write it ll.GetSubString(\"abc\", 1, 2)", true), + UNSCOPED_INFO("indexes"); + CHECK(require_fixed("print(llcompat.GetSubString(\"abc\", 0, 1))\n", "LuauLintSlCompatCall", "Write it ll.GetSubString(\"abc\", 1, 2)", true) == std::string("print(ll.GetSubString(\"abc\", 1, 2))\n")); - ensure_equals("a boolean on the right", fixed("print(0 == llcompat.SameGroup(ll.GetOwner()))\n", "LuauLintSlCompatCall", "Write it not ll.SameGroup(...)", true), + UNSCOPED_INFO("a boolean on the right"); + CHECK(require_fixed("print(0 == llcompat.SameGroup(ll.GetOwner()))\n", "LuauLintSlCompatCall", "Write it not ll.SameGroup(...)", true) == std::string("print(not ll.SameGroup(ll.GetOwner()))\n")); - ensure_equals("a boolean", fixed("print(llcompat.SameGroup(ll.GetOwner()) ~= 0)\n", "LuauLintSlCompatCall", "Write it ll.SameGroup(...)", true), + UNSCOPED_INFO("a boolean"); + CHECK(require_fixed("print(llcompat.SameGroup(ll.GetOwner()) ~= 0)\n", "LuauLintSlCompatCall", "Write it ll.SameGroup(...)", true) == std::string("print(ll.SameGroup(ll.GetOwner()))\n")); - ensure_equals("a find, first", fixed("print(llcompat.SubStringIndex(\"ab\", \"a\") == 0)\n", "LuauLintSlCompatCall", "Write it ll.SubStringIndex(...) == 1", true), + UNSCOPED_INFO("a find, first"); + CHECK(require_fixed("print(llcompat.SubStringIndex(\"ab\", \"a\") == 0)\n", "LuauLintSlCompatCall", "Write it ll.SubStringIndex(...) == 1", true) == std::string("print(ll.SubStringIndex(\"ab\", \"a\") == 1)\n")); - ensure_equals("a find, on the right", fixed("print(-1 < llcompat.SubStringIndex(\"ab\", \"a\"))\n", "LuauLintSlCompatCall", "Write it ll.SubStringIndex(...) ~= nil", true), + UNSCOPED_INFO("a find, on the right"); + CHECK(require_fixed("print(-1 < llcompat.SubStringIndex(\"ab\", \"a\"))\n", "LuauLintSlCompatCall", "Write it ll.SubStringIndex(...) ~= nil", true) == std::string("print(ll.SubStringIndex(\"ab\", \"a\") ~= nil)\n")); } - template<> template<> - void object::test<4>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlBooleanNumber: a boolean against a number; an if of 1 and 0 a note", "[ALScriptLintPass]") { - set_test_name("SlBooleanNumber: a boolean compared with a number, on either side, an error; an if-then-else of 1 and 0 compared with " - "one, a note; not a number compared with one"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local on = false\n" "local n = 1\n" "if on == 1 or 0 ~= on then print(1) end\n" @@ -285,29 +293,29 @@ namespace tut "if (if on then 1 else 0) == 1 or (if n > 2 then 0 else 1) ~= 0 then print(3) end\n" "if n == 1 or (if on then 2 else 0) == 1 then print(4) end\n", "SlBooleanNumber", ALScriptProblem::Severity::Error); - ensure_equals("each", said, + CHECK(said == std::string("2 LuauLintSlBooleanNumber|on|1\n" "2 LuauLintSlBooleanNumberAlways|on|0\n" "3 LuauLintSlBooleanNumber|ll.SameGroup(ll.GetOwner())|0\n" "3 LuauLintSlBooleanNumber|(n > 2)|5\n" "4 LuauLintSlBooleanNumberChoice|(if on then 1 else 0) == 1|on (another severity)\n" "4 LuauLintSlBooleanNumberChoice|(if n > 2 then 0 else 1) ~= 0|not (n > 2) (another severity)\n")); - ensure_equals("true", fixed("local on = false\nprint(on == 1)\n", "LuauLintSlBooleanNumber", "Write it on", false), + UNSCOPED_INFO("true"); + CHECK(require_fixed("local on = false\nprint(on == 1)\n", "LuauLintSlBooleanNumber", "Write it on", false) == std::string("local on = false\nprint(on)\n")); - ensure_equals("false, on the right", fixed("print(0 == ll.SameGroup(ll.GetOwner()))\n", "LuauLintSlBooleanNumber", "Write it not ll.SameGroup(ll.GetOwner())", false), + UNSCOPED_INFO("false, on the right"); + CHECK(require_fixed("print(0 == ll.SameGroup(ll.GetOwner()))\n", "LuauLintSlBooleanNumber", "Write it not ll.SameGroup(ll.GetOwner())", false) == std::string("print(not ll.SameGroup(ll.GetOwner()))\n")); - ensure_equals("a choice", fixed("local n = 1\nprint((if n > 2 then 0 else 1) ~= 0)\n", "LuauLintSlBooleanNumberChoice", "Write it not (n > 2)", false), + UNSCOPED_INFO("a choice"); + CHECK(require_fixed("local n = 1\nprint((if n > 2 then 0 else 1) ~= 0)\n", "LuauLintSlBooleanNumberChoice", "Write it not (n > 2)", false) == std::string("local n = 1\nprint(not (n > 2))\n")); - unfixed("local n = 1\nprint((n > 2) == 5)\n", "LuauLintSlBooleanNumber"); + require_unfixed("local n = 1\nprint((n > 2) == 5)\n", "LuauLintSlBooleanNumber"); } - template<> template<> - void object::test<5>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlGlobalAssign: globals and global functions made local; not with _G", "[ALScriptLintPass]") { - set_test_name("SlGlobalAssign: a global made at the top, once, a note; a function made global in a block, a warning; a function " - "at the top SlGlobalFunction's, a note, off on its own; not a name the script is given; each made local, in place or " - "declared before it is first named, and not where _G is"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string script = "count = 0\n" "function bump() count += 1; total = count end\n" "local function early() return later end\n" @@ -322,45 +330,50 @@ namespace tut "print(bump, early, outer, shared, total, x, y)\n" "count = 1\n"; const std::string said = found(script, "SlGlobalAssign", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("0 LuauLintSlGlobalAssign|count (another severity)\n" "1 LuauLintSlGlobalInFunction|total (another severity)\n" "3 LuauLintSlGlobalAssign|later (another severity)\n" "6 LuauLintSlGlobalFunctionInScope|inner\n" "7 LuauLintSlGlobalFunctionInScope|shared\n" "10 LuauLintSlGlobalAssign|x, y (another severity)\n")); - ensure_equals("the function at the top", found(script, "SlGlobalFunction", ALScriptProblem::Severity::Note), + UNSCOPED_INFO("the function at the top"); + CHECK(found(script, "SlGlobalFunction", ALScriptProblem::Severity::Note) == std::string("1 LuauLintSlGlobalFunction|bump\n")); - ensure_equals("off on its own: the rest still said", found("--!nolint SlGlobalFunction\n" + script, "SlGlobalAssign", ALScriptProblem::Severity::Warning), + UNSCOPED_INFO("off on its own: the rest still said"); + CHECK(found("--!nolint SlGlobalFunction\n" + script, "SlGlobalAssign", ALScriptProblem::Severity::Warning) == std::string("1 LuauLintSlGlobalAssign|count (another severity)\n" "2 LuauLintSlGlobalInFunction|total (another severity)\n" "4 LuauLintSlGlobalAssign|later (another severity)\n" "7 LuauLintSlGlobalFunctionInScope|inner\n" "8 LuauLintSlGlobalFunctionInScope|shared\n" "11 LuauLintSlGlobalAssign|x, y (another severity)\n")); - ensure("and it not", found("--!nolint SlGlobalFunction\n" + script, "SlGlobalFunction", ALScriptProblem::Severity::Note).empty()); - ensure("nor the function where SlGlobalAssign alone is off", - found("--!nolint SlGlobalAssign\n" + script, "SlGlobalFunction", ALScriptProblem::Severity::Note) == "2 LuauLintSlGlobalFunction|bump\n"); - ensure_equals("in place", fixed("count = 0\nprint(count)\n", "LuauLintSlGlobalAssign", "Write it local count", false), + UNSCOPED_INFO("and it not"); + REQUIRE(found("--!nolint SlGlobalFunction\n" + script, "SlGlobalFunction", ALScriptProblem::Severity::Note).empty()); + UNSCOPED_INFO("nor the function where SlGlobalAssign alone is off"); + CHECK(found("--!nolint SlGlobalAssign\n" + script, "SlGlobalFunction", ALScriptProblem::Severity::Note) == "2 LuauLintSlGlobalFunction|bump\n"); + UNSCOPED_INFO("in place"); + CHECK(require_fixed("count = 0\nprint(count)\n", "LuauLintSlGlobalAssign", "Write it local count", false) == std::string("local count = 0\nprint(count)\n")); - ensure_equals("declared first", fixed("local function early() return later end\nlater = 5\nprint(early())\n", "LuauLintSlGlobalAssign", - "Declare local later before it is first named", false), + UNSCOPED_INFO("declared first"); + CHECK(require_fixed("local function early() return later end\nlater = 5\nprint(early())\n", "LuauLintSlGlobalAssign", + "Declare local later before it is first named", false) == std::string("local later\nlocal function early() return later end\nlater = 5\nprint(early())\n")); - ensure_equals("a function", fixed("function f() return 1 end\nprint(f())\n", "LuauLintSlGlobalFunction", "Write it local function f", false), + UNSCOPED_INFO("a function"); + CHECK(require_fixed("function f() return 1 end\nprint(f())\n", "LuauLintSlGlobalFunction", "Write it local function f", false) == std::string("local function f() return 1 end\nprint(f())\n")); - ensure_equals("in a block", fixed("local function outer()\n function inner() return 1 end\n return inner()\nend\nprint(outer())\n", - "LuauLintSlGlobalFunctionInScope", "Write it local function inner", false), + UNSCOPED_INFO("in a block"); + CHECK(require_fixed("local function outer()\n function inner() return 1 end\n return inner()\nend\nprint(outer())\n", + "LuauLintSlGlobalFunctionInScope", "Write it local function inner", false) == std::string("local function outer()\n local function inner() return 1 end\n return inner()\nend\nprint(outer())\n")); - unfixed("local function outer()\n function shared() return 2 end\nend\nprint(outer, shared)\n", "LuauLintSlGlobalFunctionInScope"); - unfixed("print(_G)\ny = 2\n", "LuauLintSlGlobalAssign"); + require_unfixed("local function outer()\n function shared() return 2 end\nend\nprint(outer, shared)\n", "LuauLintSlGlobalFunctionInScope"); + require_unfixed("print(_G)\ny = 2\n", "LuauLintSlGlobalAssign"); } - template<> template<> - void object::test<6>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlParenCondition: a whole condition in brackets; taken out safely", "[ALScriptLintPass]") { - set_test_name("SlParenCondition: if, elseif, while, until and an if-then-else bracketed whole, a note; not brackets round a part; " - "the brackets taken out, a blank kept where a word would run on, safe; selene's name allows it"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local x = true\n" "if (x) then print(1) elseif (not x) then print(2) end\n" "while(x)do break end\n" @@ -369,27 +382,28 @@ namespace tut "if (x) and (x) then print(3) end\n" "if x then print(4) end\n", "SlParenCondition", ALScriptProblem::Severity::Note); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlParenCondition|if\n" "1 LuauLintSlParenCondition|elseif\n" "2 LuauLintSlParenCondition|while\n" "3 LuauLintSlParenCondition|until\n" "4 LuauLintSlParenCondition|if\n")); - ensure_equals("spaced", fixed("local x = true\nif (x) then print(1) end\n", "LuauLintSlParenCondition", "Take out the brackets", true), + UNSCOPED_INFO("spaced"); + CHECK(require_fixed("local x = true\nif (x) then print(1) end\n", "LuauLintSlParenCondition", "Take out the brackets", true) == std::string("local x = true\nif x then print(1) end\n")); - ensure_equals("run on", fixed("local x = true\nwhile(x)do break end\n", "LuauLintSlParenCondition", "Take out the brackets", true), + UNSCOPED_INFO("run on"); + CHECK(require_fixed("local x = true\nwhile(x)do break end\n", "LuauLintSlParenCondition", "Take out the brackets", true) == std::string("local x = true\nwhile x do break end\n")); - ensure_equals("allowed by selene's name", found("local x = true\n-- selene: allow(parenthese_conditions)\nif (x) then print(1) end\n", - "SlParenCondition", ALScriptProblem::Severity::Note), + UNSCOPED_INFO("allowed by selene's name"); + CHECK(found("local x = true\n-- selene: allow(parenthese_conditions)\nif (x) then print(1) end\n", + "SlParenCondition", ALScriptProblem::Severity::Note) == std::string()); } - template<> template<> - void object::test<7>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlAlmostSwapped: a = b then b = a is a warning fixed as a swap", "[ALScriptLintPass]") { - set_test_name("SlAlmostSwapped: a = b then b = a, locals, globals or fields, a warning, fixed as a swap; not a swap, nor with " - "something between"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local a, b = 1, 2\n" "a = b\n" "b = a\n" @@ -400,19 +414,18 @@ namespace tut "print(a)\n" "b = a\n", "SlAlmostSwapped", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlAlmostSwapped|a|b|a, b = b, a\n" "4 LuauLintSlAlmostSwapped|t.x|t.y|t.x, t.y = t.y, t.x\n")); - ensure_equals("swapped", fixed("local a, b = 1, 2\na = b\nb = a\nprint(a, b)\n", "LuauLintSlAlmostSwapped", "Write it a, b = b, a", false), + UNSCOPED_INFO("swapped"); + CHECK(require_fixed("local a, b = 1, 2\na = b\nb = a\nprint(a, b)\n", "LuauLintSlAlmostSwapped", "Write it a, b = b, a", false) == std::string("local a, b = 1, 2\na, b = b, a\nprint(a, b)\n")); } - template<> template<> - void object::test<8>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlMustUse: an answer left unread; given back to the first argument", "[ALScriptLintPass]") { - set_test_name("SlMustUse: ll's, llcompat's, a library's, a global's and a string's method that only answer, their answers unread; " - "given back where the first given is a variable of the kind answered; not a call that does something"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local l = {1, 2}\n" "local s = \"abc\"\n" "ll.DeleteSubList(l, 1, 1)\n" @@ -426,27 +439,27 @@ namespace tut "math.random()\n" "print(ll.DeleteSubList(l, 1, 1))\n", "SlMustUse", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("2 LuauLintSlMustUse|ll.DeleteSubList\n" "3 LuauLintSlMustUse|string.upper\n" "4 LuauLintSlMustUse|s:lower\n" "5 LuauLintSlMustUse|string.len\n" "6 LuauLintSlMustUse|tostring\n" "7 LuauLintSlMustUse|llcompat.GetPos\n")); - ensure_equals("a list", fixed("local l = {1, 2}\nll.DeleteSubList(l, 1, 1)\nprint(l)\n", "LuauLintSlMustUse", "Write it l = ll.DeleteSubList(...)", false), + UNSCOPED_INFO("a list"); + CHECK(require_fixed("local l = {1, 2}\nll.DeleteSubList(l, 1, 1)\nprint(l)\n", "LuauLintSlMustUse", "Write it l = ll.DeleteSubList(...)", false) == std::string("local l = {1, 2}\nl = ll.DeleteSubList(l, 1, 1)\nprint(l)\n")); - ensure_equals("a method", fixed("local s = \"abc\"\ns:upper()\nprint(s)\n", "LuauLintSlMustUse", "Write it s = s:upper(...)", false), + UNSCOPED_INFO("a method"); + CHECK(require_fixed("local s = \"abc\"\ns:upper()\nprint(s)\n", "LuauLintSlMustUse", "Write it s = s:upper(...)", false) == std::string("local s = \"abc\"\ns = s:upper()\nprint(s)\n")); - unfixed("local s = \"abc\"\nstring.len(s)\n", "LuauLintSlMustUse"); - unfixed("string.upper(\"abc\")\n", "LuauLintSlMustUse"); + require_unfixed("local s = \"abc\"\nstring.len(s)\n", "LuauLintSlMustUse"); + require_unfixed("string.upper(\"abc\")\n", "LuauLintSlMustUse"); } - template<> template<> - void object::test<9>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlGeneralizedFor: pairs and ipairs and next in a for become the table", "[ALScriptLintPass]") { - set_test_name("SlGeneralizedFor: pairs, ipairs and next, t in a for, a note, fixed as the table itself, not safe; not a function " - "of the script's, nor for over a table already"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local t = {1, 2}\n" "for k, v in pairs(t) do print(k, v) end\n" "for i, v in ipairs(t) do print(i, v) end\n" @@ -455,22 +468,22 @@ namespace tut "for k, v in walk(t) do print(k, v) end\n" "for k, v in t do print(k, v) end\n", "SlGeneralizedFor", ALScriptProblem::Severity::Note); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlGeneralizedFor|t|pairs(t)\n" "2 LuauLintSlGeneralizedForList|t|ipairs(t)\n" "3 LuauLintSlGeneralizedFor|t|next, t\n")); - ensure_equals("pairs", fixed("local t = {1}\nfor k, v in pairs(t) do print(k, v) end\n", "LuauLintSlGeneralizedFor", "Write it in t", false), + UNSCOPED_INFO("pairs"); + CHECK(require_fixed("local t = {1}\nfor k, v in pairs(t) do print(k, v) end\n", "LuauLintSlGeneralizedFor", "Write it in t", false) == std::string("local t = {1}\nfor k, v in t do print(k, v) end\n")); - ensure_equals("next", fixed("local t = {1}\nfor k, v in next, t do print(k, v) end\n", "LuauLintSlGeneralizedFor", "Write it in t", false), + UNSCOPED_INFO("next"); + CHECK(require_fixed("local t = {1}\nfor k, v in next, t do print(k, v) end\n", "LuauLintSlGeneralizedFor", "Write it in t", false) == std::string("local t = {1}\nfor k, v in t do print(k, v) end\n")); } - template<> template<> - void object::test<10>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlEmptyBlock: an empty block is a warning; an empty then or else fixed", "[ALScriptLintPass]") { - set_test_name("SlEmptyBlock: an empty if, elseif, else, while, for and repeat, a warning; not one holding a comment; an empty then " - "before an else turned round, and an empty else taken out, safe; selene's names allow it"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local c = true\n" "if c then end\n" "if c then else print(1) end\n" @@ -484,7 +497,7 @@ namespace tut "end\n" "while c do break end\n", "SlEmptyBlock", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlEmptyBlock|if\n" "2 LuauLintSlEmptyBlock|if\n" "3 LuauLintSlEmptyBlock|elseif\n" @@ -493,23 +506,25 @@ namespace tut "6 LuauLintSlEmptyBlock|for\n" "7 LuauLintSlEmptyBlock|for\n" "8 LuauLintSlEmptyBlock|repeat\n")); - ensure_equals("turned round", fixed("local c = true\nif c then else print(1) end\n", "LuauLintSlEmptyBlock", "Write it if not c then", false), + UNSCOPED_INFO("turned round"); + CHECK(require_fixed("local c = true\nif c then else print(1) end\n", "LuauLintSlEmptyBlock", "Write it if not c then", false) == std::string("local c = true\nif not c then print(1) end\n")); - ensure_equals("an else", fixed("local c = true\nif c then print(3) else end\n", "LuauLintSlEmptyBlock", "Take out the empty else", true), + UNSCOPED_INFO("an else"); + CHECK(require_fixed("local c = true\nif c then print(3) else end\n", "LuauLintSlEmptyBlock", "Take out the empty else", true) == std::string("local c = true\nif c then print(3) end\n")); - ensure_equals("an else on its own line", fixed("local c = true\nif c then\n print(3)\nelse\nend\n", "LuauLintSlEmptyBlock", "Take out the empty else", true), + UNSCOPED_INFO("an else on its own line"); + CHECK(require_fixed("local c = true\nif c then\n print(3)\nelse\nend\n", "LuauLintSlEmptyBlock", "Take out the empty else", true) == std::string("local c = true\nif c then\n print(3)\nend\n")); - ensure_equals("selene's names", found("local c = true\n-- selene: allow(empty_loop)\nwhile c do end\n-- selene: allow(empty_if)\nif c then end\n", - "SlEmptyBlock", ALScriptProblem::Severity::Warning), + UNSCOPED_INFO("selene's names"); + CHECK(found("local c = true\n-- selene: allow(empty_loop)\nwhile c do end\n-- selene: allow(empty_if)\nif c then end\n", + "SlEmptyBlock", ALScriptProblem::Severity::Warning) == std::string()); } - template<> template<> - void object::test<11>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlIndexDivision: a / in a list's index is a warning fixed as //", "[ALScriptLintPass]") { - set_test_name("SlIndexDivision: a / in what indexes a list, alone, bracketed or in a sum, read or written, a warning, fixed as //; " - "not //, nor a table that is no list, nor a count"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local t = {1, 2, 3}\n" "local n = #t\n" "print(t[n / 2], t[(n + 1) / 2], t[n / 2 + 1], t[n // 2])\n" @@ -517,22 +532,20 @@ namespace tut "local d = {a = 1}\n" "print(d[n / 2], string.rep(\"x\", n / 2))\n", "SlIndexDivision", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("2 LuauLintSlIndexDivision|n / 2|n // 2\n" "2 LuauLintSlIndexDivision|(n + 1) / 2|(n + 1) // 2\n" "2 LuauLintSlIndexDivision|n / 2|n // 2\n" "3 LuauLintSlIndexDivision|n / 2|n // 2\n")); - ensure_equals("rounded down", fixed("local t = {1}\nlocal n = #t\nprint(t[n / 2])\n", "LuauLintSlIndexDivision", "Write it n // 2", false), + UNSCOPED_INFO("rounded down"); + CHECK(require_fixed("local t = {1}\nlocal n = #t\nprint(t[n / 2])\n", "LuauLintSlIndexDivision", "Write it n // 2", false) == std::string("local t = {1}\nlocal n = #t\nprint(t[n // 2])\n")); } - template<> template<> - void object::test<12>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlVectorProduct: vectors' * as a number is vector.dot; % is vector.cross", "[ALScriptLintPass]") { - set_test_name("SlVectorProduct: two vectors multiplied where a number is wanted -- compared, added to one, given to math -- a " - "warning, fixed as vector.dot; taken % each other, the cross product as in LSL, a note naming vector.cross; not a " - "product kept a vector, nor by a number"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local a = vector(1, 0, 0)\n" "local b = vector(0, 1, 0)\n" "if a * b > 0.5 then print(1) end\n" @@ -541,25 +554,26 @@ namespace tut "local s = a * b\n" "print(a * 2, s, c)\n", "SlVectorProduct", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("2 LuauLintSlVectorProduct|a * b|a|b\n" "3 LuauLintSlVectorProduct|a * b|a|b\n" "3 LuauLintSlVectorProduct|a * b|a|b\n" "4 LuauLintSlVectorCross|a % b|a|b (another severity)\n")); - ensure_equals("a note", found("local a = vector(1, 0, 0)\nlocal c = a % a\nprint(c)\n", "SlVectorProduct", ALScriptProblem::Severity::Note), + UNSCOPED_INFO("a note"); + CHECK(found("local a = vector(1, 0, 0)\nlocal c = a % a\nprint(c)\n", "SlVectorProduct", ALScriptProblem::Severity::Note) == std::string("1 LuauLintSlVectorCross|a % a|a|a\n")); - ensure_equals("dot", fixed("local a = vector(1, 0, 0)\nprint(a * a > 0.5)\n", "LuauLintSlVectorProduct", "Write it vector.dot(a, a)", false), + UNSCOPED_INFO("dot"); + CHECK(require_fixed("local a = vector(1, 0, 0)\nprint(a * a > 0.5)\n", "LuauLintSlVectorProduct", "Write it vector.dot(a, a)", false) == std::string("local a = vector(1, 0, 0)\nprint(vector.dot(a, a) > 0.5)\n")); - ensure_equals("cross", fixed("local a = vector(1, 0, 0)\nlocal c = a % a\nprint(c)\n", "LuauLintSlVectorCross", "Write it vector.cross(a, a)", false), + UNSCOPED_INFO("cross"); + CHECK(require_fixed("local a = vector(1, 0, 0)\nlocal c = a % a\nprint(c)\n", "LuauLintSlVectorCross", "Write it vector.cross(a, a)", false) == std::string("local a = vector(1, 0, 0)\nlocal c = vector.cross(a, a)\nprint(c)\n")); } - template<> template<> - void object::test<13>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlSleepingCall in SLua: a sleeping call warned in loops; fixed as Fast", "[ALScriptLintPass]") { - set_test_name("SlSleepingCall in SLua: a call that sleeps where a Fast one does not, a note, and a warning in a loop or a timer; " - "fixed as the Fast call, its arguments put in or, where only settled, written again; a texture's, no fix"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local v = vector(1, 2, 3)\n" "ll.SetPos(v)\n" "for i = 1, 3 do ll.SetRot(quaternion(0, 0, 0, 1)) end\n" @@ -569,32 +583,33 @@ namespace tut "ll.SetLinkPrimitiveParamsFast(LINK_THIS, {})\n" "llcompat.SetPos(v)\n", "SlSleepingCall", ALScriptProblem::Severity::Note); - ensure_equals("each", said, + CHECK(said == std::string("1 LuauLintSlSleepingCall|ll.SetPos|0.2|ll.SetLinkPrimitiveParamsFast\n" "2 LuauLintSlSleepingCallOften|ll.SetRot|0.2|ll.SetLinkPrimitiveParamsFast (another severity)\n" "3 LuauLintSlSleepingCallOften|ll.SetPrimitiveParams|0.2|ll.SetLinkPrimitiveParamsFast (another severity)\n" "4 LuauLintSlSleepingTexture|ll.SetTexture|0.2|PRIM_TEXTURE\n" "5 LuauLintSlSleepingCall|ll.SetLinkRenderMaterial|0.2|ll.SetLinkPrimitiveParamsFast\n" "7 LuauLintSlSleepingCall|llcompat.SetPos|0.2|llcompat.SetLinkPrimitiveParamsFast\n")); - ensure_equals("put in", fixed("local v = vector(1, 2, 3)\nll.SetPos(v)\n", "LuauLintSlSleepingCall", - "Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_POSITION, v})", false), + UNSCOPED_INFO("put in"); + CHECK(require_fixed("local v = vector(1, 2, 3)\nll.SetPos(v)\n", "LuauLintSlSleepingCall", + "Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_POSITION, v})", false) == std::string("local v = vector(1, 2, 3)\nll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_POSITION, v})\n")); - ensure_equals("a link first, in a timer", fixed("LLTimers:every(1, function() ll.SetPrimitiveParams({}) end)\n", "LuauLintSlSleepingCallOften", - "Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {})", false), + UNSCOPED_INFO("a link first, in a timer"); + CHECK(require_fixed("LLTimers:every(1, function() ll.SetPrimitiveParams({}) end)\n", "LuauLintSlSleepingCallOften", + "Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {})", false) == std::string("LLTimers:every(1, function() ll.SetLinkPrimitiveParamsFast(LINK_THIS, {}) end)\n")); - ensure_equals("written again", fixed("ll.SetLinkRenderMaterial(LINK_THIS, \"m\", 0)\n", "LuauLintSlSleepingCall", - "Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_RENDER_MATERIAL, 0, \"m\"})", false), + UNSCOPED_INFO("written again"); + CHECK(require_fixed("ll.SetLinkRenderMaterial(LINK_THIS, \"m\", 0)\n", "LuauLintSlSleepingCall", + "Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_RENDER_MATERIAL, 0, \"m\"})", false) == std::string("ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_RENDER_MATERIAL, 0, \"m\"})\n")); - unfixed("ll.SetTexture(\"x\", 0)\n", "LuauLintSlSleepingTexture"); - unfixed("ll.SetRenderMaterial(tostring(1), 0)\n", "LuauLintSlSleepingCall"); + require_unfixed("ll.SetTexture(\"x\", 0)\n", "LuauLintSlSleepingTexture"); + require_unfixed("ll.SetRenderMaterial(tostring(1), 0)\n", "LuauLintSlSleepingCall"); } - template<> template<> - void object::test<14>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlSleepingCall in LSL: the same; fixed over commas and comments", "[ALScriptLintPass]") { - set_test_name("SlSleepingCall in LSL: the same table, a warning in a loop or the timer event; fixed over the text, a vector's commas " - "and a comment in the way"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string said = found("default {\n" " state_entry() {\n" " llSetPos(<1, 2, 3>);\n" @@ -608,28 +623,27 @@ namespace tut " }\n" "}\n", "SlSleepingCall", ALScriptProblem::Severity::Note, false); - ensure_equals("each", said, + CHECK(said == std::string("2 LSLSlSleepingCall|llSetPos|0.2|llSetLinkPrimitiveParamsFast\n" "4 LSLSlSleepingCallOften|llSetRot|0.2|llSetLinkPrimitiveParamsFast (another severity)\n" "5 LSLSlSleepingCall|llSetLinkRenderMaterial|0.2|llSetLinkPrimitiveParamsFast\n" "6 LSLSlSleepingTexture|llSetTexture|0.2|PRIM_TEXTURE\n" "9 LSLSlSleepingCallOften|llSetPrimitiveParams|0.2|llSetLinkPrimitiveParamsFast (another severity)\n")); - ensure_equals("put in", fixed("default { state_entry() { llSetPos( <1, 2, 3> /* here */ ); } }\n", "LSLSlSleepingCall", - "Write it llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_POSITION, <1, 2, 3> /* here */])", false, false), + UNSCOPED_INFO("put in"); + CHECK(require_fixed("default { state_entry() { llSetPos( <1, 2, 3> /* here */ ); } }\n", "LSLSlSleepingCall", + "Write it llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_POSITION, <1, 2, 3> /* here */])", false, false) == std::string("default { state_entry() { llSetLinkPrimitiveParamsFast( LINK_THIS, [PRIM_POSITION, <1, 2, 3> /* here */] ); } }\n")); - ensure_equals("written again", fixed("default { state_entry() { llSetLinkRenderMaterial(LINK_THIS, \"m\", ALL_SIDES); } }\n", "LSLSlSleepingCall", - "Write it llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_RENDER_MATERIAL, ALL_SIDES, \"m\"])", false, false), + UNSCOPED_INFO("written again"); + CHECK(require_fixed("default { state_entry() { llSetLinkRenderMaterial(LINK_THIS, \"m\", ALL_SIDES); } }\n", "LSLSlSleepingCall", + "Write it llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_RENDER_MATERIAL, ALL_SIDES, \"m\"])", false, false) == std::string("default { state_entry() { llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_RENDER_MATERIAL, ALL_SIDES, \"m\"]); } }\n")); - unfixed("default { state_entry() { llSetRenderMaterial(llGetInventoryName(INVENTORY_MATERIAL, 0), 0); } }\n", "LSLSlSleepingCall", false); + require_unfixed("default { state_entry() { llSetRenderMaterial(llGetInventoryName(INVENTORY_MATERIAL, 0), 0); } }\n", "LSLSlSleepingCall", false); } - template<> template<> - void object::test<15>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlMergeablePrimParams in SLua: a run of prim-params calls merged", "[ALScriptLintPass]") { - set_test_name("SlMergeablePrimParams in SLua: a run of the same prim-params call, its rules written out, a note; merged with " - "PRIM_LINK_TARGET where the link changes, or a list sends its rules elsewhere; broken by anything between, a call " - "whose arguments read the world, or another function"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_COLOR, ALL_SIDES, vector(1, 0, 0), 1})\n" "ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_GLOW, ALL_SIDES, 0.5})\n" "ll.SetLinkPrimitiveParamsFast(2, {PRIM_GLOW, ALL_SIDES, 0})\n" @@ -639,26 +653,27 @@ namespace tut "ll.SetPrimitiveParams({PRIM_GLOW, ALL_SIDES, 0})\n" "ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_GLOW, ALL_SIDES, 0})\n", "SlMergeablePrimParams", ALScriptProblem::Severity::Note); - ensure_equals("each", said, std::string("0 LuauLintSlMergeablePrimParams|3|ll.SetLinkPrimitiveParamsFast\n")); - ensure_equals("a link changed", fixed("ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_GLOW, ALL_SIDES, 0.5})\nll.SetLinkPrimitiveParamsFast(2, {PRIM_GLOW, ALL_SIDES, 0})\n", + CHECK(said == std::string("0 LuauLintSlMergeablePrimParams|3|ll.SetLinkPrimitiveParamsFast\n")); + UNSCOPED_INFO("a link changed"); + CHECK(require_fixed("ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_GLOW, ALL_SIDES, 0.5})\nll.SetLinkPrimitiveParamsFast(2, {PRIM_GLOW, ALL_SIDES, 0})\n", "LuauLintSlMergeablePrimParams", "Write it ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_GLOW, ALL_SIDES, 0.5, PRIM_LINK_TARGET, 2, PRIM_GLOW, ALL_SIDES, 0})", - false), + false) == std::string("ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_GLOW, ALL_SIDES, 0.5, PRIM_LINK_TARGET, 2, PRIM_GLOW, ALL_SIDES, 0})\n")); - ensure_equals("sent elsewhere, then back", fixed("ll.SetLinkPrimitiveParamsFast(1, {PRIM_LINK_TARGET, 2, PRIM_GLOW, ALL_SIDES, 1})\n" + UNSCOPED_INFO("sent elsewhere, then back"); + CHECK(require_fixed("ll.SetLinkPrimitiveParamsFast(1, {PRIM_LINK_TARGET, 2, PRIM_GLOW, ALL_SIDES, 1})\n" "ll.SetLinkPrimitiveParamsFast(1, {PRIM_GLOW, ALL_SIDES, 0})\n", "LuauLintSlMergeablePrimParams", "Write it ll.SetLinkPrimitiveParamsFast(1, {PRIM_LINK_TARGET, 2, PRIM_GLOW, ALL_SIDES, 1, PRIM_LINK_TARGET, 1, " "PRIM_GLOW, ALL_SIDES, 0})", - false), + false) == std::string("ll.SetLinkPrimitiveParamsFast(1, {PRIM_LINK_TARGET, 2, PRIM_GLOW, ALL_SIDES, 1, PRIM_LINK_TARGET, 1, PRIM_GLOW, ALL_SIDES, 0})\n")); } - template<> template<> - void object::test<16>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlMergeablePrimParams in LSL: the same over the text keeping the last ;", "[ALScriptLintPass]") { - set_test_name("SlMergeablePrimParams in LSL: the same, over the text, each call's ; but the last's taken with it"); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string said = found("default {\n" " state_entry() {\n" " llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, 0.5]);\n" @@ -668,34 +683,36 @@ namespace tut " }\n" "}\n", "SlMergeablePrimParams", ALScriptProblem::Severity::Note, false); - ensure_equals("each", said, std::string("2 LSLSlMergeablePrimParams|2|llSetLinkPrimitiveParamsFast\n")); - ensure_equals("a link changed", fixed("default { state_entry() {\n llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, 0.5]);\n" + CHECK(said == std::string("2 LSLSlMergeablePrimParams|2|llSetLinkPrimitiveParamsFast\n")); + UNSCOPED_INFO("a link changed"); + CHECK(require_fixed("default { state_entry() {\n llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, 0.5]);\n" " llSetLinkPrimitiveParamsFast(2, [ PRIM_GLOW, ALL_SIDES, 0.0 ]);\n} }\n", "LSLSlMergeablePrimParams", "Write it llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, 0.5, PRIM_LINK_TARGET, 2, PRIM_GLOW, ALL_SIDES, 0.0])", - false, false), + false, false) == std::string("default { state_entry() {\n llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, 0.5, PRIM_LINK_TARGET, 2, " "PRIM_GLOW, ALL_SIDES, 0.0]);\n} }\n")); - ensure_equals("no link", fixed("default { state_entry() { llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 0.5]); llSetPrimitiveParams([PRIM_COLOR, " + UNSCOPED_INFO("no link"); + CHECK(require_fixed("default { state_entry() { llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 0.5]); llSetPrimitiveParams([PRIM_COLOR, " "ALL_SIDES, <1, 0, 0>, 1.0]); } }\n", "LSLSlMergeablePrimParams", "Write it llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 0.5, PRIM_COLOR, ALL_SIDES, <1, 0, 0>, 1.0])", - false, false), + false, false) == std::string("default { state_entry() { llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 0.5, PRIM_COLOR, ALL_SIDES, <1, 0, 0>, 1.0]); } }\n")); } - template<> template<> - void object::test<17>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlCostlyListen and SlFastTimer and SlFastSensor: notes in both languages", "[ALScriptLintPass]") { - set_test_name("SlCostlyListen, SlFastTimer, SlFastSensor in both languages: notes, with no fix; not a listen narrowed, a timer " - "stopped or of a tenth, nor a sensor once a second"); - ensure("definitions: " + error, loaded); - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const auto three = [&](const std::string& script, bool lua) { return found(script, "SlCostlyListen", ALScriptProblem::Severity::Note, lua) + found(script, "SlFastTimer", ALScriptProblem::Severity::Note, lua) + found(script, "SlFastSensor", ALScriptProblem::Severity::Note, lua); }; - ensure_equals("SLua", three("ll.Listen(0, \"\", NULL_KEY, \"\")\n" + UNSCOPED_INFO("SLua"); + CHECK(three("ll.Listen(0, \"\", NULL_KEY, \"\")\n" "ll.Listen(PUBLIC_CHANNEL, \"\", \"\", \"hello\")\n" "ll.Listen(0, \"Bob\", NULL_KEY, \"\")\n" "ll.Listen(7, \"\", NULL_KEY, \"\")\n" @@ -705,13 +722,14 @@ namespace tut "llcompat.SetTimerEvent(0)\n" "ll.SensorRepeat(\"\", NULL_KEY, AGENT, 10, PI, 0.5)\n" "ll.SensorRepeat(\"\", NULL_KEY, AGENT, 10, PI, 1)\n", - true), + true) == std::string("0 LuauLintSlCostlyListen\n" "1 LuauLintSlCostlyListen\n" "4 LuauLintSlFastTimer|0.05\n" "5 LuauLintSlFastTimer|0.02\n" "8 LuauLintSlFastSensor|0.5\n")); - ensure_equals("LSL", three("default {\n" + UNSCOPED_INFO("LSL"); + CHECK(three("default {\n" " state_entry() {\n" " llListen(PUBLIC_CHANNEL, \"\", NULL_KEY, \"\");\n" " llListen(0, \"\", llGetOwner(), \"\");\n" @@ -720,20 +738,20 @@ namespace tut " llSensorRepeat(\"\", NULL_KEY, AGENT, 10.0, PI, 0.25);\n" " }\n" "}\n", - false), + false) == std::string("2 LSLSlCostlyListen\n" "4 LSLSlFastTimer|0.05\n" "6 LSLSlFastSensor|0.25\n")); } - template<> template<> - void object::test<18>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlStringBuild: a string grown in a loop; in SLua its pieces in a table", "[ALScriptLintPass]") { - set_test_name("SlStringBuild: a string joined to in a loop, from outside it, once for each string at its outermost loop, in both " - "languages; in SLua put in a table where it is a local of the block read in the loop by its appends alone"); - ensure("definitions: " + error, loaded); - ensure("builtins: " + error, lslLoaded); - ensure_equals("SLua", found("local s = \"\"\n" + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + UNSCOPED_INFO("SLua"); + CHECK(found("local s = \"\"\n" "for i = 1, 3 do s ..= tostring(i) end\n" "local t = \"\"\n" "for i = 1, 3 do t = t .. i; print(t) end\n" @@ -743,23 +761,26 @@ namespace tut "for i = 1, 2 do for j = 1, 2 do u ..= \"y\" end end\n" "for i = 1, 2 do local v = \"\"; v ..= \"z\"; print(v) end\n" "print(s, u)\n", - "SlStringBuild", ALScriptProblem::Severity::Note), + "SlStringBuild", ALScriptProblem::Severity::Note) == std::string("1 LuauLintSlStringBuild|s\n" "3 LuauLintSlStringBuild|t\n" "5 LuauLintSlStringBuild|g\n" "7 LuauLintSlStringBuild|u\n")); - ensure_equals("in a table", fixed("local s = \"\"\nfor i = 1, 3 do s ..= tostring(i) end\nprint(s)\n", "LuauLintSlStringBuild", - "Put s's pieces in a table, joined once after the loop", false), + UNSCOPED_INFO("in a table"); + CHECK(require_fixed("local s = \"\"\nfor i = 1, 3 do s ..= tostring(i) end\nprint(s)\n", "LuauLintSlStringBuild", + "Put s's pieces in a table, joined once after the loop", false) == std::string("local s = \"\"\nlocal sParts = {}\nfor i = 1, 3 do table.insert(sParts, tostring(i)) end\ns ..= table.concat(sParts)\nprint(s)\n")); - ensure_equals("named anew, indented", fixed("local function f()\n local s = \"\"\n local sParts = 1\n for i = 1, 3 do\n s = s .. i\n end\n" + UNSCOPED_INFO("named anew, indented"); + CHECK(require_fixed("local function f()\n local s = \"\"\n local sParts = 1\n for i = 1, 3 do\n s = s .. i\n end\n" " return s, sParts\nend\nprint(f())\n", - "LuauLintSlStringBuild", "Put s's pieces in a table, joined once after the loop", false), + "LuauLintSlStringBuild", "Put s's pieces in a table, joined once after the loop", false) == std::string("local function f()\n local s = \"\"\n local sParts = 1\n local sParts2 = {}\n for i = 1, 3 do\n" " table.insert(sParts2, tostring(i))\n end\n s ..= table.concat(sParts2)\n return s, sParts\nend\nprint(f())\n")); - unfixed("local t = \"\"\nfor i = 1, 3 do t = t .. i; print(t) end\n", "LuauLintSlStringBuild"); - unfixed("local function f()\n local s = \"\"\n for i = 1, 3 do\n s ..= i\n if i > 1 then return s end\n end\n return s\nend\nprint(f())\n", + require_unfixed("local t = \"\"\nfor i = 1, 3 do t = t .. i; print(t) end\n", "LuauLintSlStringBuild"); + require_unfixed("local function f()\n local s = \"\"\n for i = 1, 3 do\n s ..= i\n if i > 1 then return s end\n end\n return s\nend\nprint(f())\n", "LuauLintSlStringBuild"); - ensure_equals("LSL", found("default {\n" + UNSCOPED_INFO("LSL"); + CHECK(found("default {\n" " state_entry() {\n" " string s;\n" " integer i;\n" @@ -770,19 +791,18 @@ namespace tut " llOwnerSay(s + t);\n" " }\n" "}\n", - "SlStringBuild", ALScriptProblem::Severity::Note, false), + "SlStringBuild", ALScriptProblem::Severity::Note, false) == std::string("4 LSLSlStringBuild|s\n" "6 LSLSlStringBuild|t\n")); - ensure("the converter's note names it", std::string(ALLSLToSLua::lintOf("SluaStringBuild")) == "SlStringBuild"); + UNSCOPED_INFO("the converter's note names it"); + CHECK(std::string(ALLSLToSLua::lintOf("SluaStringBuild")) == "SlStringBuild"); } - template<> template<> - void object::test<19>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlRepeatedCall in SLua: a steady call made again kept in a local", "[ALScriptLintPass]") { - set_test_name("SlRepeatedCall in SLua: ll.GetOwner() and its kin called again in one function's body, not a nested one's; a " - "note, fixed as a local before the body's first statement that calls it"); - ensure("definitions: " + error, loaded); - ensure_equals("each", found("local function report()\n" + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); + CHECK(found("local function report()\n" " print(ll.GetOwner())\n" " if ll.GetOwner() == ll.GetKey() then print(ll.GetOwner()) end\n" " print(ll.GetKey())\n" @@ -790,22 +810,21 @@ namespace tut "print(ll.GetOwner(), llcompat.GetOwner())\n" "LLEvents:on(\"touch_start\", function() print(ll.GetScriptName()) print(ll.GetScriptName()) end)\n" "print(report)\n", - "SlRepeatedCall", ALScriptProblem::Severity::Note), + "SlRepeatedCall", ALScriptProblem::Severity::Note) == std::string("1 LuauLintSlRepeatedCall|ll.GetOwner()|3\n" "2 LuauLintSlRepeatedCall|ll.GetKey()|2\n" "6 LuauLintSlRepeatedCall|ll.GetScriptName()|2\n")); - ensure_equals("kept", fixed("local function report()\n print(ll.GetOwner())\n print(ll.GetOwner())\nend\nprint(report)\n", - "LuauLintSlRepeatedCall", "Keep ll.GetOwner() in a local, owner", false), + UNSCOPED_INFO("kept"); + CHECK(require_fixed("local function report()\n print(ll.GetOwner())\n print(ll.GetOwner())\nend\nprint(report)\n", + "LuauLintSlRepeatedCall", "Keep ll.GetOwner() in a local, owner", false) == std::string("local function report()\n local owner = ll.GetOwner()\n print(owner)\n print(owner)\nend\nprint(report)\n")); } - template<> template<> - void object::test<20>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlRepeatedCall in LSL: steady and pure calls repeated kept in a local", "[ALScriptLintPass]") { - set_test_name("SlRepeatedCall in LSL: a steady call, and a pure one given what the body never changes, declared by the body before; " - "not one given a loop's counter"); - ensure("builtins: " + error, lslLoaded); - ensure_equals("each", found("default {\n" + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + CHECK(found("default {\n" " touch_start(integer n) {\n" " string s = llDetectedName(0);\n" " llOwnerSay((string)llGetOwner());\n" @@ -814,46 +833,48 @@ namespace tut " for (i = 0; i < 2; ++i) llOwnerSay(llGetSubString(s, i, i) + llGetSubString(s, i, i));\n" " }\n" "}\n", - "SlRepeatedCall", ALScriptProblem::Severity::Note, false), + "SlRepeatedCall", ALScriptProblem::Severity::Note, false) == std::string("3 LSLSlRepeatedCall|llGetOwner()|2\n" "4 LSLSlRepeatedCall|llToUpper(s)|2\n")); - ensure_equals("a steady one", fixed("default { touch_start(integer n) {\n llOwnerSay((string)llGetOwner());\n llOwnerSay((string)llGetOwner());\n} }\n", - "LSLSlRepeatedCall", "Keep llGetOwner() in a local, owner", false, false), + UNSCOPED_INFO("a steady one"); + CHECK(require_fixed("default { touch_start(integer n) {\n llOwnerSay((string)llGetOwner());\n llOwnerSay((string)llGetOwner());\n} }\n", + "LSLSlRepeatedCall", "Keep llGetOwner() in a local, owner", false, false) == std::string("default { touch_start(integer n) {\n key owner = llGetOwner();\n llOwnerSay((string)owner);\n llOwnerSay((string)owner);\n} }\n")); - ensure_equals("a pure one", fixed("default { state_entry() {\n string s = \"a\";\n llOwnerSay(llToUpper(s) + llToUpper(s));\n} }\n", - "LSLSlRepeatedCall", "Keep llToUpper(s) in a local, toUpper", false, false), + UNSCOPED_INFO("a pure one"); + CHECK(require_fixed("default { state_entry() {\n string s = \"a\";\n llOwnerSay(llToUpper(s) + llToUpper(s));\n} }\n", + "LSLSlRepeatedCall", "Keep llToUpper(s) in a local, toUpper", false, false) == std::string("default { state_entry() {\n string s = \"a\";\n string toUpper = llToUpper(s);\n llOwnerSay(toUpper + toUpper);\n} }\n")); } - template<> template<> - void object::test<21>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: migration: migration lints and notes and llcompat calls left from LSL", "[ALScriptLintPass]") { - set_test_name("migration: a migration rule's lint, a converter's note and a call to llcompat are left from LSL; a performance " - "rule's lint and ll's own deprecation are not"); ALScriptProblem p; p.source = ALScriptProblem::Source::Lint; p.code = "SlZeroIndex"; - ensure("a migration rule's", ALScriptLintPass::migration(p)); + UNSCOPED_INFO("a migration rule's"); + CHECK(ALScriptLintPass::migration(p)); p.code = "SlSleepingCall"; - ensure("not a performance rule's", !ALScriptLintPass::migration(p)); + UNSCOPED_INFO("not a performance rule's"); + CHECK_FALSE(ALScriptLintPass::migration(p)); p.code = ""; p.key = "LuauLintDeprecatedMemberUse"; p.args = { "llcompat.Say" }; - ensure("llcompat's", ALScriptLintPass::migration(p)); + UNSCOPED_INFO("llcompat's"); + CHECK(ALScriptLintPass::migration(p)); p.args = { "ll.SetPrimitiveParams" }; - ensure("not ll's", !ALScriptLintPass::migration(p)); + UNSCOPED_INFO("not ll's"); + CHECK_FALSE(ALScriptLintPass::migration(p)); ALScriptProblem note; note.source = ALScriptProblem::Source::Assistant; note.key = "SluaNote"; - ensure("a note", ALScriptLintPass::migration(note)); + UNSCOPED_INFO("a note"); + CHECK(ALScriptLintPass::migration(note)); } - template<> template<> - void object::test<22>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlForIndexAssign: a numeric for's variable set in its body is a warning", "[ALScriptLintPass]") { - set_test_name("SlForIndexAssign: a numeric for's variable set in its body, plainly or by +=, a warning at each, in a function it makes " - "too; not a local of the same name in it, not a generic for's, not a while's; no fix"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local list = {\"a\", \"a\", \"b\"}\n" "local start = 1\n" "for i = 1, #list - 1 do\n" @@ -874,20 +895,17 @@ namespace tut "local n = 1\n" "while n < 3 do n = n + 1 end\n", "SlForIndexAssign", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("5 LuauLintSlForIndexAssign|i\n" "7 LuauLintSlForIndexAssign|i\n" "8 LuauLintSlForIndexAssign|i\n")); - unfixed("for i = 1, 3 do i = 2 end\n", "LuauLintSlForIndexAssign"); + require_unfixed("for i = 1, 3 do i = 2 end\n", "LuauLintSlForIndexAssign"); } - template<> template<> - void object::test<23>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlGlobalAssign: a global set in a function and named elsewhere goes local", "[ALScriptLintPass]") { - set_test_name("SlGlobalAssign: a global first set inside a function and named by more than one statement of the top, a note, " - "declared local before the first; one function alone naming it left to Luau's GlobalUsedAsLocal; one set at the top " - "said as before"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string shared = "function init()\n" " count = 0\n" "end\n" @@ -896,34 +914,35 @@ namespace tut " print(count)\n" "end)\n" "init()\n"; - ensure_equals("set in one, named in another", found(shared, "SlGlobalAssign", ALScriptProblem::Severity::Note), + UNSCOPED_INFO("set in one, named in another"); + CHECK(found(shared, "SlGlobalAssign", ALScriptProblem::Severity::Note) == std::string("1 LuauLintSlGlobalInFunction|count\n")); - ensure_equals("declared before the first that names it", fixed(shared, "LuauLintSlGlobalInFunction", "Declare local count before it is first named", false), + UNSCOPED_INFO("declared before the first that names it"); + CHECK(require_fixed(shared, "LuauLintSlGlobalInFunction", "Declare local count before it is first named", false) == std::string("local count\n") + shared); - ensure("one function alone: Luau's", - found("LLEvents:on(\"touch_start\", function(detected)\n total = 1\n print(total)\nend)\n", "SlGlobalAssign", ALScriptProblem::Severity::Note) + UNSCOPED_INFO("one function alone: Luau's"); + REQUIRE(found("LLEvents:on(\"touch_start\", function(detected)\n total = 1\n print(total)\nend)\n", "SlGlobalAssign", ALScriptProblem::Severity::Note) .find("LuauLintSlGlobalInFunction") == std::string::npos); - ensure("set at the top too: said there, not here", - found("count = 0\nfunction bump() count += 1 end\nbump()\n", "SlGlobalAssign", ALScriptProblem::Severity::Note).find("LuauLintSlGlobalInFunction") == + UNSCOPED_INFO("set at the top too: said there, not here"); + REQUIRE(found("count = 0\nfunction bump() count += 1 end\nbump()\n", "SlGlobalAssign", ALScriptProblem::Severity::Note).find("LuauLintSlGlobalInFunction") == std::string::npos); - ensure("not where _G is", - found(std::string("print(_G)\n") + shared, "SlGlobalAssign", ALScriptProblem::Severity::Note).find("LuauLintSlGlobalInFunction") == std::string::npos); - ensure("set at the top in a block of it: not in a function", - found("if true then\n x = 1\nend\nfunction f() x = 2 end\nf()\nprint(x)\n", "SlGlobalAssign", ALScriptProblem::Severity::Note) + UNSCOPED_INFO("not where _G is"); + REQUIRE(found(std::string("print(_G)\n") + shared, "SlGlobalAssign", ALScriptProblem::Severity::Note).find("LuauLintSlGlobalInFunction") == std::string::npos); + UNSCOPED_INFO("set at the top in a block of it: not in a function"); + REQUIRE(found("if true then\n x = 1\nend\nfunction f() x = 2 end\nf()\nprint(x)\n", "SlGlobalAssign", ALScriptProblem::Severity::Note) .find("LuauLintSlGlobalInFunction") == std::string::npos); - ensure("made by function f() at the top, its own lint off: made there", - found("--!nolint SlGlobalFunction\nfunction onTouch() end\nfunction reset() onTouch = nil end\nreset()\nonTouch()\n", "SlGlobalAssign", + UNSCOPED_INFO("made by function f() at the top, its own lint off: made there"); + REQUIRE(found("--!nolint SlGlobalFunction\nfunction onTouch() end\nfunction reset() onTouch = nil end\nreset()\nonTouch()\n", "SlGlobalAssign", ALScriptProblem::Severity::Note) .find("LuauLintSlGlobalInFunction") == std::string::npos); } - template<> template<> - void object::test<24>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlIntegerPast32Bits: an integer literal past 0xFFFFFFFF reads as -1", "[ALScriptLintPass]") { - set_test_name("SlIntegerPast32Bits: a whole number past 0xFFFFFFFF, decimal or hexadecimal, read as -1 and as 1 with a minus before it; " - "not one within 32 bits, a float, or one in a string or a comment"); - ensure("builtins: " + error, lslLoaded); - ensure_equals("LSL", found("integer a = 4294967296;\n" + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + UNSCOPED_INFO("LSL"); + CHECK(found("integer a = 4294967296;\n" "integer b = 0x100000000;\n" "integer c = -4294967296;\n" "integer d = 99999999999999999999999;\n" @@ -934,23 +953,21 @@ namespace tut " vector v = <4294967296, 0, 0>;\n" " llOwnerSay((string)(k + b + c + d + e + f + g) + (string)h + (string)i + j + (string)v);\n" "} }\n", - "SlIntegerPast32Bits", ALScriptProblem::Severity::Warning, false), + "SlIntegerPast32Bits", ALScriptProblem::Severity::Warning, false) == std::string("0 LSLSlIntegerPast32Bits|4294967296|-1\n" "1 LSLSlIntegerPast32Bits|0x100000000|-1\n" "2 LSLSlIntegerPast32Bits|-4294967296|1\n" "3 LSLSlIntegerPast32Bits|99999999999999999999999|-1\n" "7 LSLSlIntegerPast32Bits|4294967296|-1\n" "8 LSLSlIntegerPast32Bits|4294967296|-1\n")); - ensure_equals("not SLua's, whose numbers are doubles", found("local a = 4294967296\n", "SlIntegerPast32Bits", ALScriptProblem::Severity::Warning, true), + UNSCOPED_INFO("not SLua's, whose numbers are doubles"); + CHECK(found("local a = 4294967296\n", "SlIntegerPast32Bits", ALScriptProblem::Severity::Warning, true) == std::string()); } - template<> template<> - void object::test<25>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlNumberTruth beyond numbers: other types read as conditions are fixed", "[ALScriptLintPass]") { - set_test_name("SlNumberTruth beyond numbers: a uuid, a vector, a quaternion, a string, a list and an integer read as a condition, or " - "under not, each fixed as LSL asked it; not a boolean, a table that is no list, nor what may be nil though typed not -- " - "a table read by a key, a local declared with no value, or one given either"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local id = ll.GetOwner()\n" "local v = ll.GetPos()\n" "local r = ll.GetRot()\n" @@ -972,7 +989,7 @@ namespace tut "local b = true\n" "if t or b or ll.GetOwner() then print(t) end\n", "SlNumberTruth", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("6 LuauLintSlUuidTruth|id|id.istruthy\n" "7 LuauLintSlVectorTruth|v|v ~= ZERO_VECTOR\n" "7 LuauLintSlStringTruth|s|s ~= \"\"\n" @@ -986,34 +1003,40 @@ namespace tut "11 LuauLintSlQuaternionTruthNot|r|r == ZERO_ROTATION\n" "11 LuauLintSlIntegerTruthNot|i|i == 0i\n" "19 LuauLintSlUuidTruth|ll.GetOwner()|ll.GetOwner().istruthy\n")); - ensure_equals("a uuid", fixed("local id = ll.GetOwner()\nif id then print(id) end\n", "LuauLintSlUuidTruth", "Write it id.istruthy", false), + UNSCOPED_INFO("a uuid"); + CHECK(require_fixed("local id = ll.GetOwner()\nif id then print(id) end\n", "LuauLintSlUuidTruth", "Write it id.istruthy", false) == std::string("local id = ll.GetOwner()\nif id.istruthy then print(id) end\n")); - ensure_equals("a uuid under not", fixed("if not ll.GetOwner() then print(1) end\n", "LuauLintSlUuidTruthNot", "Write it not ll.GetOwner().istruthy", false), + UNSCOPED_INFO("a uuid under not"); + CHECK(require_fixed("if not ll.GetOwner() then print(1) end\n", "LuauLintSlUuidTruthNot", "Write it not ll.GetOwner().istruthy", false) == std::string("if not ll.GetOwner().istruthy then print(1) end\n")); - ensure_equals("a vector", fixed("local v = ll.GetPos()\nwhile v do break end\n", "LuauLintSlVectorTruth", "Write it v ~= ZERO_VECTOR", false), + UNSCOPED_INFO("a vector"); + CHECK(require_fixed("local v = ll.GetPos()\nwhile v do break end\n", "LuauLintSlVectorTruth", "Write it v ~= ZERO_VECTOR", false) == std::string("local v = ll.GetPos()\nwhile v ~= ZERO_VECTOR do break end\n")); - ensure_equals("a quaternion", fixed("local r = ll.GetRot()\nprint(not r)\n", "LuauLintSlQuaternionTruthNot", "Write it r == ZERO_ROTATION", false), + UNSCOPED_INFO("a quaternion"); + CHECK(require_fixed("local r = ll.GetRot()\nprint(not r)\n", "LuauLintSlQuaternionTruthNot", "Write it r == ZERO_ROTATION", false) == std::string("local r = ll.GetRot()\nprint(r == ZERO_ROTATION)\n")); - ensure_equals("a string, an if-then-else bracketed first", fixed("local c = true\nif (if c then \"a\" else \"\") then print(c) end\n", - "LuauLintSlStringTruth", "Write it (if c then \"a\" else \"\") ~= \"\"", false), + UNSCOPED_INFO("a string, an if-then-else bracketed first"); + CHECK(require_fixed("local c = true\nif (if c then \"a\" else \"\") then print(c) end\n", + "LuauLintSlStringTruth", "Write it (if c then \"a\" else \"\") ~= \"\"", false) == std::string("local c = true\nif ((if c then \"a\" else \"\") ~= \"\") then print(c) end\n")); - ensure_equals("a list", fixed("local l = {1}\nif l then print(l) end\n", "LuauLintSlListTruth", "Write it #l > 0", false), + UNSCOPED_INFO("a list"); + CHECK(require_fixed("local l = {1}\nif l then print(l) end\n", "LuauLintSlListTruth", "Write it #l > 0", false) == std::string("local l = {1}\nif #l > 0 then print(l) end\n")); - ensure_equals("a list under not", fixed("local l = {1}\nprint(not l)\n", "LuauLintSlListTruthNot", "Write it #l == 0", false), + UNSCOPED_INFO("a list under not"); + CHECK(require_fixed("local l = {1}\nprint(not l)\n", "LuauLintSlListTruthNot", "Write it #l == 0", false) == std::string("local l = {1}\nprint(#l == 0)\n")); - ensure_equals("an integer", fixed("local i = 5i\nif i then print(i) end\n", "LuauLintSlIntegerTruth", "Write it i ~= 0i", false), + UNSCOPED_INFO("an integer"); + CHECK(require_fixed("local i = 5i\nif i then print(i) end\n", "LuauLintSlIntegerTruth", "Write it i ~= 0i", false) == std::string("local i = 5i\nif i ~= 0i then print(i) end\n")); - ensure_equals("a number read by a key: not said either", found("local counts: {[string]: number} = {}\nif counts[\"a\"] then print(1) end\n", - "SlNumberTruth", ALScriptProblem::Severity::Warning), + UNSCOPED_INFO("a number read by a key: not said either"); + CHECK(found("local counts: {[string]: number} = {}\nif counts[\"a\"] then print(1) end\n", + "SlNumberTruth", ALScriptProblem::Severity::Warning) == std::string()); } - template<> template<> - void object::test<26>() + TEST_CASE_METHOD(alscriptlintpass_data, "ALScriptLintPass: SlMustUse over SLua's own types: their makers and readers left unread", "[ALScriptLintPass]") { - set_test_name("SlMustUse over SLua's own: integer's, a uuid, a vector and a quaternion made by calling their names or create, " - "buffer's that read one or make one, and table.create, their answers unread; an integer's given back to it; not " - "buffer's that write one, nor an answer read"); - ensure("definitions: " + error, loaded); + UNSCOPED_INFO("definitions: " + error); + CHECK(loaded); const std::string said = found("local i = 5i\n" "local b = buffer.create(4)\n" "integer.add(i, 1)\n" @@ -1030,7 +1053,7 @@ namespace tut "buffer.fill(b, 0, 0)\n" "print(integer.add(i, 1), buffer.readu8(b, 0), vector(1, 2, 3))\n", "SlMustUse", ALScriptProblem::Severity::Warning); - ensure_equals("each", said, + CHECK(said == std::string("2 LuauLintSlMustUse|integer.add\n" "3 LuauLintSlMustUse|uuid\n" "4 LuauLintSlMustUse|uuid.create\n" @@ -1041,8 +1064,9 @@ namespace tut "9 LuauLintSlMustUse|buffer.len\n" "10 LuauLintSlMustUse|buffer.create\n" "11 LuauLintSlMustUse|table.create\n")); - ensure_equals("an integer's", fixed("local i = 5i\ninteger.band(i, 3i)\nprint(i)\n", "LuauLintSlMustUse", "Write it i = integer.band(...)", false), + UNSCOPED_INFO("an integer's"); + CHECK(require_fixed("local i = 5i\ninteger.band(i, 3i)\nprint(i)\n", "LuauLintSlMustUse", "Write it i = integer.band(...)", false) == std::string("local i = 5i\ni = integer.band(i, 3i)\nprint(i)\n")); - unfixed("vector(1, 2, 3)\n", "LuauLintSlMustUse"); + require_unfixed("vector(1, 2, 3)\n", "LuauLintSlMustUse"); } } diff --git a/indra/alscript/tests/alscriptmessages_test.cpp b/indra/alscript/tests/alscriptmessages_test.cpp index c43ad4e4a9..355b321273 100644 --- a/indra/alscript/tests/alscriptmessages_test.cpp +++ b/indra/alscript/tests/alscriptmessages_test.cpp @@ -26,145 +26,168 @@ #include "../core/alscriptmessages.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { - struct alscriptmessages_data + TEST_CASE("alscriptmessages: an LSL compiler's line gives its zero-based place and its level and words", "[alscriptmessages]") { - }; - typedef test_group alscriptmessages_group; - typedef alscriptmessages_group::object alscriptmessages_object; - tut::alscriptmessages_group alscriptmessages_instance("alscriptmessages"); - - template<> template<> - void alscriptmessages_object::test<1>() - { - set_test_name("an LSL compiler's line gives its place, its level and its words, zero-based as the viewer counts"); const ALScriptMessages::Place place = ALScriptMessages::readDiagnostic("(12, 4) : ERROR : Syntax error", false); - ensure_equals("the line as it came", place.line, 12); - ensure_equals("the column too", place.column, 4); - ensure("a column was given", place.hasColumn); - ensure_equals("the level", place.level, std::string("ERROR")); - ensure_equals("the words", place.message, std::string("Syntax error")); + UNSCOPED_INFO("the line as it came"); + CHECK(place.line == 12); + CHECK(place.column == 4); + CHECK(place.hasColumn); + CHECK(place.level == std::string("ERROR")); + CHECK(place.message == std::string("Syntax error")); const ALScriptMessages::Place warned = ALScriptMessages::readDiagnostic("(3, 0) : WARNING : Unused variable x", false); - ensure_equals("a warning says so", warned.level, std::string("WARNING")); + CHECK(warned.level == std::string("WARNING")); // Anything else is an error with no place: "Math Error" comes so. const ALScriptMessages::Place bare = ALScriptMessages::readDiagnostic("Math Error", false); - ensure_equals("no place", bare.line, 0); - ensure("and none given", !bare.hasColumn && !bare.hasLine && place.hasLine); - ensure_equals("an error all the same", bare.level, std::string("ERROR")); - ensure_equals("the words as they came", bare.message, std::string("Math Error")); + UNSCOPED_INFO("no place"); + CHECK(bare.line == 0); + { + INFO("and none given"); + CHECK_FALSE(bare.hasColumn); + CHECK_FALSE(bare.hasLine); + CHECK(place.hasLine); + } + UNSCOPED_INFO("an error all the same"); + CHECK(bare.level == std::string("ERROR")); + UNSCOPED_INFO("the words as they came"); + CHECK(bare.message == std::string("Math Error")); } - template<> template<> - void alscriptmessages_object::test<2>() + TEST_CASE("alscriptmessages: Luau's line names its chunk and is moved from one-based to zero-based", "[alscriptmessages]") { - set_test_name("Luau's line names the chunk and counts from one, which the viewer counts from zero"); const ALScriptMessages::Place place = ALScriptMessages::readDiagnostic("script:14: unexpected symbol near 'end'", true); - ensure_equals("one off the line", place.line, 13); - ensure("no column", !place.hasColumn); - ensure_equals("an error", place.level, std::string("ERROR")); - ensure_equals("the words past the colon", place.message, std::string("unexpected symbol near 'end'")); + UNSCOPED_INFO("one off the line"); + CHECK(place.line == 13); + CHECK_FALSE(place.hasColumn); + CHECK(place.level == std::string("ERROR")); + UNSCOPED_INFO("the words past the colon"); + CHECK(place.message == std::string("unexpected symbol near 'end'")); // Line one stays line zero rather than going negative. - ensure_equals("the first line", ALScriptMessages::readDiagnostic("x:1: bad", true).line, 0); + CHECK(ALScriptMessages::readDiagnostic("x:1: bad", true).line == 0); // A line of neither form is no place: the whole is the words. const ALScriptMessages::Place other = ALScriptMessages::readDiagnostic("Math Error", true); - ensure_equals("nothing found", other.message, std::string("Math Error")); - ensure("no line named", !other.hasLine && place.hasLine); + UNSCOPED_INFO("nothing found"); + CHECK(other.message == std::string("Math Error")); + { + INFO("no line named"); + CHECK_FALSE(other.hasLine); + CHECK(place.hasLine); + } } - template<> template<> - void alscriptmessages_object::test<3>() + TEST_CASE("alscriptmessages: every line of a compile's answer in order", "[alscriptmessages]") { - set_test_name("every line of a compile's answer, in order"); LLSD errors; errors.append("(1, 2) : ERROR : first"); errors.append("(3, 4) : WARNING : second"); const std::vector places = ALScriptMessages::readDiagnostics(errors, false); - ensure_equals("both", places.size(), size_t(2)); - ensure_equals("in order", places[1].message, std::string("second")); - ensure_equals("with their places", places[1].line, 3); + REQUIRE(places.size() == size_t(2)); + UNSCOPED_INFO("in order"); + CHECK(places[1].message == std::string("second")); + UNSCOPED_INFO("with their places"); + CHECK(places[1].line == 3); } - template<> template<> - void alscriptmessages_object::test<4>() + TEST_CASE("alscriptmessages: a script with no default state is taken for Lua", "[alscriptmessages]") { - set_test_name("a script with no default state is taken for Lua"); - ensure("no state anywhere", ALScriptMessages::looksLikeLua("local x = 1\nprint(x)\n")); - ensure("a default state is LSL", !ALScriptMessages::looksLikeLua("default\n{\n state_entry()\n {\n }\n}\n")); - ensure("however it is spaced", !ALScriptMessages::looksLikeLua("default {")); - ensure("and after other code", !ALScriptMessages::looksLikeLua("integer n;\nstate two { }\ndefault {\n}\n")); + UNSCOPED_INFO("no state anywhere"); + CHECK(ALScriptMessages::looksLikeLua("local x = 1\nprint(x)\n")); + UNSCOPED_INFO("a default state is LSL"); + CHECK_FALSE(ALScriptMessages::looksLikeLua("default\n{\n state_entry()\n {\n }\n}\n")); + UNSCOPED_INFO("however it is spaced"); + CHECK_FALSE(ALScriptMessages::looksLikeLua("default {")); + UNSCOPED_INFO("and after other code"); + CHECK_FALSE(ALScriptMessages::looksLikeLua("integer n;\nstate two { }\ndefault {\n}\n")); } - template<> template<> - void alscriptmessages_object::test<5>() + TEST_CASE("alscriptmessages: a run-time error's header names the object and script and marks what follows", "[alscriptmessages]") { - set_test_name("a run-time error's header names the object and the script, and the lines that follow it belong with it"); ALScriptMessages::Header named; - ensure("a header", ALScriptMessages::readRuntimeHeader("Object [script:My Script] Script run-time error", named)); - ensure_equals("the object", named.object, std::string("Object")); - ensure_equals("the script, spaces and all", named.script, std::string("My Script")); - ensure("ending with the words marks it", ALScriptMessages::endsRuntimeError("Object [script:x] Script run-time error")); - ensure("an ordinary line is no header", !ALScriptMessages::readRuntimeHeader("Object: hello there", named)); - ensure("nor marked", !ALScriptMessages::endsRuntimeError("Object: hello there")); + REQUIRE(ALScriptMessages::readRuntimeHeader("Object [script:My Script] Script run-time error", named)); + CHECK(named.object == std::string("Object")); + UNSCOPED_INFO("the script, spaces and all"); + CHECK(named.script == std::string("My Script")); + UNSCOPED_INFO("ending with the words marks it"); + CHECK(ALScriptMessages::endsRuntimeError("Object [script:x] Script run-time error")); + UNSCOPED_INFO("an ordinary line is no header"); + CHECK_FALSE(ALScriptMessages::readRuntimeHeader("Object: hello there", named)); + UNSCOPED_INFO("nor marked"); + CHECK_FALSE(ALScriptMessages::endsRuntimeError("Object: hello there")); // The words alone, without the brackets, are not a header. - ensure("no script named", !ALScriptMessages::readRuntimeHeader("Script run-time error", named)); + UNSCOPED_INFO("no script named"); + CHECK_FALSE(ALScriptMessages::readRuntimeHeader("Script run-time error", named)); } - template<> template<> - void alscriptmessages_object::test<6>() + TEST_CASE("alscriptmessages: where a run-time error happened is read by the VM that said it", "[alscriptmessages]") { - set_test_name("where a run-time error happened, from the lines after the header, by the VM that said it"); ALScriptMessages::Location where; const std::vector luau = { "Object [script:s] Script run-time error", "s:7: attempt to index nil", "stack traceback:" }; - ensure("Luau's", ALScriptMessages::readRuntimeLocation(luau, true, where)); - ensure_equals("one off its line", where.line, 6); - ensure_equals("no column", where.column, -1); - ensure_equals("the words", where.message, std::string("attempt to index nil")); + UNSCOPED_INFO("Luau's"); + REQUIRE(ALScriptMessages::readRuntimeLocation(luau, true, where)); + UNSCOPED_INFO("one off its line"); + CHECK(where.line == 6); + UNSCOPED_INFO("no column"); + CHECK(where.column == -1); + CHECK(where.message == std::string("attempt to index nil")); const std::vector lsl = { "Object [script:s] Script run-time error", "(5, 2) : ERROR : Stack-Heap Collision" }; - ensure("LSL's", ALScriptMessages::readRuntimeLocation(lsl, false, where)); - ensure_equals("the line as it came", where.line, 5); - ensure_equals("and the column", where.column, 2); - ensure_equals("the words past the level", where.message, std::string("Stack-Heap Collision")); + UNSCOPED_INFO("LSL's"); + CHECK(ALScriptMessages::readRuntimeLocation(lsl, false, where)); + UNSCOPED_INFO("the line as it came"); + CHECK(where.line == 5); + CHECK(where.column == 2); + UNSCOPED_INFO("the words past the level"); + CHECK(where.message == std::string("Stack-Heap Collision")); // Nothing says where: the caller falls back to the first line // that is not the header. const std::vector nowhere = { "Object [script:s] Script run-time error", "Math Error" }; - ensure("nothing found", !ALScriptMessages::readRuntimeLocation(nowhere, false, where)); + UNSCOPED_INFO("nothing found"); + CHECK_FALSE(ALScriptMessages::readRuntimeLocation(nowhere, false, where)); // An LSL error read as Luau's is not read by it. - ensure("the wrong VM finds nothing", !ALScriptMessages::readRuntimeLocation(lsl, true, where) || where.line != 5); + UNSCOPED_INFO("the wrong VM finds nothing"); + CHECK((!ALScriptMessages::readRuntimeLocation(lsl, true, where) || where.line != 5)); } - template<> template<> - void alscriptmessages_object::test<7>() + TEST_CASE("alscriptmessages: a Luau traceback frame gives its chunk and line and nothing else is one", "[alscriptmessages]") { - set_test_name("a Luau traceback's frames, each its chunk and its line, and nothing else read as one"); ALScriptMessages::Frame frame; - ensure("a bare frame", ALScriptMessages::readStackFrame("s:7", frame)); - ensure_equals("its chunk", frame.chunk, std::string("s")); - ensure_equals("its line, one off", frame.line, 6); - ensure("indented, with the function", ALScriptMessages::readStackFrame(" My Script:12 function counter", frame)); - ensure_equals("a script's name, spaces and all", frame.chunk, std::string("My Script")); - ensure_equals("that line", frame.line, 11); - ensure("as Lua quotes a chunk", ALScriptMessages::readStackFrame("[string \"My Script\"]:30: in function 'tick'", frame)); - ensure_equals("the quoted name, spaces and all", frame.chunk, std::string("My Script")); - ensure_equals("and its line", frame.line, 29); - ensure("the heading is no frame", !ALScriptMessages::readStackFrame("stack traceback:", frame)); - ensure("nor a C function", !ALScriptMessages::readStackFrame("[C] function error", frame)); - ensure("nor the error's own line", !ALScriptMessages::readStackFrame("s:7: attempt to index nil", frame)); - ensure("nor words", !ALScriptMessages::readStackFrame("Math Error", frame)); - ensure("nor a line zero", !ALScriptMessages::readStackFrame("s:0", frame)); + UNSCOPED_INFO("a bare frame"); + REQUIRE(ALScriptMessages::readStackFrame("s:7", frame)); + CHECK(frame.chunk == std::string("s")); + UNSCOPED_INFO("its line, one off"); + CHECK(frame.line == 6); + UNSCOPED_INFO("indented, with the function"); + CHECK(ALScriptMessages::readStackFrame(" My Script:12 function counter", frame)); + UNSCOPED_INFO("a script's name, spaces and all"); + CHECK(frame.chunk == std::string("My Script")); + CHECK(frame.line == 11); + UNSCOPED_INFO("as Lua quotes a chunk"); + CHECK(ALScriptMessages::readStackFrame("[string \"My Script\"]:30: in function 'tick'", frame)); + UNSCOPED_INFO("the quoted name, spaces and all"); + CHECK(frame.chunk == std::string("My Script")); + CHECK(frame.line == 29); + UNSCOPED_INFO("the heading is no frame"); + CHECK_FALSE(ALScriptMessages::readStackFrame("stack traceback:", frame)); + UNSCOPED_INFO("nor a C function"); + CHECK_FALSE(ALScriptMessages::readStackFrame("[C] function error", frame)); + UNSCOPED_INFO("nor the error's own line"); + CHECK_FALSE(ALScriptMessages::readStackFrame("s:7: attempt to index nil", frame)); + UNSCOPED_INFO("nor words"); + CHECK_FALSE(ALScriptMessages::readStackFrame("Math Error", frame)); + UNSCOPED_INFO("nor a line zero"); + CHECK_FALSE(ALScriptMessages::readStackFrame("s:0", frame)); } - template<> template<> - void alscriptmessages_object::test<8>() + TEST_CASE("alscriptmessages: a SLua fault as the simulator says it: its place and the script's frames", "[alscriptmessages]") { // As the simulator's executor says a fault: the error as the VM // raised it, then lua_debugtrace's frames -- the script loaded as // "=lua_script", so its chunk reads `lua_script`; a C function as // `[C]`; a deep stack's middle folded -- each on a line of its // own (slua Executor/src/Script.cpp, VM/src/ldebug.cpp). - set_test_name("a SLua fault as the simulator says it: where it happened, and each frame of the script"); const std::vector said = { "Object [script:Counter] Script run-time error", "lua_script:12: attempt to index nil with 'field'", "[C] function error", @@ -172,79 +195,114 @@ namespace tut "... (+4 frames)", "lua_script:30" }; ALScriptMessages::Header named; - ensure("the header", ALScriptMessages::readRuntimeHeader(said[0], named)); - ensure_equals("names the script", named.script, std::string("Counter")); + REQUIRE(ALScriptMessages::readRuntimeHeader(said[0], named)); + CHECK(named.script == std::string("Counter")); ALScriptMessages::Location where; - ensure("where", ALScriptMessages::readRuntimeLocation(said, true, where)); - ensure_equals("its line", where.line, 11); - ensure_equals("its words", where.message, std::string("attempt to index nil with 'field'")); + REQUIRE(ALScriptMessages::readRuntimeLocation(said, true, where)); + CHECK(where.line == 11); + CHECK(where.message == std::string("attempt to index nil with 'field'")); std::vector frames; for (size_t i = 1; i < said.size(); ++i) { ALScriptMessages::Frame frame; if (ALScriptMessages::readStackFrame(said[i], frame)) { - ensure_equals("the script's chunk", frame.chunk, std::string("lua_script")); + UNSCOPED_INFO("the script's chunk"); + REQUIRE(frame.chunk == std::string("lua_script")); frames.push_back(frame.line); } } - ensure_equals("the script's two frames, and nothing else", frames.size(), size_t(2)); - ensure_equals("the function's", frames[0], 11); - ensure_equals("the one that called it", frames[1], 29); + UNSCOPED_INFO("the script's two frames, and nothing else"); + REQUIRE(frames.size() == size_t(2)); + UNSCOPED_INFO("the function's"); + CHECK(frames[0] == 11); + UNSCOPED_INFO("the one that called it"); + CHECK(frames[1] == 29); } - template<> template<> - void alscriptmessages_object::test<9>() + TEST_CASE("alscriptmessages: continuesRuntimeError tells a run-time error's lines from a script's own", "[alscriptmessages]") { - set_test_name("what goes on with a run-time error after its words, and what a script says besides"); for (const char* goes_on : { "lua_script:12: attempt to index nil with 'field'", "stack traceback:", "[C] function error", "lua_script:12 function tick", " lua_script:30", "... (+4 frames)", "(12, 3) : ERROR : Stack-Heap Collision" }) { - ensure(std::string("goes on: ") + goes_on, ALScriptMessages::continuesRuntimeError(goes_on)); + CAPTURE(goes_on); + REQUIRE(ALScriptMessages::continuesRuntimeError(goes_on)); } for (const char* besides : { "hello", "Counter: 12 visitors", "", " ", "Object [script:Counter] Script run-time error" }) { - ensure(std::string("its own: ") + besides, !ALScriptMessages::continuesRuntimeError(besides)); + CAPTURE(besides); + REQUIRE_FALSE(ALScriptMessages::continuesRuntimeError(besides)); } } - template<> template<> - void alscriptmessages_object::test<10>() + TEST_CASE("alscriptmessages: a line in the other compiler's form is read in that form", "[alscriptmessages]") { - set_test_name("a line in the other compiler's form is read in that form: an LSL script the grid compiles for Luau's VM may be " - "answered in either"); const ALScriptMessages::Place luau = ALScriptMessages::readDiagnostic("script:7: bad thing", false); - ensure("Luau's, for LSL", luau.hasLine && luau.line == 6 && !luau.hasColumn && luau.message == "bad thing"); + { + INFO("Luau's, for LSL"); + CHECK(luau.hasLine); + CHECK(luau.line == 6); + CHECK_FALSE(luau.hasColumn); + CHECK(luau.message == "bad thing"); + } const ALScriptMessages::Place lsl = ALScriptMessages::readDiagnostic("(4, 2) : ERROR : Syntax error", true); - ensure("LSL's, for SLua", lsl.hasLine && lsl.line == 4 && lsl.hasColumn && lsl.column == 2 && lsl.message == "Syntax error"); + { + INFO("LSL's, for SLua"); + CHECK(lsl.hasLine); + CHECK(lsl.line == 4); + CHECK(lsl.hasColumn); + CHECK(lsl.column == 2); + CHECK(lsl.message == "Syntax error"); + } } - template<> template<> - void alscriptmessages_object::test<11>() + TEST_CASE("alscriptmessages: Tailslide's line and the places it names count from one; its level as said", "[alscriptmessages]") { - set_test_name("Tailslide's line, as the grid's LSL compiler for Luau's VM says it: its line and the places its words name " - "count from one, which the viewer counts from zero; its level as it says it"); const std::string said = "Line 320: WARN: Declaration of `activeRequest' in this scope shadows previous declaration at (345, 1)"; for (const bool lua : { false, true }) { + CAPTURE(lua); const ALScriptMessages::Place place = ALScriptMessages::readDiagnostic(said, lua); - ensure("a line named", place.hasLine && !place.hasColumn); - ensure_equals("one off the line", place.line, 319); - ensure_equals("its level", place.level, std::string("WARN")); - ensure_equals("the words past the level", - place.message, std::string("Declaration of `activeRequest' in this scope shadows previous declaration at (345, 1)")); - ensure_equals("the place the words name", place.mentions.size(), size_t(1)); + { + INFO("a line named"); + REQUIRE(place.hasLine); + REQUIRE_FALSE(place.hasColumn); + } + UNSCOPED_INFO("one off the line"); + REQUIRE(place.line == 319); + REQUIRE(place.level == std::string("WARN")); + UNSCOPED_INFO("the words past the level"); + REQUIRE(place.message == std::string("Declaration of `activeRequest' in this scope shadows previous declaration at (345, 1)")); + UNSCOPED_INFO("the place the words name"); + REQUIRE(place.mentions.size() == size_t(1)); const ALScriptMessages::Mention& named = place.mentions.front(); - ensure_equals("where it stands", place.message.substr(named.at, named.length), std::string("(345, 1)")); - ensure("one off each", named.line == 344 && named.column == 0); + UNSCOPED_INFO("where it stands"); + REQUIRE(place.message.substr(named.at, named.length) == std::string("(345, 1)")); + { + INFO("one off each"); + REQUIRE(named.line == 344); + REQUIRE(named.column == 0); + } } const ALScriptMessages::Place error = ALScriptMessages::readDiagnostic("Line 1: ERROR: Duplicate declaration of `x'; previously " "declared at (1, 9). And (2, 3).", false); - ensure("an error on the first line", error.hasLine && error.line == 0 && error.level == "ERROR"); - ensure_equals("each place named", error.mentions.size(), size_t(2)); - ensure("in order", error.mentions[0].line == 0 && error.mentions[0].column == 8 && error.mentions[1].line == 1 && - error.mentions[1].column == 2); + { + INFO("an error on the first line"); + CHECK(error.hasLine); + CHECK(error.line == 0); + CHECK(error.level == "ERROR"); + } + UNSCOPED_INFO("each place named"); + REQUIRE(error.mentions.size() == size_t(2)); + { + INFO("in order"); + CHECK(error.mentions[0].line == 0); + CHECK(error.mentions[0].column == 8); + CHECK(error.mentions[1].line == 1); + CHECK(error.mentions[1].column == 2); + } // Words with a place in them, in LSL's own form, are LSL's. - ensure("no Tailslide in LSL's", ALScriptMessages::readDiagnostic("(4, 2) : ERROR : at (1, 1)", false).mentions.empty()); + UNSCOPED_INFO("no Tailslide in LSL's"); + REQUIRE(ALScriptMessages::readDiagnostic("(4, 2) : ERROR : at (1, 1)", false).mentions.empty()); } } diff --git a/indra/alscript/tests/alscriptnameindex_test.cpp b/indra/alscript/tests/alscriptnameindex_test.cpp index 0b71961279..7298a1aba7 100644 --- a/indra/alscript/tests/alscriptnameindex_test.cpp +++ b/indra/alscript/tests/alscriptnameindex_test.cpp @@ -26,11 +26,11 @@ #include "../core/alscriptnameindex.h" -#include "../test/lltut.h" +#include #include -namespace tut +namespace { struct alscriptnameindex_data { @@ -42,56 +42,78 @@ namespace tut } }; - typedef test_group alscriptnameindex_group; - typedef alscriptnameindex_group::object alscriptnameindex_object; - alscriptnameindex_group alscriptnameindex_instance("alscriptnameindex"); - - template<> template<> - void alscriptnameindex_object::test<1>() + TEST_CASE_METHOD(alscriptnameindex_data, "alscriptnameindex: once built an item's changes move what its names give and the generation", "[alscriptnameindex]") { - set_test_name("built once; an item renamed, added, taken away or moved out of reach changes what its names give, and the generation with it"); ALScriptNameIndex index; - ensure("not built, nothing given", !index.built() && index.named("lib").empty()); + { + INFO("not built, nothing given"); + CHECK_FALSE(index.built()); + REQUIRE(index.named("lib").empty()); + } index.changed(id(1), std::string("lib")); - ensure("nothing kept before it is built", index.named("lib").empty()); + UNSCOPED_INFO("nothing kept before it is built"); + REQUIRE(index.named("lib").empty()); index.build({ { id(1), "lib" }, { id(2), "lib" }, { id(3), "door" } }); - ensure("two of a name, in order", index.named("lib") == std::vector({ id(1), id(2) }) && index.size() == 3); + { + INFO("two of a name, in order"); + CHECK(index.named("lib") == std::vector({ id(1), id(2) })); + REQUIRE(index.size() == 3); + } U32 generation = index.generation(); index.changed(id(3), std::string("door")); - ensure("the same said again: nothing moves", index.generation() == generation); + UNSCOPED_INFO("the same said again: nothing moves"); + CHECK(index.generation() == generation); index.changed(id(9), std::nullopt); - ensure("an item never listed gone: nothing moves", index.generation() == generation); + UNSCOPED_INFO("an item never listed gone: nothing moves"); + CHECK(index.generation() == generation); index.changed(id(2), std::string("util")); - ensure("renamed: under its new name only", index.named("lib") == std::vector({ id(1) }) && index.named("util") == std::vector({ id(2) })); - ensure("and the generation moved", index.generation() != generation); + { + INFO("renamed: under its new name only"); + CHECK(index.named("lib") == std::vector({ id(1) })); + CHECK(index.named("util") == std::vector({ id(2) })); + } + UNSCOPED_INFO("and the generation moved"); + CHECK(index.generation() != generation); generation = index.generation(); index.changed(id(4), std::string("lib")); - ensure("added, after the rest", index.named("lib") == std::vector({ id(1), id(4) })); + UNSCOPED_INFO("added, after the rest"); + CHECK(index.named("lib") == std::vector({ id(1), id(4) })); index.changed(id(1), std::nullopt); - ensure("taken away, or into the trash", index.named("lib") == std::vector({ id(4) }) && index.size() == 3); + { + INFO("taken away, or into the trash"); + CHECK(index.named("lib") == std::vector({ id(4) })); + REQUIRE(index.size() == 3); + } index.changed(id(3), std::nullopt); - ensure("a name with nothing left gives nothing", index.named("door").empty()); + UNSCOPED_INFO("a name with nothing left gives nothing"); + REQUIRE(index.named("door").empty()); generation = index.generation(); index.forget(); - ensure("forgotten: built again at the next question", !index.built() && index.generation() != generation); + { + INFO("forgotten: built again at the next question"); + CHECK_FALSE(index.built()); + CHECK(index.generation() != generation); + } } - template<> template<> - void alscriptnameindex_object::test<2>() + TEST_CASE_METHOD(alscriptnameindex_data, "alscriptnameindex: forEach visits every listed item once with its current name", "[alscriptnameindex]") { - set_test_name("every item listed is visited once with its name, as it is now"); ALScriptNameIndex index; index.build({ { id(1), "lib" }, { id(2), "lib" }, { id(3), "door" } }); index.changed(id(2), std::string("util")); index.changed(id(3), std::nullopt); std::map seen; - index.forEach([&seen](const LLUUID& item, const std::string& name) { ensure("once each", seen.emplace(item, name).second); }); - ensure_equals("what is listed", seen.size(), size_t(2)); - ensure("by its name now", seen[id(1)] == "lib" && seen[id(2)] == "util"); + index.forEach([&seen](const LLUUID& item, const std::string& name) { UNSCOPED_INFO("once each"); REQUIRE(seen.emplace(item, name).second); }); + REQUIRE(seen.size() == size_t(2)); + { + INFO("by its name now"); + CHECK(seen[id(1)] == "lib"); + CHECK(seen[id(2)] == "util"); + } } } diff --git a/indra/alscript/tests/alscriptoutlinepairs_test.cpp b/indra/alscript/tests/alscriptoutlinepairs_test.cpp index 85ef806058..c58402b3a7 100644 --- a/indra/alscript/tests/alscriptoutlinepairs_test.cpp +++ b/indra/alscript/tests/alscriptoutlinepairs_test.cpp @@ -26,9 +26,9 @@ #include "../core/alscriptoutlinepairs.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { struct alscriptoutlinepairs_data { @@ -49,29 +49,24 @@ namespace tut return entry; } }; - typedef test_group alscriptoutlinepairs_group; - typedef alscriptoutlinepairs_group::object alscriptoutlinepairs_object; - alscriptoutlinepairs_group alscriptoutlinepairs_instance("alscriptoutlinepairs"); - template<> template<> - void alscriptoutlinepairs_object::test<1>() + TEST_CASE_METHOD(alscriptoutlinepairs_data, "alscriptoutlinepairs: functions pair by name wherever they are; lone ones and globals do not", "[alscriptoutlinepairs]") { - set_test_name("functions paired by name, wherever each now is; one on a side alone, and a global, not paired"); typedef ALScriptSymbolKind K; const std::vector left = { symbol("count", K::Variable, 0, 0), symbol("a", K::Function, 1, 4), symbol("b", K::Function, 5, 9), symbol("gone", K::Function, 10, 12) }; const std::vector right = { symbol("count", K::Variable, 0, 0), symbol("b", K::Function, 1, 5), symbol("made", K::Function, 6, 7), symbol("a", K::Function, 8, 12) }; const std::vector pairs = ALScriptOutlinePairs::pair(left, right); - ensure_equals("two", pairs.size(), size_t(2)); - ensure("a with a, in the left's order", pairs[0] == Pair{ 1, 4, 8, 12 }); - ensure("b with b", pairs[1] == Pair{ 5, 9, 1, 5 }); + REQUIRE(pairs.size() == size_t(2)); + UNSCOPED_INFO("a with a, in the left's order"); + CHECK(pairs[0] == Pair{ 1, 4, 8, 12 }); + UNSCOPED_INFO("b with b"); + CHECK(pairs[1] == Pair{ 5, 9, 1, 5 }); } - template<> template<> - void alscriptoutlinepairs_object::test<2>() + TEST_CASE_METHOD(alscriptoutlinepairs_data, "alscriptoutlinepairs: events by state; inner functions by parent; repeats in order; kinds match", "[alscriptoutlinepairs]") { - set_test_name("an event by its state, a function by the function it is in; of a name twice, the first with the first; a function not an event of the name"); typedef ALScriptSymbolKind K; const std::vector left = { symbol("default", K::State, 0, 9), symbol("state_entry", K::Event, 1, 3, 1), symbol("touch_start", K::Event, 4, 8, 1), @@ -84,22 +79,32 @@ namespace tut symbol("inner", K::Function, 16, 18, 1), symbol("touch_start", K::Event, 20, 21), }; const std::vector pairs = ALScriptOutlinePairs::pair(left, right); - ensure_equals("six", pairs.size(), size_t(6)); - ensure("default with default", pairs[0] == Pair{ 0, 9, 5, 12 }); - ensure("its state_entry with its own, not running's", pairs[1] == Pair{ 1, 3, 6, 8 }); - ensure("running with running, and its state_entry", pairs[2] == Pair{ 10, 14, 0, 4 } && pairs[3] == Pair{ 11, 13, 1, 3 }); - ensure("the first helper with the first", pairs[4] == Pair{ 15, 20, 13, 14 }); - ensure("inner by the helper it is in, the first; the second helper with the second", pairs[5] == Pair{ 21, 22, 15, 19 }); + REQUIRE(pairs.size() == size_t(6)); + UNSCOPED_INFO("default with default"); + CHECK(pairs[0] == Pair{ 0, 9, 5, 12 }); + UNSCOPED_INFO("its state_entry with its own, not running's"); + CHECK(pairs[1] == Pair{ 1, 3, 6, 8 }); + { + INFO("running with running, and its state_entry"); + CHECK(pairs[2] == Pair{ 10, 14, 0, 4 }); + CHECK(pairs[3] == Pair{ 11, 13, 1, 3 }); + } + UNSCOPED_INFO("the first helper with the first"); + CHECK(pairs[4] == Pair{ 15, 20, 13, 14 }); + UNSCOPED_INFO("inner by the helper it is in, the first; the second helper with the second"); + CHECK(pairs[5] == Pair{ 21, 22, 15, 19 }); } - template<> template<> - void alscriptoutlinepairs_object::test<3>() + TEST_CASE_METHOD(alscriptoutlinepairs_data, "alscriptoutlinepairs: a span ending at the start of a line ends on the line before", "[alscriptoutlinepairs]") { - set_test_name("a span ending at the start of a line ends on the line before"); ALScriptOutlineEntry left = symbol("a", ALScriptSymbolKind::Function, 2, 6); ALScriptOutlineEntry right = symbol("a", ALScriptSymbolKind::Function, 3, 8); left.span.endColumn = 0; const std::vector pairs = ALScriptOutlinePairs::pair({ left }, { right }); - ensure("its last line the one before", pairs.size() == 1 && pairs[0] == Pair{ 2, 5, 3, 8 }); + { + INFO("its last line the one before"); + REQUIRE(pairs.size() == 1); + CHECK(pairs[0] == Pair{ 2, 5, 3, 8 }); + } } } diff --git a/indra/alscript/tests/alscriptsnapshot_test.cpp b/indra/alscript/tests/alscriptsnapshot_test.cpp index 445e7fd98a..9c3995c715 100644 --- a/indra/alscript/tests/alscriptsnapshot_test.cpp +++ b/indra/alscript/tests/alscriptsnapshot_test.cpp @@ -25,9 +25,9 @@ #include "../preprocessor/alscriptsnapshot.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { struct alscriptsnapshot_data { @@ -41,14 +41,8 @@ namespace tut } }; - typedef test_group alscriptsnapshot_group; - typedef alscriptsnapshot_group::object alscriptsnapshot_object; - alscriptsnapshot_group alscriptsnapshot_instance("alscriptsnapshot"); - - template<> template<> - void alscriptsnapshot_object::test<1>() + TEST_CASE_METHOD(alscriptsnapshot_data, "alscriptsnapshot: a run uses answers and reports misses and notes each unknown name once", "[alscriptsnapshot]") { - set_test_name("a run takes what was answered, says what was not found, and notes each name nobody looked up once"); ALScriptSnapshot snapshot; snapshot.options().fileName = "main.lsl"; ALPreprocessor::Include lib; @@ -59,19 +53,27 @@ namespace tut snapshot.answer(ask("gone.lsl"), ALPreprocessor::Found::No); const ALPreprocessor::Result made = snapshot.run( "#include \"lib.lsl\"\n#include \"gone.lsl\"\n#include \"new.lsl\"\n#include \"new.lsl\"\ndefault { state_entry() { helper(); } }\n"); - ensure("the answered one in", made.text.find("integer helper()") != std::string::npos); - ensure("the missing one said", std::any_of(made.problems.begin(), made.problems.end(), [](const ALScriptProblem& p) { return p.key == "PreprocIncludeNotFound"; })); + UNSCOPED_INFO("the answered one in"); + CHECK(made.text.find("integer helper()") != std::string::npos); + UNSCOPED_INFO("the missing one said"); + CHECK(std::any_of(made.problems.begin(), made.problems.end(), [](const ALScriptProblem& p) { return p.key == "PreprocIncludeNotFound"; })); const std::vector missed = snapshot.missed(); - ensure("the unknown one noted once", missed.size() == 1 && missed[0].name == "new.lsl"); - ensure("taken away by the asking", snapshot.missed().empty()); + { + INFO("the unknown one noted once"); + REQUIRE(missed.size() == 1); + CHECK(missed[0].name == "new.lsl"); + } + UNSCOPED_INFO("taken away by the asking"); + CHECK(snapshot.missed().empty()); } - template<> template<> - void alscriptsnapshot_object::test<2>() + TEST_CASE_METHOD(alscriptsnapshot_data, "alscriptsnapshot: keyOf is the name and who asks and whether it is a require", "[alscriptsnapshot]") { - set_test_name("a name's key is its name, who asks, and whether it is a require"); - ensure("the same", ALScriptSnapshot::keyOf(ask("a")) == ALScriptSnapshot::keyOf(ask("a"))); - ensure("another asking", ALScriptSnapshot::keyOf(ask("a", "disk:/x.lsl")) != ALScriptSnapshot::keyOf(ask("a"))); - ensure("a require", ALScriptSnapshot::keyOf(ask("a", std::string(), true)) != ALScriptSnapshot::keyOf(ask("a"))); + UNSCOPED_INFO("the same"); + CHECK(ALScriptSnapshot::keyOf(ask("a")) == ALScriptSnapshot::keyOf(ask("a"))); + UNSCOPED_INFO("another asking"); + CHECK(ALScriptSnapshot::keyOf(ask("a", "disk:/x.lsl")) != ALScriptSnapshot::keyOf(ask("a"))); + UNSCOPED_INFO("a require"); + CHECK(ALScriptSnapshot::keyOf(ask("a", std::string(), true)) != ALScriptSnapshot::keyOf(ask("a"))); } } diff --git a/indra/alscript/tests/alscriptstack_test.cpp b/indra/alscript/tests/alscriptstack_test.cpp index 74771fb7f5..7d57bef1c7 100644 --- a/indra/alscript/tests/alscriptstack_test.cpp +++ b/indra/alscript/tests/alscriptstack_test.cpp @@ -28,7 +28,7 @@ #include "../preprocessor/alpreprocessor.h" -#include "../test/lltut.h" +#include #include #include @@ -56,24 +56,14 @@ namespace } } -namespace tut +namespace { - struct alscriptstack_data - { - }; - - typedef test_group alscriptstack_group; - typedef alscriptstack_group::object alscriptstack_object; - alscriptstack_group alscriptstack_instance("alscriptstack"); - - template<> template<> - void alscriptstack_object::test<1>() + TEST_CASE("alscriptstack: the work runs on the asking thread and what it throws comes back", "[alscriptstack]") { - set_test_name("the work runs, on the thread that asked, and what it throws comes back"); const std::thread::id asking = std::this_thread::get_id(); std::thread::id ran; alScriptOnLargeStack([&]() { ran = std::this_thread::get_id(); }); - ensure("on the same thread", ran == asking); + CHECK(ran == asking); bool caught = false; try @@ -84,17 +74,17 @@ namespace tut { caught = std::string(e.what()) == "from below"; } - ensure("thrown again once back", caught); + UNSCOPED_INFO("thrown again once back"); + CHECK(caught); } - template<> template<> - void alscriptstack_object::test<2>() + TEST_CASE("alscriptstack: a pool thread with a half megabyte stack goes eight megabytes deep", "[alscriptstack]") { - set_test_name("a pool's thread, whose own stack is half a megabyte on a Mac, goes eight megabytes deep"); int got = -1; std::thread worker([&]() { alScriptOnLargeStack([&]() { got = descend(8000); }); }); worker.join(); - ensure_equals("all the way down and back", got, 8000); + UNSCOPED_INFO("all the way down and back"); + CHECK(got == 8000); // And the preprocessor as deep as its own bounds let a script go, // on such a thread. @@ -117,13 +107,15 @@ namespace tut }); }); preprocessing.join(); - ensure("expanded, within the bound", !result.overran && result.text.find("1;") != std::string::npos); + { + INFO("expanded, within the bound"); + CHECK_FALSE(result.overran); + CHECK(result.text.find("1;") != std::string::npos); + } } - template<> template<> - void alscriptstack_object::test<3>() + TEST_CASE("alscriptstack: the stack is kept between jobs and a nested job runs where it stands", "[alscriptstack]") { - set_test_name("the thread's stack kept from one job to the next, and a job begun inside another run on it where it stands"); int first = -1, second = -1, nested = -1; bool threw = false; std::thread worker([&]() { @@ -143,9 +135,13 @@ namespace tut }); }); worker.join(); - ensure_equals("the first all the way", first, 8000); - ensure("a throw between", threw); - ensure_equals("the second, on the same stack", second, 4000); - ensure_equals("and one inside it", nested, 3000); + UNSCOPED_INFO("the first all the way"); + CHECK(first == 8000); + UNSCOPED_INFO("a throw between"); + CHECK(threw); + UNSCOPED_INFO("the second, on the same stack"); + CHECK(second == 4000); + UNSCOPED_INFO("and one inside it"); + CHECK(nested == 3000); } } diff --git a/indra/alscript/tests/alscripttempfiles_test.cpp b/indra/alscript/tests/alscripttempfiles_test.cpp index eda560df85..d7b9090ad7 100644 --- a/indra/alscript/tests/alscripttempfiles_test.cpp +++ b/indra/alscript/tests/alscripttempfiles_test.cpp @@ -30,12 +30,12 @@ #include "llfile.h" #include "lluuid.h" -#include "../test/lltut.h" +#include #include #include -namespace tut +namespace { struct alscripttempfiles_data { @@ -67,48 +67,46 @@ namespace tut static bool there(const std::string& path) { return LLFile::isfile(path); } }; - typedef test_group alscripttempfiles_group; - typedef alscripttempfiles_group::object alscripttempfiles_object; - alscripttempfiles_group alscripttempfiles_instance("alscripttempfiles"); - - template<> template<> - void alscripttempfiles_object::test<1>() + TEST_CASE_METHOD(alscripttempfiles_data, "alscripttempfiles: a copy's name is the cleaned script name and id and language's extension", "[alscripttempfiles]") { - set_test_name("a copy's name: the script's without what a file system refuses, the id, the language's extension"); - ensure_equals("LSL", ALScriptTempFiles::nameFor("/tmp", "My Script", "abc", false), std::string("/tmp/sl_script_My Script_abc.lsl")); - ensure_equals("SLua, the name cleaned", ALScriptTempFiles::nameFor("/tmp/", "a/b:c?", "abc", true), + CHECK(ALScriptTempFiles::nameFor("/tmp", "My Script", "abc", false) == std::string("/tmp/sl_script_My Script_abc.lsl")); + UNSCOPED_INFO("SLua, the name cleaned"); + CHECK(ALScriptTempFiles::nameFor("/tmp/", "a/b:c?", "abc", true) == std::string("/tmp/sl_script_abc_abc.luau")); - ensure_equals("nothing left of the name", ALScriptTempFiles::nameFor("/tmp/", "<>*", "abc", false), std::string("/tmp/sl_script_abc.lsl")); - ensure("a copy", ALScriptTempFiles::isCopy("/tmp/sl_script_x_abc.lsl.log")); - ensure("a notecard's", ALScriptTempFiles::isCopy("/tmp/sl_notecard_n_abc.txt")); - ensure("not one", !ALScriptTempFiles::isCopy("/tmp/notes_sl_script_.txt")); + UNSCOPED_INFO("nothing left of the name"); + CHECK(ALScriptTempFiles::nameFor("/tmp/", "<>*", "abc", false) == std::string("/tmp/sl_script_abc.lsl")); + UNSCOPED_INFO("a copy"); + CHECK(ALScriptTempFiles::isCopy("/tmp/sl_script_x_abc.lsl.log")); + UNSCOPED_INFO("a notecard's"); + CHECK(ALScriptTempFiles::isCopy("/tmp/sl_notecard_n_abc.txt")); + UNSCOPED_INFO("not one"); + CHECK_FALSE(ALScriptTempFiles::isCopy("/tmp/notes_sl_script_.txt")); } - template<> template<> - void alscripttempfiles_object::test<2>() + TEST_CASE_METHOD(alscripttempfiles_data, "alscripttempfiles: two editors of one script share a copy that goes with the last of them", "[alscripttempfiles]") { - set_test_name("two editors of one script hold one copy, and it goes with the last of them, whichever that is"); ALScriptTempFiles files(lists, "one"); const std::string copy = ALScriptTempFiles::nameFor(temp, "s", "id", false); std::shared_ptr studio = files.claim(copy); std::shared_ptr window = files.claim(copy); - ensure("one hold", studio == window); + UNSCOPED_INFO("one hold"); + CHECK(studio == window); put(copy); studio.reset(); - ensure("the other still has it", there(copy)); + UNSCOPED_INFO("the other still has it"); + CHECK(there(copy)); window.reset(); - ensure("gone with the last", !there(copy)); + UNSCOPED_INFO("gone with the last"); + CHECK_FALSE(there(copy)); // Held again after it went: written again, and listed once. std::shared_ptr again = files.claim(copy); put(copy); - ensure("there", there(copy)); + CHECK(there(copy)); } - template<> template<> - void alscripttempfiles_object::test<3>() + TEST_CASE_METHOD(alscripttempfiles_data, "alscripttempfiles: a sweep takes what ended sessions left but not what a running one lists", "[alscripttempfiles]") { - set_test_name("a sweep takes what sessions that are over left, and leaves what a running one lists, and what is no copy"); // A session that crashed: its list, and its lock that nobody holds. const std::string left = ALScriptTempFiles::nameFor(temp, "left", "1", false); const std::string left_log = left + ".log"; @@ -130,33 +128,52 @@ namespace tut std::shared_ptr held = running.claim(shared); ALScriptTempFiles sweeper(lists, "now"); - ensure_equals("three went", sweeper.sweep(), size_t(3)); - ensure("the crashed one's copy", !there(left)); - ensure("and its log", !there(left_log)); - ensure("the older one's", !there(older)); - ensure("one a running session lists stays", there(shared)); - ensure("what is no copy stays", there(notes)); - ensure("the crashed one's list and lock gone", !there(lists + "crashed.list") && !there(lists + "crashed.lock")); - ensure("and the older one's list", !there(lists + "older.list")); - ensure("the running one's kept", there(lists + "running.list") && there(lists + "running.lock")); - - ensure_equals("swept again, nothing", sweeper.sweep(), size_t(0)); - ensure("the running one's still", there(shared)); + CHECK(sweeper.sweep() == size_t(3)); + UNSCOPED_INFO("the crashed one's copy"); + CHECK_FALSE(there(left)); + UNSCOPED_INFO("and its log"); + CHECK_FALSE(there(left_log)); + UNSCOPED_INFO("the older one's"); + CHECK_FALSE(there(older)); + UNSCOPED_INFO("one a running session lists stays"); + CHECK(there(shared)); + UNSCOPED_INFO("what is no copy stays"); + CHECK(there(notes)); + { + INFO("the crashed one's list and lock gone"); + CHECK_FALSE(there(lists + "crashed.list")); + CHECK_FALSE(there(lists + "crashed.lock")); + } + UNSCOPED_INFO("and the older one's list"); + CHECK_FALSE(there(lists + "older.list")); + { + INFO("the running one's kept"); + CHECK(there(lists + "running.list")); + CHECK(there(lists + "running.lock")); + } + + UNSCOPED_INFO("swept again, nothing"); + CHECK(sweeper.sweep() == size_t(0)); + UNSCOPED_INFO("the running one's still"); + CHECK(there(shared)); } - template<> template<> - void alscripttempfiles_object::test<4>() + TEST_CASE_METHOD(alscripttempfiles_data, "alscripttempfiles: a session that lets go of everything leaves no list or lock or copy", "[alscripttempfiles]") { - set_test_name("a session that ends letting go of everything leaves no list, no lock, and no copy"); const std::string copy = ALScriptTempFiles::nameFor(temp, "s", "id", false); { ALScriptTempFiles files(lists, "clean"); std::shared_ptr held = files.claim(copy); put(copy); - ensure("listed", there(lists + "clean.list") && there(lists + "clean.lock")); + { + INFO("listed"); + CHECK(there(lists + "clean.list")); + CHECK(there(lists + "clean.lock")); + } } - ensure("no copy", !there(copy)); - ensure("no list, no lock", !there(lists + "clean.list") && !there(lists + "clean.lock")); + CHECK_FALSE(there(copy)); + CHECK_FALSE(there(lists + "clean.list")); + CHECK_FALSE(there(lists + "clean.lock")); // One that let go of the store first, a hold outliving it: the // lock is kept until the hold goes. @@ -167,9 +184,16 @@ namespace tut put(copy); } ALScriptTempFiles sweeper(lists, "now"); - ensure_equals("still running, as far as a sweep can tell", sweeper.sweep(), size_t(0)); - ensure("its copy kept", there(copy)); + UNSCOPED_INFO("still running, as far as a sweep can tell"); + CHECK(sweeper.sweep() == size_t(0)); + UNSCOPED_INFO("its copy kept"); + CHECK(there(copy)); held.reset(); - ensure("gone with the hold", !there(copy) && !there(lists + "late.list") && !there(lists + "late.lock")); + { + INFO("gone with the hold"); + CHECK_FALSE(there(copy)); + CHECK_FALSE(there(lists + "late.list")); + CHECK_FALSE(there(lists + "late.lock")); + } } } diff --git a/indra/alscript/tests/alscripttextcache_test.cpp b/indra/alscript/tests/alscripttextcache_test.cpp index cbea4350a1..cd2e51db0a 100644 --- a/indra/alscript/tests/alscripttextcache_test.cpp +++ b/indra/alscript/tests/alscripttextcache_test.cpp @@ -26,72 +26,98 @@ #include "../preprocessor/alscripttextcache.h" -#include "../test/lltut.h" +#include -namespace tut +namespace { struct alscripttextcache_data { ALScriptTextCache cache; }; - typedef test_group alscripttextcache_group; - typedef alscripttextcache_group::object alscripttextcache_object; - alscripttextcache_group alscripttextcache_instance("alscripttextcache"); - - template<> template<> - void alscripttextcache_object::test<1>() + TEST_CASE_METHOD(alscripttextcache_data, "alscripttextcache: a text is taken only as its asset; a failure is kept until put or forgotten", "[alscripttextcache]") { - set_test_name("a text held by its identity and asset, taken only as that asset; a failure kept until put or forgotten"); const LLUUID one = LLUUID::generateNewID(), two = LLUUID::generateNewID(); std::string text; - ensure("nothing held", !cache.take("inventory:a", one, text) && !cache.holds("inventory:a")); + { + INFO("nothing held"); + CHECK_FALSE(cache.take("inventory:a", one, text)); + CHECK_FALSE(cache.holds("inventory:a")); + } cache.failed("inventory:a"); - ensure("failed", cache.hasFailed("inventory:a")); + CHECK(cache.hasFailed("inventory:a")); cache.put("inventory:a", one, "integer a;\n"); - ensure("put: no longer failed", !cache.hasFailed("inventory:a")); - ensure("taken as its asset", cache.take("inventory:a", one, text) && text == "integer a;\n"); - ensure("not as another", !cache.take("inventory:a", two, text)); - ensure("held whatever the asset", cache.held("inventory:a", text) && cache.paths() == std::vector{ "inventory:a" }); + UNSCOPED_INFO("put: no longer failed"); + CHECK_FALSE(cache.hasFailed("inventory:a")); + { + INFO("taken as its asset"); + CHECK(cache.take("inventory:a", one, text)); + CHECK(text == "integer a;\n"); + } + UNSCOPED_INFO("not as another"); + CHECK_FALSE(cache.take("inventory:a", two, text)); + { + INFO("held whatever the asset"); + CHECK(cache.held("inventory:a", text)); + CHECK(cache.paths() == std::vector{ "inventory:a" }); + } cache.put("inventory:a", two, "integer b;\n"); - ensure_equals("put again: the bytes counted once", cache.bytes(), size_t(11)); + UNSCOPED_INFO("put again: the bytes counted once"); + CHECK(cache.bytes() == size_t(11)); cache.failed("inventory:b"); cache.forgetFailure("inventory:b"); - ensure("forgotten", !cache.hasFailed("inventory:b")); + CHECK_FALSE(cache.hasFailed("inventory:b")); } - template<> template<> - void alscripttextcache_object::test<2>() + TEST_CASE_METHOD(alscripttextcache_data, "alscripttextcache: past the budget the least lately read goes first; the newest always stays", "[alscripttextcache]") { - set_test_name("past the budget the least lately read goes first, and the newest stays whatever its size"); const LLUUID asset = LLUUID::generateNewID(); const std::string third(ALScriptTextCache::BUDGET / 3 + 1, 'x'); std::string text; cache.put("a", asset, third); cache.put("b", asset, third); - ensure("a read, so kept longer", cache.take("a", asset, text)); + UNSCOPED_INFO("a read, so kept longer"); + CHECK(cache.take("a", asset, text)); cache.put("c", asset, third); - ensure("b, least lately read, gone", !cache.holds("b") && cache.holds("a") && cache.holds("c")); + { + INFO("b, least lately read, gone"); + CHECK_FALSE(cache.holds("b")); + CHECK(cache.holds("a")); + CHECK(cache.holds("c")); + } cache.put("huge", asset, std::string(ALScriptTextCache::BUDGET + 1, 'y')); - ensure("the newest kept alone", cache.holds("huge") && cache.paths().size() == 1); + { + INFO("the newest kept alone"); + CHECK(cache.holds("huge")); + REQUIRE(cache.paths().size() == 1); + } } - template<> template<> - void alscripttextcache_object::test<3>() + TEST_CASE_METHOD(alscripttextcache_data, "alscripttextcache: a hundred texts past the budget trim to within it; failures are bounded", "[alscripttextcache]") { - set_test_name("a hundred texts past the budget: the least lately read go, in one trim, until what is held is within it; failures bounded"); const LLUUID asset = LLUUID::generateNewID(); const std::string fiftieth(ALScriptTextCache::BUDGET / 50, 'x'); for (int i = 0; i < 100; ++i) { cache.put("t" + std::to_string(i), asset, fiftieth); } - ensure("within the budget", cache.bytes() <= ALScriptTextCache::BUDGET); - ensure("the newest kept, the oldest gone", cache.holds("t99") && cache.holds("t60") && !cache.holds("t0") && !cache.holds("t40")); + UNSCOPED_INFO("within the budget"); + CHECK(cache.bytes() <= ALScriptTextCache::BUDGET); + { + INFO("the newest kept, the oldest gone"); + CHECK(cache.holds("t99")); + CHECK(cache.holds("t60")); + CHECK_FALSE(cache.holds("t0")); + CHECK_FALSE(cache.holds("t40")); + } for (size_t i = 0; i <= ALScriptTextCache::FAILURES_KEPT; ++i) { cache.failed("f" + std::to_string(i)); } - ensure("past the bound, the failures before it let go of", !cache.hasFailed("f0") && cache.hasFailed("f" + std::to_string(ALScriptTextCache::FAILURES_KEPT))); + { + INFO("past the bound, the failures before it let go of"); + CHECK_FALSE(cache.hasFailed("f0")); + CHECK(cache.hasFailed("f" + std::to_string(ALScriptTextCache::FAILURES_KEPT))); + } } } diff --git a/indra/alscript/tests/alscriptweight_test.cpp b/indra/alscript/tests/alscriptweight_test.cpp index 2dd4ecc3b2..af1b88a4c4 100644 --- a/indra/alscript/tests/alscriptweight_test.cpp +++ b/indra/alscript/tests/alscriptweight_test.cpp @@ -31,13 +31,13 @@ #include "../luau/alluausharedstart.h" #include "../preprocessor/alsourcemap.h" -#include "../test/lltut.h" +#include #include #include #include -namespace tut +namespace { struct alscriptweight_data { @@ -122,7 +122,7 @@ namespace tut // A shared start's edits made from the end back, as an editor's // replaceAll does. - static std::string applied(const std::string& script, const ALLuauSharedStart::Rewrite& rewrite) + static std::string require_applied(const std::string& script, const ALLuauSharedStart::Rewrite& rewrite) { std::vector lines; for (size_t from = 0; from <= script.size();) @@ -133,7 +133,8 @@ namespace tut } for (auto edit = rewrite.edits.rbegin(); edit != rewrite.edits.rend(); ++edit) { - ensure("one line each", edit->line == edit->endLine); + UNSCOPED_INFO("one line each"); + REQUIRE(edit->line == edit->endLine); std::string& line = lines[static_cast(edit->line)]; line.replace(static_cast(edit->column), static_cast(edit->endColumn - edit->column), edit->text); } @@ -156,15 +157,11 @@ namespace tut return out; } }; - typedef test_group alscriptweight_group; - typedef alscriptweight_group::object alscriptweight_object; - alscriptweight_group alscriptweight_instance("ALScriptWeight"); // SLua weighs what the server charges, the bytecode; each function is // its prototype, where its lines are, and each line the instructions // that came of it. - template<> template<> - void alscriptweight_object::test<1>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: SLua weighs the bytecode the server charges; each function a part", "[ALScriptWeight]") { const std::string script = "local function greet(name: string)\n" " ll.OwnerSay(\"hello \" .. name)\n" @@ -181,42 +178,61 @@ namespace tut "nothing()\n" "greet(tostring(count(10)))\n"; const ALScriptWeight weight = ALScriptWeigh::slua(script); - ensure("compiled: " + weight.error, weight.compiled); - ensure_equals("against SLua's limit", weight.limit, size_t(131072)); - ensure("not an estimate", !weight.estimate); - ensure("something, and not much", weight.total > 50 && weight.total < 2000); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); + UNSCOPED_INFO("against SLua's limit"); + CHECK(weight.limit == size_t(131072)); + CHECK_FALSE(weight.estimate); + CHECK(weight.total > 50); + CHECK(weight.total < 2000); const ALScriptWeight::Part* greet = named(weight, "greet"); const ALScriptWeight::Part* count = named(weight, "count"); - ensure("each function a part:" + listed(weight), greet && count); - ensure_equals("where it is declared", greet->line, 0); - ensure_equals("and where it ends", count->line, 4); - ensure("to its last line", count->endLine >= 8); + { + INFO("each function a part:" + listed(weight)); + REQUIRE(greet); + REQUIRE(count); + } + UNSCOPED_INFO("where it is declared"); + CHECK(greet->line == 0); + UNSCOPED_INFO("and where it ends"); + CHECK(count->line == 4); + UNSCOPED_INFO("to its last line"); + CHECK(count->endLine >= 8); const ALScriptWeight::Part* nothing = named(weight, "nothing"); - ensure("a function that does nothing weighs least:" + listed(weight), - nothing && nothing->bytes < greet->bytes && nothing->bytes < count->bytes); - ensure("the script itself, and its strings", named(weight, "script") && named(weight, "strings")); + { + INFO("a function that does nothing weighs least:" + listed(weight)); + REQUIRE(nothing); + CHECK(nothing->bytes < greet->bytes); + CHECK(nothing->bytes < count->bytes); + } + CHECK(named(weight, "script")); + CHECK(named(weight, "strings")); const size_t parts = std::accumulate(weight.parts.begin(), weight.parts.end(), size_t(0), [](size_t sum, const ALScriptWeight::Part& p) { return sum + p.bytes; }); - ensure("the parts are nearly the whole: " + std::to_string(parts) + " of " + std::to_string(weight.total), - parts <= weight.total && weight.total - parts < 16); + { + INFO("the parts are nearly the whole: " << parts << " of " << weight.total); + CHECK(parts <= weight.total); + CHECK(weight.total - parts < 16); + } bool body = false; for (const ALScriptWeight::Line& line : weight.lines) { body = body || (line.line == 7 && line.bytes > 0); } - ensure("the loop's body has bytes of its own", body); + UNSCOPED_INFO("the loop's body has bytes of its own"); + CHECK(body); } // A long string is weight as much as code is; and a script that does // not compile says so. - template<> template<> - void alscriptweight_object::test<2>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: a long string weighs as code does; a script that fails to compile says so", "[ALScriptWeight]") { const std::string words(2000, 'x'); const ALScriptWeight heavy = ALScriptWeigh::slua("local s = \"" + words + "\"\nprint(s)\n"); const ALScriptWeight light = ALScriptWeigh::slua("local s = \"x\"\nprint(s)\n"); - ensure("both compiled", heavy.compiled && light.compiled); - ensure("the string weighs", heavy.total >= light.total + 2000); + CHECK(heavy.compiled); + CHECK(light.compiled); + CHECK(heavy.total >= light.total + 2000); // Heavy, it is a part of its own, and no longer the table of // strings'. const ALScriptWeight::Part* constant = nullptr; @@ -226,10 +242,24 @@ namespace tut } // At the line that loads it: a local never set again is folded into // where it is used, print(s), as -O1 compiles it. - ensure("a part of its own:" + listed(heavy), constant && constant->bytes >= 2000 && constant->line == 1); - ensure("named by its start: " + (constant ? constant->name : std::string()), constant && constant->name.rfind("\"xxxx", 0) == 0); - ensure("not the strings' too", named(heavy, "strings") && named(heavy, "strings")->bytes < 100); - ensure("a light one stays the strings'", std::none_of(light.parts.begin(), light.parts.end(), [](const ALScriptWeight::Part& one) { + { + INFO("a part of its own:" + listed(heavy)); + REQUIRE(constant); + CHECK(constant->bytes >= 2000); + CHECK(constant->line == 1); + } + { + INFO("named by its start: " + (constant ? constant->name : std::string())); + REQUIRE(constant); + CHECK(constant->name.rfind("\"xxxx", 0) == 0); + } + { + INFO("not the strings' too"); + REQUIRE(named(heavy, "strings")); + CHECK(named(heavy, "strings")->bytes < 100); + } + UNSCOPED_INFO("a light one stays the strings'"); + CHECK(std::none_of(light.parts.begin(), light.parts.end(), [](const ALScriptWeight::Part& one) { return one.kind == ALScriptWeight::Part::Kind::Constant && one.name != "strings"; })); // And for the LSL targets: LSO's in its code, Mono's in its strings, @@ -246,8 +276,15 @@ namespace tut sum += part.bytes; } const std::string target = ALScriptWeight::nameOf(one.target); - ensure(target + ": a part of its own:" + listed(one), one.compiled && found && found->bytes >= 2000 && found->line == 0); - ensure(target + ": counted once", sum <= one.total); + { + INFO(target + ": a part of its own:" + listed(one)); + REQUIRE(one.compiled); + REQUIRE(found); + REQUIRE(found->bytes >= 2000); + REQUIRE(found->line == 0); + } + UNSCOPED_INFO(target + ": counted once"); + REQUIRE(sum <= one.total); } // A table's values written in it, which no instruction loads -- the // template carries them: heavy ones are parts of their own too, at @@ -259,7 +296,8 @@ namespace tut { values += one.kind == ALScriptWeight::Part::Kind::Constant && one.name != "strings" && one.bytes >= 300 && one.line >= 0 && one.line <= 3; } - ensure_equals("each value a part of its own, at the table:" + listed(table), values, size_t(2)); + UNSCOPED_INFO("each value a part of its own, at the table:" + listed(table)); + CHECK(values == size_t(2)); // Named by whole characters: the cut, 32 bytes in, falls inside // one, or just after one. const auto whole = [](const std::string& name) { @@ -296,20 +334,26 @@ namespace tut { found = one.kind == ALScriptWeight::Part::Kind::Constant && one.name != "strings" ? &one : found; } - ensure("a wide one named: " + listed(wide), found && whole(found->name) && found->name.find("\xE2\x80\xA6") != std::string::npos); + { + INFO("a wide one named: " + listed(wide)); + REQUIRE(found); + REQUIRE(whole(found->name)); + REQUIRE(found->name.find("\xE2\x80\xA6") != std::string::npos); + } } const ALScriptWeight broken = ALScriptWeigh::slua("local function (\n"); - ensure("a script that does not compile", !broken.compiled && !broken.error.empty()); + CHECK_FALSE(broken.compiled); + CHECK_FALSE(broken.error.empty()); } // LSO weighs its image to the top of its heap: the registers, the // globals, the functions, the states with their handlers, and the heap; // each function, state, handler and global a part, where it is. - template<> template<> - void alscriptweight_object::test<3>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: LSO weighs its image to the top of its heap", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "integer gCount = 0;\n" "string gName = \"a name a little longer than a word\";\n" "integer twice(integer n)\n" @@ -328,29 +372,43 @@ namespace tut " }\n" "}\n"; const ALScriptWeight weight = ALScriptWeigh::lso(script); - ensure("compiled: " + weight.error, weight.compiled); - ensure_equals("against LSO's limit", weight.limit, size_t(16384)); - ensure("well under it", weight.total > 0 && weight.total < 4096); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); + UNSCOPED_INFO("against LSO's limit"); + CHECK(weight.limit == size_t(16384)); + CHECK(weight.total > 0); + CHECK(weight.total < 4096); const ALScriptWeight::Part* twice = named(weight, "twice"); const ALScriptWeight::Part* state = named(weight, "default"); const ALScriptWeight::Part* touch = named(weight, "touch_start"); - ensure("each a part:" + listed(weight), twice && state && touch && named(weight, "state_entry") && named(weight, "registers")); - ensure_equals("a function where it is", twice->line, 2); - ensure_equals("a handler in its state", touch->within, std::string("default")); + { + INFO("each a part:" + listed(weight)); + REQUIRE(twice); + REQUIRE(state); + REQUIRE(touch); + CHECK(named(weight, "state_entry")); + CHECK(named(weight, "registers")); + } + UNSCOPED_INFO("a function where it is"); + CHECK(twice->line == 2); + UNSCOPED_INFO("a handler in its state"); + CHECK(touch->within == std::string("default")); // Where its handlers begin and a jump table entry for each; each // handler, from where it begins to where it ends, its own part. - ensure_equals("a state is its table, its handlers apart:" + listed(weight), state->bytes, size_t(5 + 8 * 2)); + UNSCOPED_INFO("a state is its table, its handlers apart:" + listed(weight)); + CHECK(state->bytes == size_t(5 + 8 * 2)); const ALScriptWeight::Part* count = named(weight, "gCount"); const ALScriptWeight::Part* name = named(weight, "gName"); - ensure("the globals too", count && name); - ensure("a string global weighs its string", name->bytes > count->bytes); + REQUIRE(count); + REQUIRE(name); + UNSCOPED_INFO("a string global weighs its string"); + CHECK(name->bytes > count->bytes); } - // What does not fit in 16 KB says so. - template<> template<> - void alscriptweight_object::test<4>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: what does not fit in 16 KB says so", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); std::string script = "list gWords = ["; for (int i = 0; i < 600; ++i) { @@ -358,37 +416,25 @@ namespace tut } script += "];\ndefault\n{\n state_entry()\n {\n llOwnerSay(llList2String(gWords, 0));\n }\n}\n"; const ALScriptWeight weight = ALScriptWeigh::lso(script); - ensure("does not fit: " + weight.error + " " + std::to_string(weight.total), !weight.compiled && !weight.error.empty()); - ensure("and weighs more than there is", weight.total > weight.limit); - const ALScriptWeight broken = ALScriptWeigh::lso("default { state_entry() { integer x = ; } }\n"); - ensure("a script that does not parse", !broken.compiled && !broken.error.empty()); - } - - // A long list parses on every platform: each element nests the parse two - // states deeper, and the parser's limit is the same everywhere. - template<> template<> - void alscriptweight_object::test<10>() - { - ensure("builtins: " + error, lslLoaded); - std::string script = "list gNumbers = ["; - for (int i = 0; i < 3000; ++i) { - script += (i ? ", " : "") + std::to_string(i); + INFO("does not fit: " << weight.error << " " << weight.total); + CHECK_FALSE(weight.compiled); + CHECK_FALSE(weight.error.empty()); } - script += "];\ndefault\n{\n state_entry()\n {\n llOwnerSay((string)llGetListLength(gNumbers));\n }\n}\n"; - lsl.check(script); - ensure("it parses", lsl.parsed()); - const ALScriptWeight weight = ALScriptWeigh::lso(script); - ensure("and is weighed: " + weight.error, weight.total > 0); + UNSCOPED_INFO("and weighs more than there is"); + CHECK(weight.total > weight.limit); + const ALScriptWeight broken = ALScriptWeigh::lso("default { state_entry() { integer x = ; } }\n"); + CHECK_FALSE(broken.compiled); + CHECK_FALSE(broken.error.empty()); } // Mono is an estimate: the IL sized by the opcode table with what it // declares, over what an assembly costs; each function and handler a // part where it is, and the parts are the whole. - template<> template<> - void alscriptweight_object::test<5>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: Mono is an estimate: the IL sized by the opcode table with what it declares", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "integer gCount = 0;\n" "integer twice(integer n)\n" "{\n" @@ -406,30 +452,43 @@ namespace tut " }\n" "}\n"; const ALScriptWeight weight = ALScriptWeigh::mono(script); - ensure("compiled: " + weight.error, weight.compiled); - ensure("an estimate", weight.estimate); - ensure_equals("against Mono's limit", weight.limit, size_t(65536)); - ensure("over what an assembly costs, and under the limit", weight.total > ALScriptWeigh::MONO_BASE_BYTES && weight.total < 8192); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); + CHECK(weight.estimate); + UNSCOPED_INFO("against Mono's limit"); + CHECK(weight.limit == size_t(65536)); + CHECK(weight.total > ALScriptWeigh::MONO_BASE_BYTES); + CHECK(weight.total < 8192); const ALScriptWeight::Part* twice = named(weight, "twice"); const ALScriptWeight::Part* touch = named(weight, "touch_start"); const ALScriptWeight::Part* entry = named(weight, "state_entry"); - ensure("each a part:" + listed(weight), twice && touch && entry && named(weight, "globals") && named(weight, "assembly")); - ensure_equals("a function where it is", twice->line, 1); - ensure_equals("a handler in its state", touch->within, std::string("default")); - ensure_equals("where it is", touch->line, 11); - ensure("a long string weighs:" + listed(weight), touch->bytes > entry->bytes); + { + INFO("each a part:" + listed(weight)); + REQUIRE(twice); + REQUIRE(touch); + REQUIRE(entry); + CHECK(named(weight, "globals")); + CHECK(named(weight, "assembly")); + } + UNSCOPED_INFO("a function where it is"); + CHECK(twice->line == 1); + UNSCOPED_INFO("a handler in its state"); + CHECK(touch->within == std::string("default")); + CHECK(touch->line == 11); + UNSCOPED_INFO("a long string weighs:" + listed(weight)); + CHECK(touch->bytes > entry->bytes); const size_t parts = std::accumulate(weight.parts.begin(), weight.parts.end(), size_t(0), [](size_t sum, const ALScriptWeight::Part& p) { return sum + p.bytes; }); - ensure_equals("the parts are the whole", parts, weight.total); + CHECK(parts == weight.total); } // LSL on Luau: the fork's own compiler, the asset as the server makes // it, the bytecode read back as SLua's is; and again with its lines, // for what each comes to. - template<> template<> - void alscriptweight_object::test<6>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: LSL on Luau: the asset as the server makes it with the fork's compiler", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::string script = "integer gCount = 0;\n" "integer twice(integer n)\n" "{\n" @@ -447,72 +506,119 @@ namespace tut " }\n" "}\n"; const ALScriptWeight weight = ALScriptWeigh::lslLuau(script); - ensure("compiled: " + weight.error + listed(weight), weight.compiled); - ensure_equals("against Luau's limit", weight.limit, size_t(131072)); - ensure("not an estimate", !weight.estimate); - ensure("something, and not much: " + std::to_string(weight.total) + listed(weight), weight.total > 50 && weight.total < 4096); + UNSCOPED_INFO("compiled: " + weight.error + listed(weight)); + CHECK(weight.compiled); + UNSCOPED_INFO("against Luau's limit"); + CHECK(weight.limit == size_t(131072)); + CHECK_FALSE(weight.estimate); + { + INFO("parts:" + listed(weight)); + CHECK(weight.total > 50); + CHECK(weight.total < 4096); + } const ALScriptWeight::Part* twice = named(weight, "twice"); - ensure("a function a part:" + listed(weight), twice != nullptr); - ensure("where it is: " + std::to_string(twice->line), twice->line == 1); + UNSCOPED_INFO("a function a part:" + listed(weight)); + REQUIRE(twice != nullptr); + CHECK(twice->line == 1); const ALScriptWeight::Part* touch = named(weight, "touch_start"); - ensure("a handler in its state:" + listed(weight), touch && touch->within == "default" && touch->kind == ALScriptWeight::Part::Kind::Handler); - ensure_equals("where it is", touch->line, 11); + { + INFO("a handler in its state:" + listed(weight)); + REQUIRE(touch); + CHECK(touch->within == "default"); + CHECK(touch->kind == ALScriptWeight::Part::Kind::Handler); + } + CHECK(touch->line == 11); // Its lines, read off the compiler's lined bytecode, which the // server's is not: the total stays what the server charges. - ensure("each line weighed:" + lined(weight), at(weight, 9) > 0 && at(weight, 13) > 0 && at(weight, 3) > 0); - ensure("the handler's long line its own:" + lined(weight), at(weight, 13) > at(weight, 9)); - ensure("nothing past the script's lines:" + lined(weight), within(weight, 16, 1000) == 0); + { + INFO("each line weighed:" + lined(weight)); + CHECK(at(weight, 9) > 0); + CHECK(at(weight, 13) > 0); + CHECK(at(weight, 3) > 0); + } + UNSCOPED_INFO("the handler's long line its own:" + lined(weight)); + CHECK(at(weight, 13) > at(weight, 9)); + UNSCOPED_INFO("nothing past the script's lines:" + lined(weight)); + CHECK(within(weight, 16, 1000) == 0); const ALScriptWeight broken = ALScriptWeigh::lslLuau("default { state_entry() { integer x = ; } }\n"); - ensure("a script that does not compile", !broken.compiled && !broken.error.empty()); + CHECK_FALSE(broken.compiled); + CHECK_FALSE(broken.error.empty()); } // LSO by line: what the compiler wrote while it stood at each // statement and expression, a function's missing return at its closing // brace, and a global's value on its line. A function's lines are its // code, which is the function less its header. - template<> template<> - void alscriptweight_object::test<7>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: LSO by line: what the compiler wrote at each statement and expression", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const ALScriptWeight weight = ALScriptWeigh::lso(byLine()); - ensure("compiled: " + weight.error, weight.compiled); - ensure("by line:" + lined(weight), !weight.lines.empty()); - ensure("the loop's body" + lined(weight), at(weight, 13) > 0); - ensure("the loop itself: its test, its step and its jump back" + lined(weight), at(weight, 11) > 0); - ensure("a long string weighs more than a short one" + lined(weight), at(weight, 15) > at(weight, 16) + 40); - ensure("the return it does not write, at its closing brace" + lined(weight), at(weight, 17) > 0); - ensure("a function's return" + lined(weight), at(weight, 4) > 0); - ensure("a global string's value" + lined(weight), at(weight, 1) >= 34); - ensure("a brace that makes nothing" + lined(weight), at(weight, 7) == 0 && at(weight, 3) == 0); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); + CHECK_FALSE(weight.lines.empty()); + UNSCOPED_INFO("the loop's body" + lined(weight)); + CHECK(at(weight, 13) > 0); + UNSCOPED_INFO("the loop itself: its test, its step and its jump back" + lined(weight)); + CHECK(at(weight, 11) > 0); + UNSCOPED_INFO("a long string weighs more than a short one" + lined(weight)); + CHECK(at(weight, 15) > at(weight, 16) + 40); + UNSCOPED_INFO("the return it does not write, at its closing brace" + lined(weight)); + CHECK(at(weight, 17) > 0); + UNSCOPED_INFO("a function's return" + lined(weight)); + CHECK(at(weight, 4) > 0); + UNSCOPED_INFO("a global string's value" + lined(weight)); + CHECK(at(weight, 1) >= 34); + { + INFO("a brace that makes nothing" + lined(weight)); + CHECK(at(weight, 7) == 0); + CHECK(at(weight, 3) == 0); + } const ALScriptWeight::Part* entry = named(weight, "state_entry"); const ALScriptWeight::Part* twice = named(weight, "twice"); - ensure("both parts:" + listed(weight), entry && twice); - ensure_equals("a handler's lines are all of it but its header, where its code begins", entry->bytes - within(weight, entry->line, entry->endLine), + { + INFO("both parts:" + listed(weight)); + REQUIRE(entry); + REQUIRE(twice); + } + UNSCOPED_INFO("a handler's lines are all of it but its header, where its code begins"); + CHECK(entry->bytes - within(weight, entry->line, entry->endLine) == size_t(5)); - ensure_equals("a function's lines are all of it but its header: where its code begins, its type and its parameter's", - twice->bytes - within(weight, twice->line, twice->endLine), size_t(9)); + UNSCOPED_INFO("a function's lines are all of it but its header: where its code begins, its type and its parameter's"); + CHECK(twice->bytes - within(weight, twice->line, twice->endLine) == size_t(9)); } // Mono by line, the same way: each instruction, with its string, is // the line it was written for; a global's initialiser is its line's. - template<> template<> - void alscriptweight_object::test<8>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: Mono by line: each instruction with its string is its line's", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const ALScriptWeight weight = ALScriptWeigh::mono(byLine()); - ensure("compiled: " + weight.error, weight.compiled); - ensure("by line:" + lined(weight), !weight.lines.empty()); - ensure("the loop's body" + lined(weight), at(weight, 13) > 0); - ensure("the loop itself" + lined(weight), at(weight, 11) > 0); - ensure("a long string weighs more than a short one" + lined(weight), at(weight, 15) > at(weight, 16) + 40); - ensure("the return it does not write, at its closing brace" + lined(weight), at(weight, 17) > 0); - ensure("a global string's initialiser" + lined(weight), at(weight, 1) >= 34); - ensure("a brace that makes nothing" + lined(weight), at(weight, 7) == 0 && at(weight, 3) == 0); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); + CHECK_FALSE(weight.lines.empty()); + UNSCOPED_INFO("the loop's body" + lined(weight)); + CHECK(at(weight, 13) > 0); + UNSCOPED_INFO("the loop itself" + lined(weight)); + CHECK(at(weight, 11) > 0); + UNSCOPED_INFO("a long string weighs more than a short one" + lined(weight)); + CHECK(at(weight, 15) > at(weight, 16) + 40); + UNSCOPED_INFO("the return it does not write, at its closing brace" + lined(weight)); + CHECK(at(weight, 17) > 0); + UNSCOPED_INFO("a global string's initialiser" + lined(weight)); + CHECK(at(weight, 1) >= 34); + { + INFO("a brace that makes nothing" + lined(weight)); + CHECK(at(weight, 7) == 0); + CHECK(at(weight, 3) == 0); + } for (const ALScriptWeight::Part& part : weight.parts) { if (part.line >= 0) { - ensure(part.name + "'s lines are no more than it" + lined(weight), within(weight, part.line, part.endLine) <= part.bytes); + UNSCOPED_INFO(part.name + "'s lines are no more than it" + lined(weight)); + REQUIRE(within(weight, part.line, part.endLine) <= part.bytes); } } size_t all = 0; @@ -520,16 +626,17 @@ namespace tut { all += line.bytes; } - ensure("the lines are less than the whole", all > 0 && all < weight.total); + CHECK(all > 0); + CHECK(all < weight.total); } // In the source's places: a part in an include is in the include, at // its line there, and a line's bytes are the source line's; what the // map has no origin for keeps its part without a place. - template<> template<> - void alscriptweight_object::test<9>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: in the source's places: a part in an include is at its line there", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const std::vector lines = { "integer twice(integer n)", "{", " return n * 2;", @@ -565,45 +672,82 @@ namespace tut return map; }; const ALScriptWeight weight = ALScriptWeigh::lso(expanded); - ensure("compiled: " + weight.error, weight.compiled); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); const ALScriptWeight placed = weight.inSource(mapped(true)); - ensure_equals("the same weight", placed.total, weight.total); + CHECK(placed.total == weight.total); const ALScriptWeight::Part* twice = named(placed, "twice"); const ALScriptWeight::Part* entry = named(placed, "state_entry"); - ensure("both:" + listed(placed), twice && entry); - ensure_equals("a function in the include", twice->file, std::string("disk:/scripts/lib.lsl")); - ensure_equals("at its line there", twice->line, 0); - ensure_equals("to its end there", twice->endLine, 3); - ensure("a handler in the script", entry->file.empty()); - ensure_equals("at the script's line", entry->line, 3); + { + INFO("both:" + listed(placed)); + REQUIRE(twice); + REQUIRE(entry); + } + UNSCOPED_INFO("a function in the include"); + CHECK(twice->file == std::string("disk:/scripts/lib.lsl")); + UNSCOPED_INFO("at its line there"); + CHECK(twice->line == 0); + UNSCOPED_INFO("to its end there"); + CHECK(twice->endLine == 3); + UNSCOPED_INFO("a handler in the script"); + CHECK(entry->file.empty()); + UNSCOPED_INFO("at the script's line"); + CHECK(entry->line == 3); size_t in_include = 0, in_script = 0; for (const ALScriptWeight::Line& line : placed.lines) { - ensure("a line of one of the two" + lined(placed), line.file.empty() || line.file == "disk:/scripts/lib.lsl"); - ensure("not the #include's" + lined(placed), !(line.file.empty() && line.line == 0)); + UNSCOPED_INFO("a line of one of the two" + lined(placed)); + REQUIRE((line.file.empty() || line.file == "disk:/scripts/lib.lsl")); + UNSCOPED_INFO("not the #include's" + lined(placed)); + REQUIRE_FALSE((line.file.empty() && line.line == 0)); (line.file.empty() ? in_script : in_include) += line.bytes; } - ensure("the include's return is its line's", in_include > 0); - ensure("the call is the script's", in_script > 0); + UNSCOPED_INFO("the include's return is its line's"); + CHECK(in_include > 0); + UNSCOPED_INFO("the call is the script's"); + CHECK(in_script > 0); size_t all = 0; for (const ALScriptWeight::Line& line : weight.lines) { all += line.bytes; } - ensure_equals("every line's bytes somewhere", in_include + in_script, all); + UNSCOPED_INFO("every line's bytes somewhere"); + CHECK(in_include + in_script == all); const ALScriptWeight lost = weight.inSource(mapped(false)); - ensure("a part the map has no origin for is kept" + listed(lost), named(lost, "twice") != nullptr); - ensure_equals("without a place", named(lost, "twice")->line, -1); - ensure_equals("the handler still placed", named(lost, "state_entry")->line, 3); + UNSCOPED_INFO("a part the map has no origin for is kept" + listed(lost)); + REQUIRE(named(lost, "twice") != nullptr); + UNSCOPED_INFO("without a place"); + CHECK(named(lost, "twice")->line == -1); + UNSCOPED_INFO("the handler still placed"); + CHECK(named(lost, "state_entry")->line == 3); + } + + // A long list parses on every platform: each element nests the parse two + // states deeper, and the parser's limit is the same everywhere. + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: a long list parses on every platform: each element nests two states deeper", "[ALScriptWeight]") + { + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); + std::string script = "list gNumbers = ["; + for (int i = 0; i < 3000; ++i) + { + script += (i ? ", " : "") + std::to_string(i); + } + script += "];\ndefault\n{\n state_entry()\n {\n llOwnerSay((string)llGetListLength(gNumbers));\n }\n}\n"; + lsl.check(script); + CHECK(lsl.parsed()); + const ALScriptWeight weight = ALScriptWeigh::lso(script); + UNSCOPED_INFO("and is weighed: " + weight.error); + CHECK(weight.total > 0); } // Mono's user string heap holds a string once, however many loads of // it there are: a second load costs the instruction alone, on its line. - template<> template<> - void alscriptweight_object::test<11>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: Mono's user string heap holds a string once however often it is loaded", "[ALScriptWeight]") { - ensure("builtins: " + error, lslLoaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(lslLoaded); const auto script = [](int says) { std::string text = "default\n{\n state_entry()\n {\n"; for (int i = 0; i < says; ++i) @@ -615,17 +759,24 @@ namespace tut const ALScriptWeight one = ALScriptWeigh::mono(script(1)); const ALScriptWeight two = ALScriptWeigh::mono(script(2)); const ALScriptWeight three = ALScriptWeigh::mono(script(3)); - ensure("compiled: " + one.error + two.error + three.error, one.compiled && two.compiled && three.compiled); - ensure_equals("the third load as the second", three.total - two.total, two.total - one.total); - ensure("the second less than the string" + lined(two), two.total - one.total < 20); - ensure("the string is the first line's" + lined(two), at(two, 4) > at(two, 5) + 80); + { + INFO("compiled: " + one.error + two.error + three.error); + CHECK(one.compiled); + CHECK(two.compiled); + CHECK(three.compiled); + } + UNSCOPED_INFO("the third load as the second"); + CHECK(three.total - two.total == two.total - one.total); + UNSCOPED_INFO("the second less than the string" + lined(two)); + CHECK(two.total - one.total < 20); + UNSCOPED_INFO("the string is the first line's" + lined(two)); + CHECK(at(two, 4) > at(two, 5) + 80); } // SLua's handlers and callbacks are named as the script means them, a // state's with its state, each part from its function to its end; and // a line carries the strings and constants it is first to name. - template<> template<> - void alscriptweight_object::test<12>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: SLua's handlers and callbacks are named as the script means them", "[ALScriptWeight]") { const std::string script = "local greeting = \"a sentence of some length, said to the owner as the script starts up\"\n" "local function twice(n: number): number\n" @@ -645,7 +796,8 @@ namespace tut " print(\"tick\")\n" "end)\n"; const ALScriptWeight weight = ALScriptWeigh::slua(script); - ensure("compiled: " + weight.error, weight.compiled); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); const auto find = [&](const std::string& name, const std::string& within) -> const ALScriptWeight::Part* { for (const ALScriptWeight::Part& part : weight.parts) { @@ -657,32 +809,60 @@ namespace tut return nullptr; }; const ALScriptWeight::Part* twice = find("twice", ""); - ensure("a local function, from its function to its end:" + listed(weight), twice && twice->line == 1 && twice->endLine == 3 && twice->endColumn == 3); - ensure("not a handler", twice->kind == ALScriptWeight::Part::Kind::Function); + { + INFO("a local function, from its function to its end:" + listed(weight)); + REQUIRE(twice); + CHECK(twice->line == 1); + CHECK(twice->endLine == 3); + CHECK(twice->endColumn == 3); + } + UNSCOPED_INFO("not a handler"); + CHECK(twice->kind == ALScriptWeight::Part::Kind::Function); const ALScriptWeight::Part* entry = find("state_entry", "default"); - ensure("a state's handler, with its state:" + listed(weight), entry && entry->kind == ALScriptWeight::Part::Kind::Handler); - ensure("from its function to its end", entry->line == 6 && entry->column == 18 && entry->endLine == 8); - ensure("a state named by a string:" + listed(weight), find("touch_start", "other place") != nullptr); + { + INFO("a state's handler, with its state:" + listed(weight)); + REQUIRE(entry); + CHECK(entry->kind == ALScriptWeight::Part::Kind::Handler); + } + { + INFO("from its function to its end"); + CHECK(entry->line == 6); + CHECK(entry->column == 18); + CHECK(entry->endLine == 8); + } + UNSCOPED_INFO("a state named by a string:" + listed(weight)); + CHECK(find("touch_start", "other place") != nullptr); const ALScriptWeight::Part* touched = find("touch_start", ""); - ensure("LLEvents' handler by its event:" + listed(weight), touched && touched->kind == ALScriptWeight::Part::Kind::Handler && touched->line == 11); + { + INFO("LLEvents' handler by its event:" + listed(weight)); + REQUIRE(touched); + CHECK(touched->kind == ALScriptWeight::Part::Kind::Handler); + CHECK(touched->line == 11); + } const ALScriptWeight::Part* tick = find("LLTimers:every", ""); - ensure("what LLTimers calls, by how it is set going:" + listed(weight), tick && tick->kind == ALScriptWeight::Part::Kind::Function); + { + INFO("what LLTimers calls, by how it is set going:" + listed(weight)); + REQUIRE(tick); + CHECK(tick->kind == ALScriptWeight::Part::Kind::Function); + } // The greeting, a constant the compiler puts where it is used, is // that line's; the strings every line names are counted on some // line, so that the lines come to most of the whole. - ensure("the long string on the line that says it:" + lined(weight), at(weight, 7) > 80); - ensure("a short one on its own line:" + lined(weight), at(weight, 12) > 4 + std::string("touched").size()); + UNSCOPED_INFO("the long string on the line that says it:" + lined(weight)); + CHECK(at(weight, 7) > 80); + UNSCOPED_INFO("a short one on its own line:" + lined(weight)); + CHECK(at(weight, 12) > 4 + std::string("touched").size()); const size_t lines = within(weight, 0, 100); - ensure("most of the whole on some line: " + std::to_string(lines) + " of " + std::to_string(weight.total), lines * 3 > weight.total * 2); + UNSCOPED_INFO("most of the whole on some line: " << lines << " of " << weight.total); + CHECK(lines * 3 > weight.total * 2); } // Each string the table keeps: what it takes up, how many instructions // name it and how many load it as a value, the first line naming it, // and a function's name as such. They come to the table, its count // before them. - template<> template<> - void alscriptweight_object::test<13>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: each string the table keeps: its bytes and uses and loads and first line", "[ALScriptWeight]") { const std::string script = "local function greet(name: string)\n" // 0 " ll.OwnerSay(\"hello \" .. name)\n" // 1 @@ -693,7 +873,8 @@ namespace tut "greet(\"hello again\")\n" // 6 "greet(\"hello again\")\n"; // 7 const ALScriptWeight w = ALScriptWeigh::slua(script); - ensure("compiled: " + w.error, w.compiled); + UNSCOPED_INFO("compiled: " + w.error); + CHECK(w.compiled); const auto of = [&w](const std::string& text) -> const ALScriptWeight::String* { for (const ALScriptWeight::String& one : w.strings) { @@ -705,41 +886,79 @@ namespace tut return nullptr; }; const ALScriptWeight::String* again = of("hello again"); - ensure("a value loaded twice", again && again->uses == 2 && again->loads == 2 && again->line == 6 && again->bytes == 12); + { + INFO("a value loaded twice"); + REQUIRE(again); + CHECK(again->uses == 2); + CHECK(again->loads == 2); + CHECK(again->line == 6); + CHECK(again->bytes == 12); + } const ALScriptWeight::String* colour = of("colour"); - ensure("a field's name: named, never loaded", colour && colour->uses >= 2 && colour->loads == 0 && colour->line == 4); + { + INFO("a field's name: named, never loaded"); + REQUIRE(colour); + CHECK(colour->uses >= 2); + CHECK(colour->loads == 0); + CHECK(colour->line == 4); + } const ALScriptWeight::String* blue = of("blue"); - ensure("a comparison's: named, never loaded", blue && blue->uses == 1 && blue->loads == 0); + { + INFO("a comparison's: named, never loaded"); + REQUIRE(blue); + CHECK(blue->uses == 1); + CHECK(blue->loads == 0); + } const ALScriptWeight::String* say = of("OwnerSay"); - ensure("an import's path", say && say->uses >= 1 && say->loads == 0 && say->line == 1); + { + INFO("an import's path"); + REQUIRE(say); + CHECK(say->uses >= 1); + CHECK(say->loads == 0); + CHECK(say->line == 1); + } const ALScriptWeight::String* greet = of("greet"); - ensure("a function's name, at the line it starts on", greet && greet->name && greet->uses == 0 && greet->line == 0); + { + INFO("a function's name, at the line it starts on"); + REQUIRE(greet); + CHECK(greet->name); + CHECK(greet->uses == 0); + CHECK(greet->line == 0); + } size_t bytes = 0; for (const ALScriptWeight::String& one : w.strings) { bytes += one.bytes; } const ALScriptWeight::Part* table = named(w, "strings"); - ensure("they come to the table, with its count", table && table->bytes == bytes + 1); + { + INFO("they come to the table, with its count"); + REQUIRE(table); + CHECK(table->bytes == bytes + 1); + } // LSL compiled for Luau's VM: its strings, at the lines the lined // compile says. - ensure("the builtins: " + error, lslLoaded); + UNSCOPED_INFO("the builtins: " + error); + CHECK(lslLoaded); const ALScriptWeight lsl = ALScriptWeigh::lslLuau(byLine()); bool found = false; for (const ALScriptWeight::String& one : lsl.strings) { found = found || (one.text == "a sentence of some length, said to the owner on every start" && one.line == 15); } - ensure("LSL on Luau's, placed", lsl.compiled && found); + { + INFO("LSL on Luau's, placed"); + CHECK(lsl.compiled); + CHECK(found); + } } // Strings that start alike: where keeping the start once and joining it // back on would weigh less, what it would save -- the table's bytes // before less after, less the join at each load and the local once. // Only strings loaded as values; a short start saves nothing. - template<> template<> - void alscriptweight_object::test<14>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: strings that start alike: where keeping the start once would weigh less", "[ALScriptWeight]") { const std::string start = "You have touched the object of the day: "; const std::vector rests = { "alpha", "bravo", "charlie", "delta" }; @@ -753,11 +972,15 @@ namespace tut // Two sharing a start too short to be worth it. script += "ll.OwnerSay(\"short one\")\nll.OwnerSay(\"short two\")\n"; const ALScriptWeight w = ALScriptWeigh::slua(script); - ensure("compiled: " + w.error, w.compiled); - ensure_equals("one start worth keeping", w.sharedStarts.size(), size_t(1)); + UNSCOPED_INFO("compiled: " + w.error); + CHECK(w.compiled); + UNSCOPED_INFO("one start worth keeping"); + REQUIRE(w.sharedStarts.size() == size_t(1)); const ALScriptWeight::SharedStart& shared = w.sharedStarts.front(); - ensure_equals("the start the four share", shared.start, start); - ensure_equals("the four, not the field's", shared.strings.size(), rests.size()); + UNSCOPED_INFO("the start the four share"); + CHECK(shared.start == start); + UNSCOPED_INFO("the four, not the field's"); + CHECK(shared.strings.size() == rests.size()); size_t before = 0; size_t after = 1 + start.size(); for (const std::string& rest : rests) @@ -766,10 +989,12 @@ namespace tut after += 1 + rest.size(); } const size_t cost = after + rests.size() * ALScriptWeigh::SHARED_START_PER_LOAD + ALScriptWeigh::SHARED_START_ONCE; - ensure_equals("what it saves", shared.saved, before - cost); + UNSCOPED_INFO("what it saves"); + CHECK(shared.saved == before - cost); for (const size_t i : shared.strings) { - ensure("each one of them", w.strings[i].text.rfind(start, 0) == 0 && w.strings[i].text != start + "zulu"); + REQUIRE(w.strings[i].text.rfind(start, 0) == 0); + REQUIRE(w.strings[i].text != start + "zulu"); } // The costs against the compiler's own: the four written whole, and @@ -784,23 +1009,29 @@ namespace tut } const ALScriptWeight as_whole = ALScriptWeigh::slua(whole); const ALScriptWeight as_apart = ALScriptWeigh::slua(apart); - ensure("both compiled", as_whole.compiled && as_apart.compiled && as_whole.sharedStarts.size() == 1); + CHECK(as_whole.compiled); + CHECK(as_apart.compiled); + REQUIRE(as_whole.sharedStarts.size() == 1); const S64 really = S64(as_whole.total) - S64(as_apart.total); - ensure(llformat("saves about what was said: %lld against %zu", (long long)really, as_whole.sharedStarts[0].saved), - std::abs(really - S64(as_whole.sharedStarts[0].saved)) <= 8); + UNSCOPED_INFO("saves about what was said: " << really << " against " << as_whole.sharedStarts[0].saved); + CHECK(std::abs(really - S64(as_whole.sharedStarts[0].saved)) <= 8); // Nothing where only two share a start: two loads cost more than // one start saves, until the start is long. - ensure("two with a middling start: nothing", - ALScriptWeigh::slua("ll.OwnerSay(\"twenty bytes of start, then one\")\nll.OwnerSay(\"twenty bytes of start, then two\")\n").sharedStarts.empty()); - ensure("two with a long one: worth it", - ALScriptWeigh::slua("ll.OwnerSay(\"a start long enough to be worth keeping once, then one\")\n" - "ll.OwnerSay(\"a start long enough to be worth keeping once, then two\")\n") - .sharedStarts.size() == 1); + UNSCOPED_INFO("two with a middling start: nothing"); + CHECK(ALScriptWeigh::slua("ll.OwnerSay(\"twenty bytes of start, then one\")\nll.OwnerSay(\"twenty bytes of start, then two\")\n").sharedStarts.empty()); + UNSCOPED_INFO("two with a long one: worth it"); + CHECK(ALScriptWeigh::slua("ll.OwnerSay(\"a start long enough to be worth keeping once, then one\")\n" + "ll.OwnerSay(\"a start long enough to be worth keeping once, then two\")\n") + .sharedStarts.size() == 1); // Never inside a character. const ALScriptWeight cut = ALScriptWeigh::slua("ll.OwnerSay(\"a start long enough to be worth keeping once \xC3\xA9t\xC3\xA9\")\n" "ll.OwnerSay(\"a start long enough to be worth keeping once \xC3\xA8re\")\n"); - ensure("cut before the character", cut.sharedStarts.size() == 1 && cut.sharedStarts[0].start == "a start long enough to be worth keeping once "); + { + INFO("cut before the character"); + REQUIRE(cut.sharedStarts.size() == 1); + CHECK(cut.sharedStarts[0].start == "a start long enough to be worth keeping once "); + } } // A shared start kept once in the script itself: each literal of its @@ -812,8 +1043,7 @@ namespace tut // works out as it compiles and a string not of them left alone. What it // weighs after is what was said apart, with nothing left to keep once; // and it checks, strict as well. - template<> template<> - void alscriptweight_object::test<15>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: a shared start kept once in the script itself", "[ALScriptWeight]") { const std::string start = "You have touched the object of the day: "; const std::string script = "--!strict\n" // 0 @@ -825,52 +1055,73 @@ namespace tut "print \"" + start + "delta\"\n" // 6 "ll.OwnerSay(\"other\")\n"; // 7 const ALScriptWeight before = ALScriptWeigh::slua(script); - ensure("weighed: " + before.error, before.compiled); - ensure_equals("one start", before.sharedStarts.size(), size_t(1)); + UNSCOPED_INFO("weighed: " + before.error); + CHECK(before.compiled); + UNSCOPED_INFO("one start"); + REQUIRE(before.sharedStarts.size() == size_t(1)); const std::vector strings = textsOf(before, before.sharedStarts[0]); - ensure("not the require's path, which is no string to keep once", - std::find(strings.begin(), strings.end(), start + "x") == strings.end()); + UNSCOPED_INFO("not the require's path, which is no string to keep once"); + CHECK(std::find(strings.begin(), strings.end(), start + "x") == strings.end()); ALLuauSharedStart::Rewrite rewrite; std::string error; const bool done = ALLuauSharedStart::rewrite(script, before.sharedStarts[0].start, strings, rewrite, error); - ensure("rewritten: " + error, done); - ensure_equals("its name", rewrite.name, std::string("sharedStart")); - const std::string after = applied(script, rewrite); + UNSCOPED_INFO("rewritten: " + error); + CHECK(done); + CHECK(rewrite.name == std::string("sharedStart")); + const std::string after = require_applied(script, rewrite); const auto has = [&after](const std::string& text) { return after.find(text) != std::string::npos; }; - ensure("the local past the --! comment: " + after, after.rfind("--!strict\nlocal sharedStart\nsharedStart = \"" + start + "\"\n\n", 0) == 0); - ensure("a call's: " + after, has("ll.OwnerSay(sharedStart .. \"alpha\")")); - ensure("constant strings joined, still folded: " + after, has("ll.OwnerSay(sharedStart .. (\"bravo\" .. \"!\"))")); - ensure("a table's value, not its key: " + after, has("[\"" + start + "key\"] = sharedStart .. \"charlie\"")); - ensure("a length Luau works out, left: " + after, has("print(#\"" + start + "delta\")")); - ensure("and a call with no brackets: " + after, has("print (sharedStart .. \"delta\")")); - ensure("a require's path left: " + after, has("require(\"" + start + "x\")")); - ensure("another string left: " + after, has("ll.OwnerSay(\"other\")")); + UNSCOPED_INFO("the local past the --! comment: " + after); + CHECK(after.rfind("--!strict\nlocal sharedStart\nsharedStart = \"" + start + "\"\n\n", 0) == 0); + UNSCOPED_INFO("a call's: " + after); + CHECK(has("ll.OwnerSay(sharedStart .. \"alpha\")")); + UNSCOPED_INFO("constant strings joined, still folded: " + after); + CHECK(has("ll.OwnerSay(sharedStart .. (\"bravo\" .. \"!\"))")); + UNSCOPED_INFO("a table's value, not its key: " + after); + CHECK(has("[\"" + start + "key\"] = sharedStart .. \"charlie\"")); + UNSCOPED_INFO("a length Luau works out, left: " + after); + CHECK(has("print(#\"" + start + "delta\")")); + UNSCOPED_INFO("and a call with no brackets: " + after); + CHECK(has("print (sharedStart .. \"delta\")")); + UNSCOPED_INFO("a require's path left: " + after); + CHECK(has("require(\"" + start + "x\")")); + UNSCOPED_INFO("another string left: " + after); + CHECK(has("ll.OwnerSay(\"other\")")); // What it weighs now, and that it checks. const ALScriptWeight weighed = ALScriptWeigh::slua(after); - ensure("weighed after: " + weighed.error, weighed.compiled); + UNSCOPED_INFO("weighed after: " + weighed.error); + CHECK(weighed.compiled); // Nothing left to keep once: the start is the local's one value, // kept once already, and the require's path no literal to keep. - ensure_equals("nothing offered again", weighed.sharedStarts.size(), size_t(0)); - ensure(llformat("lighter: %zu then %zu", before.total, weighed.total), weighed.total < before.total); + UNSCOPED_INFO("nothing offered again"); + CHECK(weighed.sharedStarts.size() == size_t(0)); + CHECK(weighed.total < before.total); ALLuauService service; llifstream in(std::string(AL_LSL_DEFINITIONS_DIR) + "/secondlife.d.luau", std::ios::binary); std::stringstream definitions; definitions << in.rdbuf(); - ensure("definitions: " + error, service.loadDefinitions(definitions.str(), error)); + UNSCOPED_INFO("definitions: " + error); + REQUIRE(service.loadDefinitions(definitions.str(), error)); std::string said; for (const ALScriptProblem& p : service.check(after)) { said += p.severity == ALScriptProblem::Severity::Error && p.message.find("require") == std::string::npos ? p.message + "\n" : std::string(); } - ensure("checks, strict:\n" + said + "---\n" + after, said.empty()); + UNSCOPED_INFO("checks, strict:\n" + said + "---\n" + after); + CHECK(said.empty()); - ensure("nothing to make of a script without them", - !ALLuauSharedStart::rewrite("print(\"x\")\n", start, strings, rewrite, error) && !error.empty()); + { + INFO("nothing to make of a script without them"); + CHECK_FALSE(ALLuauSharedStart::rewrite("print(\"x\")\n", start, strings, rewrite, error)); + CHECK_FALSE(error.empty()); + } const bool taken = ALLuauSharedStart::rewrite("local sharedStart = 1\nprint(\"" + start + "alpha\")\n", start, { start + "alpha" }, rewrite, error); - ensure("rewritten with a name taken: " + error, taken); - ensure_equals("a name it has taken, another", rewrite.name, std::string("sharedStart2")); - ensure_equals("quoted as SLua reads it", ALLuauSharedStart::quoted("a\"b\\c\n\x01" "2"), std::string("\"a\\\"b\\\\c\\n\\0012\"")); + UNSCOPED_INFO("rewritten with a name taken: " + error); + CHECK(taken); + UNSCOPED_INFO("a name it has taken, another"); + CHECK(rewrite.name == std::string("sharedStart2")); + UNSCOPED_INFO("quoted as SLua reads it"); + CHECK(ALLuauSharedStart::quoted("a\"b\\c\n\x01" "2") == std::string("\"a\\\"b\\\\c\\n\\0012\"")); } // Strings loaded in closures: each function that loads one, and each @@ -878,8 +1129,7 @@ namespace tut // script's body close it over as it ends, which the saving counts. // What it says is about what the compiler makes: a closure shared // before frees its constant made anew, a byte or two each. - template<> template<> - void alscriptweight_object::test<16>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: strings loaded in closures: the saving counts each closure's capture", "[ALScriptWeight]") { const std::string start = "You have touched the object of the day: "; const std::vector rests = { "alpha", "bravo", "charlie" }; @@ -893,13 +1143,20 @@ namespace tut } const ALScriptWeight as_whole = ALScriptWeigh::slua(whole); const ALScriptWeight as_apart = ALScriptWeigh::slua(apart); - ensure("both compiled: " + as_whole.error + as_apart.error, as_whole.compiled && as_apart.compiled); - ensure_equals("one start", as_whole.sharedStarts.size(), size_t(1)); + { + INFO("both compiled: " + as_whole.error + as_apart.error); + CHECK(as_whole.compiled); + CHECK(as_apart.compiled); + } + UNSCOPED_INFO("one start"); + REQUIRE(as_whole.sharedStarts.size() == size_t(1)); const ALScriptWeight::SharedStart& shared = as_whole.sharedStarts.front(); - ensure_equals("the start the three share", shared.start, start); + UNSCOPED_INFO("the start the three share"); + CHECK(shared.start == start); for (const size_t i : shared.strings) { - ensure_equals("each loaded in a function of its own", as_whole.strings[i].functions.size(), size_t(1)); + UNSCOPED_INFO("each loaded in a function of its own"); + REQUIRE(as_whole.strings[i].functions.size() == size_t(1)); } size_t before = 0; size_t after = 1 + start.size(); @@ -910,10 +1167,11 @@ namespace tut } const size_t cost = after + rests.size() * (ALScriptWeigh::SHARED_START_PER_LOAD + ALScriptWeigh::SHARED_START_PER_FUNCTION) + ALScriptWeigh::SHARED_START_ONCE + ALScriptWeigh::SHARED_START_CAPTURED; - ensure_equals("what it saves, a capture for each closure", shared.saved, before - cost); + UNSCOPED_INFO("what it saves, a capture for each closure"); + CHECK(shared.saved == before - cost); const S64 really = S64(as_whole.total) - S64(as_apart.total); - ensure(llformat("saves about what was said: %lld against %zu", (long long)really, shared.saved), - std::abs(really - S64(shared.saved)) <= S64(8 + 2 * rests.size())); + UNSCOPED_INFO("saves about what was said: " << really << " against " << shared.saved); + CHECK(std::abs(really - S64(shared.saved)) <= S64(8 + 2 * rests.size())); } // Where the start's local goes: before the first of the script that is @@ -923,8 +1181,7 @@ namespace tut // comment on it. What would not parse is refused, as is a start not // every string of which is written out; and a byte of no character is // quoted in hex. - template<> template<> - void alscriptweight_object::test<17>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: where the start's local goes: before the first thing that is not a comment", "[ALScriptWeight]") { const std::string start = "You have touched the object of the day: "; const std::vector strings = { start + "alpha", start + "bravo" }; @@ -934,40 +1191,60 @@ namespace tut ALLuauSharedStart::Rewrite rewrite; std::string why; const bool done = ALLuauSharedStart::rewrite(script, start, strings, rewrite, why); - ensure("rewritten: " + why + "\n" + script, done); - return alscriptweight_data::applied(script, rewrite); + UNSCOPED_INFO("rewritten: " + why + "\n" + script); + REQUIRE(done); + return alscriptweight_data::require_applied(script, rewrite); }; const auto heads = [&](const std::string& head, const std::string& text) { return text.rfind(head + local, 0) == 0; }; std::string after = kept("-- Door script\n--!strict\n" + said); - ensure("past an ordinary comment and the --! one: " + after, heads("-- Door script\n--!strict\n", after)); - ensure("and the strings kept: " + after, after.find("ll.OwnerSay(sharedStart .. \"alpha\")") != std::string::npos); + UNSCOPED_INFO("past an ordinary comment and the --! one: " + after); + CHECK(heads("-- Door script\n--!strict\n", after)); + UNSCOPED_INFO("and the strings kept: " + after); + CHECK(after.find("ll.OwnerSay(sharedStart .. \"alpha\")") != std::string::npos); after = kept("\n--!strict\n" + said); - ensure("past a blank first line: " + after, heads("\n--!strict\n", after)); + UNSCOPED_INFO("past a blank first line: " + after); + CHECK(heads("\n--!strict\n", after)); after = kept("--!strict\n\n--!optimize 2\n" + said); - ensure("past --! comments apart: " + after, heads("--!strict\n\n--!optimize 2\n", after)); + UNSCOPED_INFO("past --! comments apart: " + after); + CHECK(heads("--!strict\n\n--!optimize 2\n", after)); after = kept("--!strict\n " + said); - ensure("at the start of an indented line: " + after, heads("--!strict\n", after) && after.find(local + " ll.OwnerSay") != std::string::npos); + { + INFO("at the start of an indented line: " + after); + CHECK(heads("--!strict\n", after)); + CHECK(after.find(local + " ll.OwnerSay") != std::string::npos); + } after = kept("--[[ The door ]] " + said); - ensure("after a comment on the same line: " + after, heads("--[[ The door ]] ", after)); + UNSCOPED_INFO("after a comment on the same line: " + after); + CHECK(heads("--[[ The door ]] ", after)); ALLuauSharedStart::Rewrite rewrite; std::string error; - ensure("a call's string on a line of its own would be ambiguous bracketed", - !ALLuauSharedStart::rewrite("ll.OwnerSay\n\"" + start + "alpha\"\nll.OwnerSay(\"" + start + "bravo\")\n", start, strings, rewrite, error) && - error.find("does not parse") != std::string::npos); - ensure("nothing made of it", rewrite.edits.empty()); - ensure("a string not written out: " + error, - !ALLuauSharedStart::rewrite("ll.OwnerSay(\"" + start + "alpha\")\n", start, strings, rewrite, error) && - error.find("not every one") != std::string::npos); - ensure("none written out, a local joined on: " + error, - !ALLuauSharedStart::rewrite("local greeting = \"" + start + "\"\nll.OwnerSay(greeting .. \"alpha\")\nll.OwnerSay(greeting .. \"bravo\")\n", - start, strings, rewrite, error) && - error.find("none of these strings") != std::string::npos); + { + INFO("a call's string on a line of its own would be ambiguous bracketed"); + CHECK_FALSE(ALLuauSharedStart::rewrite("ll.OwnerSay\n\"" + start + "alpha\"\nll.OwnerSay(\"" + start + "bravo\")\n", start, strings, rewrite, error)); + CHECK(error.find("does not parse") != std::string::npos); + } + UNSCOPED_INFO("nothing made of it"); + CHECK(rewrite.edits.empty()); + { + INFO("a string not written out: " + error); + CHECK_FALSE(ALLuauSharedStart::rewrite("ll.OwnerSay(\"" + start + "alpha\")\n", start, strings, rewrite, error)); + CHECK(error.find("not every one") != std::string::npos); + } + { + INFO("none written out, a local joined on: " + error); + CHECK_FALSE(ALLuauSharedStart::rewrite("local greeting = \"" + start + "\"\nll.OwnerSay(greeting .. \"alpha\")\nll.OwnerSay(greeting .. \"bravo\")\n", + start, strings, rewrite, error)); + CHECK(error.find("none of these strings") != std::string::npos); + } - ensure_equals("a byte of no character in hex, a whole one as it is", ALLuauSharedStart::quoted("\xFF" "a\xC3\xA9\xC3"), + UNSCOPED_INFO("a byte of no character in hex, a whole one as it is"); + CHECK(ALLuauSharedStart::quoted("\xFF" "a\xC3\xA9\xC3") == std::string("\"\\xFFa\xC3\xA9\\xC3\"")); - ensure_equals("an overlong form is no character", ALLuauSharedStart::quoted("\xC0\x80"), std::string("\"\\xC0\\x80\"")); - ensure_equals("four bytes, whole", ALLuauSharedStart::quoted("\xF0\x9F\x98\x80"), std::string("\"\xF0\x9F\x98\x80\"")); + UNSCOPED_INFO("an overlong form is no character"); + CHECK(ALLuauSharedStart::quoted("\xC0\x80") == std::string("\"\\xC0\\x80\"")); + UNSCOPED_INFO("four bytes, whole"); + CHECK(ALLuauSharedStart::quoted("\xF0\x9F\x98\x80") == std::string("\"\xF0\x9F\x98\x80\"")); } // What is not offered: strings Luau makes as it compiles -- an @@ -976,31 +1253,31 @@ namespace tut // is not, the start kept once already, and a module's strings in the // source's places. What is: strings a `..` that is not folded goes on // to, each a literal of its own, kept once in place. - template<> template<> - void alscriptweight_object::test<18>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: what is not offered: strings Luau makes as it compiles", "[ALScriptWeight]") { const std::string start = "You have touched the object of the day: "; - ensure("an interpolated string's pattern", - ALScriptWeigh::slua("local function said(name: string)\n" - " ll.OwnerSay(`" + start + "{name}, first`)\n" - " ll.OwnerSay(`" + start + "{name}, second`)\n" - " ll.OwnerSay(`" + start + "{name}, third`)\n" - "end\n" - "said(\"x\")\n") - .sharedStarts.empty()); - ensure("strings joined on a local's constant value", - ALScriptWeigh::slua("local greeting = \"" + start + "\"\n" - "ll.OwnerSay(greeting .. \"alpha\")\n" - "ll.OwnerSay(greeting .. \"bravo\")\n" - "ll.OwnerSay(greeting .. \"charlie\")\n") - .sharedStarts.empty()); + UNSCOPED_INFO("an interpolated string's pattern"); + CHECK(ALScriptWeigh::slua("local function said(name: string)\n" + " ll.OwnerSay(`" + start + "{name}, first`)\n" + " ll.OwnerSay(`" + start + "{name}, second`)\n" + " ll.OwnerSay(`" + start + "{name}, third`)\n" + "end\n" + "said(\"x\")\n") + .sharedStarts.empty()); + UNSCOPED_INFO("strings joined on a local's constant value"); + CHECK(ALScriptWeigh::slua("local greeting = \"" + start + "\"\n" + "ll.OwnerSay(greeting .. \"alpha\")\n" + "ll.OwnerSay(greeting .. \"bravo\")\n" + "ll.OwnerSay(greeting .. \"charlie\")\n") + .sharedStarts.empty()); const auto feelings = [&start](const std::string& type) { return "type Mood = " + type + "\nlocal function feel(mood: Mood) end\n" "feel(\"" + start + "happy\")\nfeel(\"" + start + "sad\")\nfeel(\"" + start + "bored\")\n"; }; - ensure_equals("given for a string", ALScriptWeigh::slua(feelings("string")).sharedStarts.size(), size_t(1)); - ensure("given for a singleton", - ALScriptWeigh::slua(feelings("\"" + start + "happy\" | \"" + start + "sad\" | \"" + start + "bored\"")).sharedStarts.empty()); + UNSCOPED_INFO("given for a string"); + CHECK(ALScriptWeigh::slua(feelings("string")).sharedStarts.size() == size_t(1)); + UNSCOPED_INFO("given for a singleton"); + CHECK(ALScriptWeigh::slua(feelings("\"" + start + "happy\" | \"" + start + "sad\" | \"" + start + "bored\"")).sharedStarts.empty()); // Kept once, with an interpolated string sharing the start beside // them: neither it nor the start's local is offered again. @@ -1012,15 +1289,23 @@ namespace tut "ll.OwnerSay(\"" + start + "charlie\")\n" "said(\"x\")\n"; const ALScriptWeight kept = ALScriptWeigh::slua(kept_script); - ensure("weighed: " + kept.error, kept.compiled && kept.sharedStarts.size() == 1); - ensure_equals("the three written out, not the pattern", kept.sharedStarts[0].strings.size(), size_t(3)); + { + INFO("weighed: " + kept.error); + CHECK(kept.compiled); + REQUIRE(kept.sharedStarts.size() == 1); + } + UNSCOPED_INFO("the three written out, not the pattern"); + CHECK(kept.sharedStarts[0].strings.size() == size_t(3)); ALLuauSharedStart::Rewrite rewrite; std::string error; bool done = ALLuauSharedStart::rewrite(kept_script, start, textsOf(kept, kept.sharedStarts[0]), rewrite, error); - ensure("rewritten: " + error, done); - const ALScriptWeight again = ALScriptWeigh::slua(applied(kept_script, rewrite)); - ensure("weighed again: " + again.error, again.compiled); - ensure_equals("nothing offered again", again.sharedStarts.size(), size_t(0)); + UNSCOPED_INFO("rewritten: " + error); + CHECK(done); + const ALScriptWeight again = ALScriptWeigh::slua(require_applied(kept_script, rewrite)); + UNSCOPED_INFO("weighed again: " + again.error); + CHECK(again.compiled); + UNSCOPED_INFO("nothing offered again"); + CHECK(again.sharedStarts.size() == size_t(0)); // Joined on to a name, each piece is loaded on its own. const std::string joined = "local function touched(name: string)\n" @@ -1030,13 +1315,21 @@ namespace tut "end\n" "touched(\"x\")\n"; const ALScriptWeight pieces = ALScriptWeigh::slua(joined); - ensure("weighed: " + pieces.error, pieces.compiled && pieces.sharedStarts.size() == 1 && pieces.sharedStarts[0].start == start); + { + INFO("weighed: " + pieces.error); + CHECK(pieces.compiled); + REQUIRE(pieces.sharedStarts.size() == 1); + CHECK(pieces.sharedStarts[0].start == start); + } done = ALLuauSharedStart::rewrite(joined, start, textsOf(pieces, pieces.sharedStarts[0]), rewrite, error); - ensure("rewritten: " + error, done); - const std::string pieces_after = applied(joined, rewrite); - ensure("each piece kept in place: " + pieces_after, pieces_after.find("ll.OwnerSay(name .. sharedStart .. \"alpha\" .. \"!\")") != std::string::npos); + UNSCOPED_INFO("rewritten: " + error); + CHECK(done); + const std::string pieces_after = require_applied(joined, rewrite); + UNSCOPED_INFO("each piece kept in place: " + pieces_after); + CHECK(pieces_after.find("ll.OwnerSay(name .. sharedStart .. \"alpha\" .. \"!\")") != std::string::npos); const ALScriptWeight lighter = ALScriptWeigh::slua(pieces_after); - ensure(llformat("lighter: %zu then %zu", pieces.total, lighter.total), lighter.compiled && lighter.total < pieces.total); + CHECK(lighter.compiled); + CHECK(lighter.total < pieces.total); // A module's strings, in the source's places, are not the script's // to keep once: the first three lines are a module's. @@ -1067,42 +1360,55 @@ namespace tut return map; }; const ALScriptWeight bundled = ALScriptWeigh::slua(bundle); - ensure("weighed: " + bundled.error, bundled.compiled && bundled.sharedStarts.size() == 1); - ensure_equals("all the script's: kept", bundled.inSource(mapped(0)).sharedStarts.size(), size_t(1)); - ensure_equals("two a module's: nothing", bundled.inSource(mapped(3)).sharedStarts.size(), size_t(0)); + { + INFO("weighed: " + bundled.error); + CHECK(bundled.compiled); + CHECK(bundled.sharedStarts.size() == 1); + } + UNSCOPED_INFO("all the script's: kept"); + CHECK(bundled.inSource(mapped(0)).sharedStarts.size() == size_t(1)); + UNSCOPED_INFO("two a module's: nothing"); + CHECK(bundled.inSource(mapped(3)).sharedStarts.size() == size_t(0)); } // LSL's constants are folded as the grid's compiler folds them: a // constant read is its value in the bytecode, as if written out, and // not a global looked up by its name. - template<> template<> - void alscriptweight_object::test<19>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: LSL's constants are folded as the grid's compiler folds them", "[ALScriptWeight]") { const ALScriptWeight named = ALScriptWeigh::slua("local x = PRIM_SIZE\nprint(x)\n"); const ALScriptWeight written = ALScriptWeigh::slua("local x = 7\nprint(x)\n"); - ensure("both compiled", named.compiled && written.compiled); - ensure_equals("the same as its value written out", named.total, written.total); + CHECK(named.compiled); + CHECK(written.compiled); + UNSCOPED_INFO("the same as its value written out"); + CHECK(named.total == written.total); bool looked_up = false; for (const ALScriptWeight::String& one : named.strings) { looked_up = looked_up || one.text == "PRIM_SIZE"; } - ensure("not its name among the strings", !looked_up); + UNSCOPED_INFO("not its name among the strings"); + CHECK_FALSE(looked_up); } // A heavy string is named on one line as a string of the table is, a // return in it written out as a break and a tab are. - template<> template<> - void alscriptweight_object::test<20>() + TEST_CASE_METHOD(alscriptweight_data, "ALScriptWeight: a heavy string is named on one line as a string of the table is", "[ALScriptWeight]") { const ALScriptWeight weight = ALScriptWeigh::slua("local s = \"a\\rb\\tc" + std::string(300, 'x') + "\"\nprint(s)\n"); - ensure("compiled: " + weight.error, weight.compiled); + UNSCOPED_INFO("compiled: " + weight.error); + CHECK(weight.compiled); const ALScriptWeight::Part* found = nullptr; for (const ALScriptWeight::Part& one : weight.parts) { found = one.kind == ALScriptWeight::Part::Kind::Constant && one.name != "strings" ? &one : found; } - ensure("a part of its own:" + listed(weight), found != nullptr); - ensure("named on one line: " + found->name, found->name.rfind("\"a\\rb\\tc", 0) == 0 && found->name.find('\r') == std::string::npos); + UNSCOPED_INFO("a part of its own:" + listed(weight)); + REQUIRE(found != nullptr); + { + INFO("named on one line: " + found->name); + CHECK(found->name.rfind("\"a\\rb\\tc", 0) == 0); + CHECK(found->name.find('\r') == std::string::npos); + } } } diff --git a/indra/alscript/tests/alsourcemap_test.cpp b/indra/alscript/tests/alsourcemap_test.cpp index 186641fc0f..e1ba8b7497 100644 --- a/indra/alscript/tests/alsourcemap_test.cpp +++ b/indra/alscript/tests/alsourcemap_test.cpp @@ -25,7 +25,7 @@ #include "../preprocessor/alsourcemap.h" -#include "../test/lltut.h" +#include namespace { @@ -43,20 +43,10 @@ namespace } } -namespace tut +namespace { - struct alsourcemap_data - { - }; - - typedef test_group alsourcemap_group; - typedef alsourcemap_group::object alsourcemap_object; - alsourcemap_group alsourcemap_instance("alsourcemap"); - - template<> template<> - void alsourcemap_object::test<1>() + TEST_CASE("alsourcemap: verbatim tokens in a run are one segment; a macro or line or file starts anew", "[alsourcemap]") { - set_test_name("verbatim tokens that carry on from one another are one segment; a macro's product, another line or another file start their own"); ALSourceMap map; map.addFile("script", "script"); map.addFile("lib", "disk:/lib"); @@ -67,19 +57,39 @@ namespace tut map.add(segment(0, 14, 1, 0, 0, 12)); // ; -- not where the last left off map.add(segment(1, 0, 4, 1, 0, 0)); // another file map.finish(); - ensure("the first three one", map.toSource(0, 8).column == 8 && map.toSource(0, 0).line == 0); + { + INFO("the first three one"); + CHECK(map.toSource(0, 8).column == 8); + CHECK(map.toSource(0, 0).line == 0); + } ALSourceMap::Loc begin, end; - ensure("one stretch copied as it stands", map.verbatimSpan(0, 0, 9, begin, end) && begin.column == 0 && end.column == 9); - ensure("the macro's to its invocation", map.toSource(0, 11).line == 3 && map.toSource(0, 11).column == 2); - ensure("the next its own", map.toSource(0, 14).column == 12); - ensure("the other file's", map.toSource(1, 2).file == 1 && map.toSource(1, 2).column == 2); - ensure("and back", map.toExpanded(0, 0, 8).line == 0 && map.toExpanded(0, 0, 8).column == 8); + { + INFO("one stretch copied as it stands"); + CHECK(map.verbatimSpan(0, 0, 9, begin, end)); + CHECK(begin.column == 0); + CHECK(end.column == 9); + } + { + INFO("the macro's to its invocation"); + CHECK(map.toSource(0, 11).line == 3); + CHECK(map.toSource(0, 11).column == 2); + } + UNSCOPED_INFO("the next its own"); + CHECK(map.toSource(0, 14).column == 12); + { + INFO("the other file's"); + CHECK(map.toSource(1, 2).file == 1); + CHECK(map.toSource(1, 2).column == 2); + } + { + INFO("and back"); + CHECK(map.toExpanded(0, 0, 8).line == 0); + CHECK(map.toExpanded(0, 0, 8).column == 8); + } } - template<> template<> - void alsourcemap_object::test<2>() + TEST_CASE("alsourcemap: the segment at or before a column on a long line; nothing on a line with none", "[alsourcemap]") { - set_test_name("a long line of a macro's products looked up: the segment at or before the column, the first before any, nothing on a line with none"); ALSourceMap map; map.addFile("script", "script"); for (S32 i = 0; i < 3000; ++i) @@ -88,17 +98,23 @@ namespace tut } map.add(segment(2, 0, 3, 0, 5, 0)); map.finish(); - ensure_equals("the first", map.toSource(0, 4).column, 0); - ensure_equals("in the middle", map.toSource(0, 4 + 1500 * 3 + 1).column, 1500); - ensure_equals("before any: the first", map.toSource(0, 1).column, 0); - ensure_equals("past the last: the last", map.toSource(0, 20000).column, 2999); - ensure("a line with none: nowhere", !map.toSource(1, 0).found() && map.toSource(2, 1).found()); + UNSCOPED_INFO("the first"); + CHECK(map.toSource(0, 4).column == 0); + UNSCOPED_INFO("in the middle"); + CHECK(map.toSource(0, 4 + 1500 * 3 + 1).column == 1500); + UNSCOPED_INFO("before any: the first"); + CHECK(map.toSource(0, 1).column == 0); + UNSCOPED_INFO("past the last: the last"); + CHECK(map.toSource(0, 20000).column == 2999); + { + INFO("a line with none: nowhere"); + CHECK_FALSE(map.toSource(1, 0).found()); + CHECK(map.toSource(2, 1).found()); + } } - template<> template<> - void alsourcemap_object::test<3>() + TEST_CASE("alsourcemap: a map composed through another reads a stretch through each inner segment", "[alsourcemap]") { - set_test_name("a map read through another: one copied stretch over several of the other's segments read through each, exact where they are"); // The inner map: its output line 0 is `abc` from the script's line // 4, then `MACRO` a macro made at line 9 column 1, then `xyz` from // the include's line 2. @@ -116,26 +132,43 @@ namespace tut outer.add(segment(1, 0, 3, 0, 7, 0)); outer.finish(); const ALSourceMap through = outer.composed(inner); - ensure("the script's own, exact", through.toSource(0, 3).file == 0 && through.toSource(0, 3).line == 4 && through.toSource(0, 3).column == 1); - ensure("the macro's, to its invocation", through.toSource(0, 7).line == 9 && through.toSource(0, 7).column == 1); - ensure("the include's, exact", through.toSource(0, 11).file == 1 && through.toSource(0, 11).column == 1); - ensure("a line the inner has nothing for: dropped", !through.toSource(1, 0).found()); + { + INFO("the script's own, exact"); + CHECK(through.toSource(0, 3).file == 0); + CHECK(through.toSource(0, 3).line == 4); + CHECK(through.toSource(0, 3).column == 1); + } + { + INFO("the macro's, to its invocation"); + CHECK(through.toSource(0, 7).line == 9); + CHECK(through.toSource(0, 7).column == 1); + } + { + INFO("the include's, exact"); + CHECK(through.toSource(0, 11).file == 1); + CHECK(through.toSource(0, 11).column == 1); + } + UNSCOPED_INFO("a line the inner has nothing for: dropped"); + CHECK_FALSE(through.toSource(1, 0).found()); ALSourceMap::Loc begin, end; - ensure("the copied stretches stay verbatim", through.verbatimSpan(0, 2, 5, begin, end) && !through.verbatimSpan(0, 5, 10, begin, end)); + { + INFO("the copied stretches stay verbatim"); + CHECK(through.verbatimSpan(0, 2, 5, begin, end)); + CHECK_FALSE(through.verbatimSpan(0, 5, 10, begin, end)); + } } - template<> template<> - void alsourcemap_object::test<4>() + TEST_CASE("alsourcemap: a text mapped to itself: every line its own even the last without a newline", "[alsourcemap]") { - set_test_name("a text mapped to itself: every line its own, the last without a newline too"); const ALSourceMap map = ALSourceMap::identity("one\ntwo three\nlast", "same"); - ensure("named", map.files().size() == 1 && map.files()[0].name == "same"); - ensure("each line", map.toSource(1, 5).line == 1 && map.toSource(1, 5).column == 5 && map.toSource(2, 2).line == 2); + REQUIRE(map.files().size() == 1); + CHECK(map.files()[0].name == "same"); + CHECK(map.toSource(1, 5).line == 1); + CHECK(map.toSource(1, 5).column == 5); + CHECK(map.toSource(2, 2).line == 2); } - template<> template<> - void alsourcemap_object::test<5>() + TEST_CASE("alsourcemap: a verbatim stretch on a long line: the first segment to reach the column", "[alsourcemap]") { - set_test_name("a verbatim stretch found on a long line: the first segment that reaches the column, as a walk from the line's start finds it"); ALSourceMap map; map.addFile("script", "script"); // A long line of alternating pieces: a macro's product, then a @@ -150,11 +183,25 @@ namespace tut map.finish(); ALSourceMap::Loc begin, end; const S32 at = 7 * 250 + 4; - ensure("within a token copied, deep in the line", map.verbatimSpan(0, at + 1, at + 3, begin, end) && begin.line == 1 && begin.column == 2501 && end.column == 2503); - ensure("an insertion just after it goes with it", map.verbatimSpan(0, at + 3, at + 3, begin, end) && begin.column == 2503); - ensure("an insertion just before it goes with the macro's product before it", !map.verbatimSpan(0, at, at, begin, end)); - ensure("and so does a stretch from its first column", !map.verbatimSpan(0, at, at + 3, begin, end)); - ensure("into the macro's product after it: not the source's", !map.verbatimSpan(0, at + 1, at + 5, begin, end)); - ensure("past the line's end", !map.verbatimSpan(0, out + 2, out + 3, begin, end)); + { + INFO("within a token copied, deep in the line"); + REQUIRE(map.verbatimSpan(0, at + 1, at + 3, begin, end)); + CHECK(begin.line == 1); + CHECK(begin.column == 2501); + CHECK(end.column == 2503); + } + { + INFO("an insertion just after it goes with it"); + CHECK(map.verbatimSpan(0, at + 3, at + 3, begin, end)); + CHECK(begin.column == 2503); + } + UNSCOPED_INFO("an insertion just before it goes with the macro's product before it"); + CHECK_FALSE(map.verbatimSpan(0, at, at, begin, end)); + UNSCOPED_INFO("and so does a stretch from its first column"); + CHECK_FALSE(map.verbatimSpan(0, at, at + 3, begin, end)); + UNSCOPED_INFO("into the macro's product after it: not the source's"); + CHECK_FALSE(map.verbatimSpan(0, at + 1, at + 5, begin, end)); + UNSCOPED_INFO("past the line's end"); + CHECK_FALSE(map.verbatimSpan(0, out + 2, out + 3, begin, end)); } } diff --git a/indra/alscript/tests/aluploadheader_test.cpp b/indra/alscript/tests/aluploadheader_test.cpp index 027907f959..1c79a3208e 100644 --- a/indra/alscript/tests/aluploadheader_test.cpp +++ b/indra/alscript/tests/aluploadheader_test.cpp @@ -28,13 +28,13 @@ #include "lldate.h" -#include "../test/lltut.h" +#include #include #include -namespace tut +namespace { struct aluploadheader_data { @@ -65,25 +65,19 @@ namespace tut } }; - typedef test_group aluploadheader_group; - typedef aluploadheader_group::object aluploadheader_object; - aluploadheader_group aluploadheader_instance("aluploadheader"); - - template<> template<> - void aluploadheader_object::test<1>() + TEST_CASE_METHOD(aluploadheader_data, "aluploadheader: ours is written byte for byte in either language with only the fields it has", "[aluploadheader]") { - set_test_name("ours is written byte for byte, a field only where there is one, in each language's comments"); const ALUploadHeader header = aluploadheader_data::full(); - ensure_equals("LSL", - header.write(false), + UNSCOPED_INFO("LSL"); + CHECK(header.write(false) == std::string("// ================ alchemy meta ================\n" "// @file net/door.lsl\n" "// @hash xxh128:0123456789abcdef0123456789abcdef\n" "// @date 2026-10-07 14:03:11\n" "// @creator Rye Resident\n" "// ==============================================\n")); - ensure_equals("SLua", - header.write(true), + UNSCOPED_INFO("SLua"); + CHECK(header.write(true) == std::string("-- ================ alchemy meta ================\n" "-- @file net/door.lsl\n" "-- @hash xxh128:0123456789abcdef0123456789abcdef\n" @@ -94,8 +88,8 @@ namespace tut ALUploadHeader bare; bare.hash = "xxh128:00"; bare.date = "now"; - ensure_equals("no file and no creator", - bare.write(false), + UNSCOPED_INFO("no file and no creator"); + CHECK(bare.write(false) == std::string("// ================ alchemy meta ================\n// @hash xxh128:00\n// @date now\n" "// ==============================================\n")); @@ -103,21 +97,22 @@ namespace tut { ALUploadHeader absolute = bare; absolute.file = where; - ensure_equals(std::string("never absolute: ") + where, absolute.write(false), bare.write(false)); + INFO("never absolute: " << where); + REQUIRE(absolute.write(false) == bare.write(false)); } ALUploadHeader aliased = bare; aliased.file = "@lib/util.luau"; - ensure("an alias is no root", aliased.write(true).find("-- @file @lib/util.luau\n") != std::string::npos); + UNSCOPED_INFO("an alias is no root"); + CHECK(aliased.write(true).find("-- @file @lib/util.luau\n") != std::string::npos); ALUploadHeader broken = bare; broken.creator = "two\nlines\r"; - ensure("a line break in a field stays in its comment", broken.write(false).find("// @creator two lines \n") != std::string::npos); + UNSCOPED_INFO("a line break in a field stays in its comment"); + CHECK(broken.write(false).find("// @creator two lines \n") != std::string::npos); } - template<> template<> - void aluploadheader_object::test<2>() + TEST_CASE_METHOD(aluploadheader_data, "aluploadheader: ours reads back and take() steps past it to the code", "[aluploadheader]") { - set_test_name("ours reads back, and take() steps past it to the code"); for (const bool lua : { false, true }) { const ALUploadHeader written = aluploadheader_data::full(); @@ -125,30 +120,32 @@ namespace tut const std::string text = written.write(lua) + code; std::string_view rest = text; const std::optional back = ALUploadHeader::take(rest, lua); - const std::string name = lua ? "SLua: " : "LSL: "; - ensure(name + "read", back.has_value()); - ensure(name + "ours", back->ours); - ensure_equals(name + "file", back->file, written.file); - ensure_equals(name + "hash", back->hash, written.hash); - ensure_equals(name + "date", back->date, written.date); - ensure_equals(name + "creator", back->creator, written.creator); - ensure_equals(name + "no key", back->creatorId, std::string()); - ensure_equals(name + "the code is what is left", std::string(rest), code); - ensure(name + "not in the other language's comments", !ALUploadHeader::parse(text, !lua).has_value()); + CAPTURE(lua); + REQUIRE(back.has_value()); + REQUIRE(back->ours); + REQUIRE(back->file == written.file); + REQUIRE(back->hash == written.hash); + REQUIRE(back->date == written.date); + REQUIRE(back->creator == written.creator); + REQUIRE(back->creatorId == std::string()); + UNSCOPED_INFO("the code is what is left"); + REQUIRE(std::string(rest) == code); + UNSCOPED_INFO("not in the other language's comments"); + REQUIRE_FALSE(ALUploadHeader::parse(text, !lua).has_value()); } // Written on Windows, every line ends in a carriage return as well. std::string_view crlf = "// ================ alchemy meta ================\r\n// @hash h\r\n// ===\r\ncode"; const std::optional windows = ALUploadHeader::take(crlf, false); - ensure("carriage returns", windows.has_value()); - ensure_equals("and none in a field", windows->hash, std::string("h")); - ensure_equals("past it", std::string(crlf), std::string("code")); + UNSCOPED_INFO("carriage returns"); + REQUIRE(windows.has_value()); + UNSCOPED_INFO("and none in a field"); + CHECK(windows->hash == std::string("h")); + CHECK(std::string(crlf) == std::string("code")); } - template<> template<> - void aluploadheader_object::test<3>() + TEST_CASE_METHOD(aluploadheader_data, "aluploadheader: the plugin's block reads as it writes it", "[aluploadheader]") { - set_test_name("the plugin's block reads as it writes it"); // As sl-vscode-plugin's prefixWithMetaInformation writes it, its // creator lines and all. const std::string text = "// ================ sl-vscode-plugin meta ================\n" @@ -161,26 +158,31 @@ namespace tut "default { }\n"; std::string_view rest = text; const std::optional back = ALUploadHeader::take(rest, false); - ensure("read", back.has_value()); - ensure("not ours", !back->ours); - ensure_equals("file", back->file, std::string("scripts/door.lsl")); - ensure_equals("its sha256, as it is", back->hash, std::string("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")); - ensure_equals("date", back->date, std::string("2026-10-07 14:03:11")); - ensure_equals("creator", back->creator, std::string("Rye Resident")); - ensure_equals("key", back->creatorId, std::string("0b8f2ef9-1d4a-4f4c-9b62-7d3a1c4e5f60")); - ensure_equals("the code is what is left", std::string(rest), std::string("default { }\n")); + REQUIRE(back.has_value()); + CHECK_FALSE(back->ours); + CHECK(back->file == std::string("scripts/door.lsl")); + UNSCOPED_INFO("its sha256, as it is"); + CHECK(back->hash == std::string("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")); + CHECK(back->date == std::string("2026-10-07 14:03:11")); + CHECK(back->creator == std::string("Rye Resident")); + CHECK(back->creatorId == std::string("0b8f2ef9-1d4a-4f4c-9b62-7d3a1c4e5f60")); + UNSCOPED_INFO("the code is what is left"); + CHECK(std::string(rest) == std::string("default { }\n")); const std::optional slua = ALUploadHeader::parse("-- ================ sl-vscode-plugin meta ================\n" "-- @file a.luau\n-- @someday more\n" "-- =======================================================\n", true); - ensure("SLua's, a field to come passed over", slua.has_value() && slua->file == "a.luau" && !slua->ours); + { + INFO("SLua's, a field to come passed over"); + REQUIRE(slua.has_value()); + CHECK(slua->file == "a.luau"); + CHECK_FALSE(slua->ours); + } } - template<> template<> - void aluploadheader_object::test<4>() + TEST_CASE_METHOD(aluploadheader_data, "aluploadheader: what only looks like a header is code", "[aluploadheader]") { - set_test_name("what only looks like a header is code"); const char* texts[] = { // Nothing of the kind. "default { }\n", @@ -200,8 +202,9 @@ namespace tut for (const char* text : texts) { std::string_view rest = text; - ensure(std::string("not a header: ") + text, !ALUploadHeader::take(rest, false).has_value()); - ensure_equals(std::string("and nothing taken: ") + text, std::string(rest), std::string(text)); + CAPTURE(text); + REQUIRE_FALSE(ALUploadHeader::take(rest, false).has_value()); + REQUIRE(std::string(rest) == std::string(text)); } std::string runs_on = "// ================ alchemy meta ================\n"; for (int i = 0; i < 40; ++i) @@ -209,44 +212,56 @@ namespace tut runs_on += "// @x y\n"; } runs_on += "// ====\n"; - ensure("a block far longer than any header", !ALUploadHeader::parse(runs_on, false).has_value()); + UNSCOPED_INFO("a block far longer than any header"); + CHECK_FALSE(ALUploadHeader::parse(runs_on, false).has_value()); } - template<> template<> - void aluploadheader_object::test<5>() + TEST_CASE_METHOD(aluploadheader_data, "aluploadheader: the hash is XXH128 of v1 and the target and source and code split by noughts", "[aluploadheader]") { - set_test_name("the hash is XXH128 over v1, the target, the source and the code, each after a nought, as xxhsum writes it"); using namespace std::string_literals; const std::string hash = ALUploadHeader::hashOf("mono", "#define X 1\nX", "1"); - ensure_equals("xxHash's own", hash, aluploadheader_data::xxhsum("v1\0mono\0#define X 1\nX\0001"s)); - ensure_equals("its scheme and 32 digits", hash.size(), std::string("xxh128:").size() + 32); - ensure_equals("plain: the text alone", ALUploadHeader::hashOfPlain("luau", "print(1)"), + UNSCOPED_INFO("xxHash's own"); + CHECK(hash == aluploadheader_data::xxhsum("v1\0mono\0#define X 1\nX\0001"s)); + UNSCOPED_INFO("its scheme and 32 digits"); + CHECK(hash.size() == std::string("xxh128:").size() + 32); + UNSCOPED_INFO("plain: the text alone"); + CHECK(ALUploadHeader::hashOfPlain("luau", "print(1)") == aluploadheader_data::xxhsum("v1\0luau\0print(1)"s)); - ensure_equals("no target", ALUploadHeader::hashOfPlain("", "x"), aluploadheader_data::xxhsum("v1\0\0x"s)); - - ensure("the same, the same", hash == ALUploadHeader::hashOf("mono", "#define X 1\nX", "1")); - ensure("another target", hash != ALUploadHeader::hashOf("lsl2", "#define X 1\nX", "1")); - ensure("another source", hash != ALUploadHeader::hashOf("mono", "#define X 2\nX", "1")); - ensure("other code", hash != ALUploadHeader::hashOf("mono", "#define X 1\nX", "2")); - ensure("where the source ends", ALUploadHeader::hashOf("mono", "ab", "c") != ALUploadHeader::hashOf("mono", "a", "bc")); - ensure("wrapped with nothing expanded is not plain", ALUploadHeader::hashOf("mono", "x", "") != ALUploadHeader::hashOfPlain("mono", "x")); + UNSCOPED_INFO("no target"); + CHECK(ALUploadHeader::hashOfPlain("", "x") == aluploadheader_data::xxhsum("v1\0\0x"s)); + + UNSCOPED_INFO("the same, the same"); + CHECK(hash == ALUploadHeader::hashOf("mono", "#define X 1\nX", "1")); + UNSCOPED_INFO("another target"); + CHECK(hash != ALUploadHeader::hashOf("lsl2", "#define X 1\nX", "1")); + UNSCOPED_INFO("another source"); + CHECK(hash != ALUploadHeader::hashOf("mono", "#define X 2\nX", "1")); + UNSCOPED_INFO("other code"); + CHECK(hash != ALUploadHeader::hashOf("mono", "#define X 1\nX", "2")); + UNSCOPED_INFO("where the source ends"); + CHECK(ALUploadHeader::hashOf("mono", "ab", "c") != ALUploadHeader::hashOf("mono", "a", "bc")); + UNSCOPED_INFO("wrapped with nothing expanded is not plain"); + CHECK(ALUploadHeader::hashOf("mono", "x", "") != ALUploadHeader::hashOfPlain("mono", "x")); } - template<> template<> - void aluploadheader_object::test<6>() + TEST_CASE_METHOD(aluploadheader_data, "aluploadheader: a date as @date writes it in UTC; and what is absolute", "[aluploadheader]") { - set_test_name("a date as @date writes it, in UTC; and what is absolute"); - ensure_equals("the epoch", ALUploadHeader::dateOf(LLDate(0.0)), std::string("1970-01-01 00:00:00")); - ensure_equals("a day", ALUploadHeader::dateOf(LLDate(1791381791.0)), std::string("2026-10-07 14:03:11")); - ensure_equals("a leap day's last second", ALUploadHeader::dateOf(LLDate(951868799.5)), std::string("2000-02-29 23:59:59")); + UNSCOPED_INFO("the epoch"); + CHECK(ALUploadHeader::dateOf(LLDate(0.0)) == std::string("1970-01-01 00:00:00")); + UNSCOPED_INFO("a day"); + CHECK(ALUploadHeader::dateOf(LLDate(1791381791.0)) == std::string("2026-10-07 14:03:11")); + UNSCOPED_INFO("a leap day's last second"); + CHECK(ALUploadHeader::dateOf(LLDate(951868799.5)) == std::string("2000-02-29 23:59:59")); for (const char* path : { "/a", "\\a", "\\\\host\\a", "C:\\a", "c:/a", "z:a", "~/a", "~rye/a" }) { - ensure(std::string("absolute: ") + path, ALUploadHeader::absolute(path)); + CAPTURE(path); + REQUIRE(ALUploadHeader::absolute(path)); } for (const char* path : { "", "a", "net/door.lsl", "../shared/util.luau", "@lib/util.luau", "1:x", ":a" }) { - ensure(std::string("not: ") + path, !ALUploadHeader::absolute(path)); + CAPTURE(path); + REQUIRE_FALSE(ALUploadHeader::absolute(path)); } } } diff --git a/indra/cmake/ALAddTestBinary.cmake b/indra/cmake/ALAddTestBinary.cmake index 15cd931401..ac294aa44c 100644 --- a/indra/cmake/ALAddTestBinary.cmake +++ b/indra/cmake/ALAddTestBinary.cmake @@ -13,6 +13,7 @@ endif() # [LIBRARIES ...] [SUPPORT ...] # [INCLUDES ...] [DEFINES ...] [ENVIRONMENT ...] # [GL] [PYTHON] [ISA_TIER ] [RESOURCE_LOCK ...] [LAUNCHER ...] +# [RERUN RERUN_SOURCES ... RERUN_ENVIRONMENT ...] # [COMBINED]) # # Builds the test files into one Catch2 executable, , with the harness @@ -30,17 +31,18 @@ endif() # own, since every other test links the code it tests. # # Each test file is a CTest entry, /, that runs the file's -# test cases -- chosen by the tag Catch2's --filenames-as-tags gives each case -# from the name of its file -- in one process, as an executable to a test file -# ran them. Naming a tag would also choose the file's hidden cases, such as its -# benchmarks, so the filter leaves those out, as a run with no filter does. A -# filter that matches nothing exits 2, so a file whose test cases went missing -# fails rather than passing empty. A stem names one file in a binary, and a -# file belongs to one binary, or to one at each ISA tier. The tag is the -# file's name less its last extension, the stem here too. A case written in a -# header, or in a SUPPORT library, is tagged with that file's name and no -# file's entry would run it, so /stray_cases fails when the binary has -# one: it runs whatever no file's tag names, and expects nothing to run. +# test cases -- chosen by the tag the main gives each case from the name of +# its file, as Catch2's --filenames-as-tags does -- in one process, as an +# executable to a test file ran them. Naming a tag would also choose the +# file's hidden cases, such as its benchmarks, so the filter leaves those out, +# as a run with no filter does. A filter that matches nothing exits 2, so a +# file whose test cases went missing fails rather than passing empty. A stem +# names one file in a binary, and a file belongs to one binary, or to one at +# each ISA tier. The tag is the file's name less its last extension, the stem +# here too. A case written in a header, or in a SUPPORT library, is tagged +# with that file's name and no file's entry would run it, so +# /stray_cases fails when the binary has one: it runs whatever no file's +# tag names, and expects nothing to run. # # /combined runs all of them in one process. It is labelled # combined_pending and manual, which the default run leaves out, until the @@ -61,13 +63,19 @@ endif() # a command each entry runs the binary through, its path and arguments after # the command's own: llcorehttp's tests run under the Python peer that serves # their requests. A per-case entry, and its discovery, go through it too. +# +# RERUN runs some of the files a second time, each in a process of its own, +# with RERUN_ENVIRONMENT added to the binary's: _/, or +# with AL_TEST_PER_CASE each of their cases as _/. alscript's +# tests that check SLua run again under Luau's new type solver, as a scripter +# may choose it. function(al_add_test_binary name) cmake_parse_arguments( PARSE_ARGV 1 arg "GL;PYTHON;COMBINED" - "PROJECT;ISA_TIER" - "SOURCES;UNIT;LIBRARIES;SUPPORT;INCLUDES;DEFINES;ENVIRONMENT;RESOURCE_LOCK;LAUNCHER" + "PROJECT;ISA_TIER;RERUN" + "SOURCES;UNIT;LIBRARIES;SUPPORT;INCLUDES;DEFINES;ENVIRONMENT;RESOURCE_LOCK;LAUNCHER;RERUN_SOURCES;RERUN_ENVIRONMENT" ) if(NOT arg_PROJECT) message(FATAL_ERROR "al_add_test_binary(${name}): PROJECT is required") @@ -81,6 +89,12 @@ function(al_add_test_binary name) "al_add_test_binary(${name}): unexpected arguments: ${arg_UNPARSED_ARGUMENTS}" ) endif() + if(arg_RERUN_SOURCES AND NOT arg_RERUN) + message(FATAL_ERROR "al_add_test_binary(${name}): RERUN_SOURCES needs RERUN to name the run") + endif() + if(arg_RERUN AND NOT arg_RERUN_SOURCES) + message(FATAL_ERROR "al_add_test_binary(${name}): RERUN ${arg_RERUN} names no RERUN_SOURCES") + endif() set(target ${name}) if(arg_ISA_TIER) if(NOT arg_ISA_TIER MATCHES "^(baseline|v2|v3|v4)$") @@ -116,6 +130,17 @@ function(al_add_test_binary name) endif() set_property(GLOBAL PROPERTY AL_TEST_SOURCE_${key} ${target}) endforeach() + set(rerun_stems) + foreach(source IN LISTS arg_RERUN_SOURCES) + get_filename_component(stem "${source}" NAME_WLE) + if(NOT stem IN_LIST stems) + message( + FATAL_ERROR + "al_add_test_binary(${name}): RERUN_SOURCES' ${source} is not one of its SOURCES" + ) + endif() + list(APPEND rerun_stems ${stem}) + endforeach() # vcpkg's toolchain hangs an app-local copy step on every executable it # sees declared; a test runs from the build tree, as al_add_test explains. @@ -182,6 +207,13 @@ function(al_add_test_binary name) endif() endif() + # The runs, each a prefix to its entries' names: the binary's own, and + # RERUN's of its files. + set(prefixes ${target}) + if(arg_RERUN) + list(APPEND prefixes ${target}_${arg_RERUN}) + endif() + if(AL_TEST_PER_CASE) # Beside Catch2's package config, where vcpkg puts it, and not on the # module path. @@ -202,67 +234,91 @@ function(al_add_test_binary name) if(arg_LAUNCHER) set_target_properties(${target} PROPERTIES CROSSCOMPILING_EMULATOR "${arg_LAUNCHER}") endif() - # The main makes Catch2's "all skipped" CTest's 125, so a skip is not - # left to the discovery's own code for it. - catch_discover_tests( - ${target} - TEST_PREFIX "${target}/" - TEST_LIST ${target}_cases - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - EXTRA_ARGS ${arguments} - DISCOVERY_MODE PRE_TEST - SKIP_IS_FAILURE - DL_PATHS ${SHARED_LIB_STAGING_DIR} - PROPERTIES ${per_case_properties} - ) - # The environment, labels and lock a file's entry has are lists, which - # catch_discover_tests flattens with the rest of its PROPERTIES, and a - # CTest script cannot append to a property: a script run after the - # discovery sets them on each case whole. The labels are a file entry's, - # not the cases' Catch2 tags, so that -L and -LE choose alike either way. - # The library path is DL_PATHS', and not AL_TEST_ENVIRONMENT's, whose - # generator expressions a script written here would leave unevaluated. - set(case_script "${CMAKE_CURRENT_BINARY_DIR}/${target}_cases.cmake") - set(case_properties "LABELS [==[${labels}]==]") - if(environment) - string(APPEND case_properties " ENVIRONMENT [==[${environment}]==]") - endif() - if(arg_RESOURCE_LOCK) - string(APPEND case_properties " RESOURCE_LOCK [==[${arg_RESOURCE_LOCK}]==]") - endif() - # The list holds each name as Catch2 escaped it for a list: [ and ] as - # the bytes 2 and 3. The test is registered under the name itself. - file( - WRITE "${case_script}" - "string(ASCII 2 open)\n" - "string(ASCII 3 close)\n" - "foreach(case IN LISTS ${target}_cases)\n" - " string(REPLACE \"\${open}\" \"[\" case \"\${case}\")\n" - " string(REPLACE \"\${close}\" \"]\" case \"\${case}\")\n" - " set_tests_properties(\"\${case}\" PROPERTIES ${case_properties})\n" - "endforeach()\n" - ) - set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES "${case_script}") - else() - foreach(stem IN LISTS stems) - add_test( - NAME ${target}/${stem} - COMMAND ${arg_LAUNCHER} $ --filenames-as-tags "[#${stem}]~[.]" ${arguments} + foreach(prefix IN LISTS prefixes) + # A rerun's cases are its files', by the tag each file's name gives + # them, its hidden ones left out as the binary's own run leaves them. + set(spec) + set(run_environment ${environment}) + if(NOT prefix STREQUAL target) + set(files) + foreach(stem IN LISTS rerun_stems) + list(APPEND files "[#${stem}]~[.]") + endforeach() + list(JOIN files "," files) + set(spec "${files}") + list(APPEND run_environment ${arg_RERUN_ENVIRONMENT}) + endif() + # The main makes Catch2's "all skipped" CTest's 125, so a skip is not + # left to the discovery's own code for it. + catch_discover_tests( + ${target} + TEST_PREFIX "${prefix}/" + TEST_LIST ${prefix}_cases + TEST_SPEC ${spec} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + EXTRA_ARGS ${arguments} + DISCOVERY_MODE PRE_TEST + SKIP_IS_FAILURE + DL_PATHS ${SHARED_LIB_STAGING_DIR} + PROPERTIES ${per_case_properties} ) - set_tests_properties( - ${target}/${stem} - PROPERTIES - ENVIRONMENT "${environment}" - ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" - LABELS "${labels}" - DISABLED ${disabled} - SKIP_RETURN_CODE 125 - TIMEOUT ${timeout} - ) + # The environment, labels and lock a file's entry has are lists, which + # catch_discover_tests flattens with the rest of its PROPERTIES, and a + # CTest script cannot append to a property: a script run after the + # discovery sets them on each case whole. The labels are a file entry's, + # not the cases' Catch2 tags, so that -L and -LE choose alike either way. + # The library path is DL_PATHS', and not AL_TEST_ENVIRONMENT's, whose + # generator expressions a script written here would leave unevaluated. + set(case_script "${CMAKE_CURRENT_BINARY_DIR}/${prefix}_cases.cmake") + set(case_properties "LABELS [==[${labels}]==]") + if(run_environment) + string(APPEND case_properties " ENVIRONMENT [==[${run_environment}]==]") + endif() if(arg_RESOURCE_LOCK) - set_tests_properties(${target}/${stem} PROPERTIES RESOURCE_LOCK "${arg_RESOURCE_LOCK}") + string(APPEND case_properties " RESOURCE_LOCK [==[${arg_RESOURCE_LOCK}]==]") + endif() + # The list holds each name as Catch2 escaped it for a list: [ and ] as + # the bytes 2 and 3. The test is registered under the name itself. + file( + WRITE "${case_script}" + "string(ASCII 2 open)\n" + "string(ASCII 3 close)\n" + "foreach(case IN LISTS ${prefix}_cases)\n" + " string(REPLACE \"\${open}\" \"[\" case \"\${case}\")\n" + " string(REPLACE \"\${close}\" \"]\" case \"\${case}\")\n" + " set_tests_properties(\"\${case}\" PROPERTIES ${case_properties})\n" + "endforeach()\n" + ) + set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES "${case_script}") + endforeach() + else() + foreach(prefix IN LISTS prefixes) + set(run_stems ${stems}) + set(run_environment ${environment}) + if(NOT prefix STREQUAL target) + set(run_stems ${rerun_stems}) + list(APPEND run_environment ${arg_RERUN_ENVIRONMENT}) endif() + foreach(stem IN LISTS run_stems) + add_test( + NAME ${prefix}/${stem} + COMMAND ${arg_LAUNCHER} $ "[#${stem}]~[.]" ${arguments} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties( + ${prefix}/${stem} + PROPERTIES + ENVIRONMENT "${run_environment}" + ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" + LABELS "${labels}" + DISABLED ${disabled} + SKIP_RETURN_CODE 125 + TIMEOUT ${timeout} + ) + if(arg_RESOURCE_LOCK) + set_tests_properties(${prefix}/${stem} PROPERTIES RESOURCE_LOCK "${arg_RESOURCE_LOCK}") + endif() + endforeach() endforeach() # Every case no file's tag names: a filter of nothing but exclusions @@ -276,8 +332,8 @@ function(al_add_test_binary name) add_test( NAME ${target}/stray_cases COMMAND - ${CMAKE_COMMAND} "-DEXE=$" "-DARGS=--filenames-as-tags|${strays}~[.]" - -DEXPECT_EXIT=2 -P ${INDRA_SOURCE_DIR}/test/run_and_expect.cmake + ${CMAKE_COMMAND} "-DEXE=$" "-DARGS=${strays}~[.]" -DEXPECT_EXIT=2 -P + ${INDRA_SOURCE_DIR}/test/run_and_expect.cmake WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) set_tests_properties( diff --git a/indra/lscript/CMakeLists.txt b/indra/lscript/CMakeLists.txt index 740199fe1d..78f731c67f 100755 --- a/indra/lscript/CMakeLists.txt +++ b/indra/lscript/CMakeLists.txt @@ -172,17 +172,17 @@ endif() set_source_files_properties(${lscript_sources} PROPERTIES COMPILE_OPTIONS "${lscript_ieee_options}") # tests -# LL's compiler over a handful of scripts, each to LSO and to CIL. -al_add_test(allslreference PROJECT lscript LIBRARIES lscript llcommon) +# LL's compiler over a handful of scripts, each to LSO and to CIL; and # Tailslide's LSO images and CIL, and the Mono weigher, held against LL's -# compiler's over Tailslide's test scripts. -al_add_test( - allslreferencecheck +# compiler's over Tailslide's test scripts. One Catch2 binary, each file run +# in a process of its own. +al_add_test_binary( + lscript_tests PROJECT lscript + SOURCES tests/allslreference_test.cpp tests/allslreferencecheck_test.cpp + SUPPORT alscript_test_checks llcommon_test_checks LIBRARIES lscript alscript llcommon DEFINES "AL_LSL_DEFINITIONS_DIR=\"${LSL_DEFINITIONS_DIR}\"" "AL_ALSCRIPT_TEST_DIR=\"${INDRA_SOURCE_DIR}/alscript/tests\"" ) - - diff --git a/indra/lscript/tests/allslreference_test.cpp b/indra/lscript/tests/allslreference_test.cpp index 62ecb0b9b2..5b7ab80631 100644 --- a/indra/lscript/tests/allslreference_test.cpp +++ b/indra/lscript/tests/allslreference_test.cpp @@ -30,7 +30,7 @@ #include "../lscript_library.h" #include "lltimer.h" -#include "../test/lltut.h" +#include #include @@ -93,20 +93,10 @@ namespace constexpr size_t NER = 92; } -namespace tut +namespace { - struct allslreference_data - { - }; - - typedef test_group allslreference_group; - typedef allslreference_group::object allslreference_object; - allslreference_group allslreference_instance("allslreference"); - - template<> template<> - void allslreference_object::test<1>() + TEST_CASE("allslreference: a script compiles to a whole LSO image and to CIL named from its id", "[allslreference]") { - set_test_name("a script compiles to a whole LSO image, and to CIL whose class is named from the script's id"); const std::string text = "integer g = 3;\n" "default\n" "{\n" @@ -116,26 +106,35 @@ namespace tut " }\n" "}\n"; const ALLSLReference::Result lso = ALLSLReference::compile(text, Target::LSO); - ensure("LSO: " + lso.messages, lso.ok); - ensure_equals("the image fills LSO's memory", lso.image.size(), (size_t)16384); - ensure_equals("top of memory", reg32(lso.image, TM), 16384); - ensure_equals("LSO2", reg32(lso.image, VN), 0x0200); - ensure_equals("the default state handles state_entry", reg64(lso.image, NER), (U64)0x1); + UNSCOPED_INFO("LSO: " + lso.messages); + CHECK(lso.ok); + UNSCOPED_INFO("the image fills LSO's memory"); + REQUIRE(lso.image.size() == (size_t)16384); + UNSCOPED_INFO("top of memory"); + CHECK(reg32(lso.image, TM) == 16384); + UNSCOPED_INFO("LSO2"); + CHECK(reg32(lso.image, VN) == 0x0200); + UNSCOPED_INFO("the default state handles state_entry"); + CHECK(reg64(lso.image, NER) == (U64)0x1); // LL's compiler calls every library function by its two-byte form. - ensure("llSay is library function 23", holds(lso.image, { 0xd1, 0x00, 23 })); + UNSCOPED_INFO("llSay is library function 23"); + CHECK(holds(lso.image, { 0xd1, 0x00, 23 })); const ALLSLReference::Result cil = ALLSLReference::compile(text, Target::CIL); - ensure("CIL: " + cil.messages, cil.ok); - ensure("the class: " + cil.cil, has(cil.cil, ".class public auto ansi serializable beforefieldinit LSL_00000000_0000_0000_0000_000000000000 extends")); - ensure("the global a field: " + cil.cil, has(cil.cil, ".field public int32 'g'")); - ensure("the handler a method: " + cil.cil, has(cil.cil, "instance default void edefaultstate_entry() cil managed")); - ensure("llSay called by name: " + cil.cil, has(cil.cil, "Library::'llSay'(int32, string)")); + UNSCOPED_INFO("CIL: " + cil.messages); + CHECK(cil.ok); + UNSCOPED_INFO("the class: " + cil.cil); + CHECK(has(cil.cil, ".class public auto ansi serializable beforefieldinit LSL_00000000_0000_0000_0000_000000000000 extends")); + UNSCOPED_INFO("the global a field: " + cil.cil); + CHECK(has(cil.cil, ".field public int32 'g'")); + UNSCOPED_INFO("the handler a method: " + cil.cil); + CHECK(has(cil.cil, "instance default void edefaultstate_entry() cil managed")); + UNSCOPED_INFO("llSay called by name: " + cil.cil); + CHECK(has(cil.cil, "Library::'llSay'(int32, string)")); } - template<> template<> - void allslreference_object::test<2>() + TEST_CASE("allslreference: the grid's definitions: a post-2015 function at its number and new constants", "[allslreference]") { - set_test_name("the grid's definitions: a function newer than the 2015 table at its own number, and constants newer than the 2015 lexer"); const std::string text = "default\n" "{\n" " state_entry()\n" @@ -145,20 +144,24 @@ namespace tut " }\n" "}\n"; const ALLSLReference::Result lso = ALLSLReference::compile(text, Target::LSO); - ensure("LSO: " + lso.messages, lso.ok); - ensure("llLinksetDataWrite is library function 650", holds(lso.image, { 0xd1, 0x02, 0x8a })); + UNSCOPED_INFO("LSO: " + lso.messages); + CHECK(lso.ok); + UNSCOPED_INFO("llLinksetDataWrite is library function 650"); + CHECK(holds(lso.image, { 0xd1, 0x02, 0x8a })); const ALLSLReference::Result cil = ALLSLReference::compile(text, Target::CIL); - ensure("CIL: " + cil.messages, cil.ok); - ensure("llLinksetDataWrite: " + cil.cil, has(cil.cil, "Library::'llLinksetDataWrite'(string, string)")); - ensure("PRIM_GLTF_BASE_COLOR is 48: " + cil.cil, has(cil.cil, "ldc.i4 48")); - ensure("OBJECT_DAMAGE is 51: " + cil.cil, has(cil.cil, "ldc.i4 51")); + UNSCOPED_INFO("CIL: " + cil.messages); + CHECK(cil.ok); + UNSCOPED_INFO("llLinksetDataWrite: " + cil.cil); + CHECK(has(cil.cil, "Library::'llLinksetDataWrite'(string, string)")); + UNSCOPED_INFO("PRIM_GLTF_BASE_COLOR is 48: " + cil.cil); + CHECK(has(cil.cil, "ldc.i4 48")); + UNSCOPED_INFO("OBJECT_DAMAGE is 51: " + cil.cil); + CHECK(has(cil.cil, "ldc.i4 51")); } - template<> template<> - void allslreference_object::test<3>() + TEST_CASE("allslreference: the events new since 2015 compile and LSO's default state has each one's bit", "[allslreference]") { - set_test_name("the events new since 2015 compile, and LSO's default state carries each one's bit"); const std::string text = "default\n" "{\n" " experience_permissions(key agent) {}\n" @@ -172,24 +175,26 @@ namespace tut " final_damage(integer n) {}\n" "}\n"; const ALLSLReference::Result lso = ALLSLReference::compile(text, Target::LSO); - ensure("LSO: " + lso.messages, lso.ok); + UNSCOPED_INFO("LSO: " + lso.messages); + REQUIRE(lso.ok); // Events 35 to 43, bits 34 to 42. - ensure_equals("their bits", reg64(lso.image, NER), (U64)0x1ff << 34); + UNSCOPED_INFO("their bits"); + CHECK(reg64(lso.image, NER) == (U64)0x1ff << 34); const ALLSLReference::Result cil = ALLSLReference::compile(text, Target::CIL); - ensure("CIL: " + cil.messages, cil.ok); + UNSCOPED_INFO("CIL: " + cil.messages); + CHECK(cil.ok); for (const char* handler : { "edefaultexperience_permissions(", "edefaulttransaction_result(", "edefaultpath_update(", "edefaultexperience_permissions_denied(", "edefaultlinkset_data(", "edefaultgame_control(", "edefaulton_death(", "edefaulton_damage(", "edefaultfinal_damage(" }) { - ensure(std::string(handler) + ": " + cil.cil, has(cil.cil, handler)); + INFO(handler << ": " << cil.cil); + REQUIRE(has(cil.cil, handler)); } } - template<> template<> - void allslreference_object::test<4>() + TEST_CASE("allslreference: globals read in functions and events and remote_data compile and free cleanly", "[allslreference]") { - set_test_name("a global read in a function and an event, and a remote_data handler, compile and free cleanly"); const std::string text = "integer g = 1;\n" "integer f() { return g + 1; }\n" "default\n" @@ -200,43 +205,53 @@ namespace tut for (Target target : { Target::LSO, Target::CIL }) { const ALLSLReference::Result r = ALLSLReference::compile(text, target); - ensure("compiles: " + r.messages, r.ok); + UNSCOPED_INFO("compiles: " + r.messages); + REQUIRE(r.ok); } } - template<> template<> - void allslreference_object::test<5>() + TEST_CASE("allslreference: LL's compiler rejects bad syntax and on LSO a list or unset global in a list", "[allslreference]") { - set_test_name("what LL's compiler rejects: a syntax error; and, building LSO's image alone, a list or an unset global in a global's list"); const ALLSLReference::Result syntax = ALLSLReference::compile("default { state_entry() { integer = 1; } }\n", Target::LSO); - ensure("a syntax error: " + syntax.messages, !syntax.ok && has(syntax.messages, "ERROR : Syntax error")); + { + INFO("a syntax error: " + syntax.messages); + CHECK_FALSE(syntax.ok); + CHECK(has(syntax.messages, "ERROR : Syntax error")); + } // A global list's elements are checked as LSO's image lays them out // (LSCP_LIST_BUILD_SIMPLE), a pass the CIL never runs. const std::string nested = "list a = [1];\nlist b = [a];\ndefault { state_entry() { llOwnerSay((string)b); } }\n"; const ALLSLReference::Result nested_lso = ALLSLReference::compile(nested, Target::LSO); - ensure("LSO rejects a list in a global's list: " + nested_lso.messages, - !nested_lso.ok && has(nested_lso.messages, "Lists can't be included in lists")); + { + INFO("LSO rejects a list in a global's list: " + nested_lso.messages); + CHECK_FALSE(nested_lso.ok); + CHECK(has(nested_lso.messages, "Lists can't be included in lists")); + } const ALLSLReference::Result nested_cil = ALLSLReference::compile(nested, Target::CIL); - ensure("CIL takes it: " + nested_cil.messages, nested_cil.ok); + UNSCOPED_INFO("CIL takes it: " + nested_cil.messages); + CHECK(nested_cil.ok); const std::string unset = "string s;\nlist l = [s];\ndefault { state_entry() { llOwnerSay((string)l); } }\n"; const ALLSLReference::Result unset_lso = ALLSLReference::compile(unset, Target::LSO); - ensure("LSO rejects an unset global in a global's list: " + unset_lso.messages, - !unset_lso.ok && has(unset_lso.messages, "Unitialized variables can't be included in lists")); + { + INFO("LSO rejects an unset global in a global's list: " + unset_lso.messages); + CHECK_FALSE(unset_lso.ok); + CHECK(has(unset_lso.messages, "Unitialized variables can't be included in lists")); + } const ALLSLReference::Result unset_cil = ALLSLReference::compile(unset, Target::CIL); - ensure("CIL takes it: " + unset_cil.messages, unset_cil.ok); + UNSCOPED_INFO("CIL takes it: " + unset_cil.messages); + CHECK(unset_cil.ok); } - template<> template<> - void allslreference_object::test<6>() + TEST_CASE("allslreference: LL's VM runs compiled state_entry code with a stand-in answering llSay", "[allslreference]") { - set_test_name("LL's VM runs a compiled image's state_entry, a global, a function and a cast in it, its library call answered by a stand-in"); const std::string text = "integer g = 6;\n" "integer twice(integer n) { return n * 2; }\n" "default { state_entry() { llSay(0, (string)(twice(g) * 3 + 6)); } }\n"; const ALLSLReference::Result lso = ALLSLReference::compile(text, Target::LSO); - ensure("LSO: " + lso.messages, lso.ok); + UNSCOPED_INFO("LSO: " + lso.messages); + REQUIRE(lso.ok); // llSay is function 23: heard here for the run, as it was after. gSaid.clear(); @@ -251,9 +266,9 @@ namespace tut vm.runQuanta(FALSE, LLUUID::null, &error, 1.f, events, timer); } } - ensure("no fault: " + std::string(error ? error : ""), !error); - ensure_equals("one handler run", events, (U32)1); - ensure_equals("said once", gSaid.size(), (size_t)1); - ensure_equals("what it worked out", gSaid[0], std::string("42")); + CHECK_FALSE(error); + CHECK(events == (U32)1); + REQUIRE(gSaid.size() == (size_t)1); + CHECK(gSaid[0] == std::string("42")); } } diff --git a/indra/lscript/tests/allslreferencecheck_test.cpp b/indra/lscript/tests/allslreferencecheck_test.cpp index 1f9aa1baf1..2ed9ed57c8 100644 --- a/indra/lscript/tests/allslreferencecheck_test.cpp +++ b/indra/lscript/tests/allslreferencecheck_test.cpp @@ -35,7 +35,7 @@ #include "alscriptweight.h" #include "llfile.h" -#include "../test/lltut.h" +#include #include #include @@ -224,7 +224,7 @@ namespace } } -namespace tut +namespace { struct allslreferencecheck_data { @@ -233,15 +233,11 @@ namespace tut std::string error; allslreferencecheck_data() { loaded = lsl.loadBuiltins(std::string(AL_LSL_DEFINITIONS_DIR) + "/builtins.txt", error); } }; - typedef test_group allslreferencecheck_group; - typedef allslreferencecheck_group::object allslreferencecheck_object; - allslreferencecheck_group allslreferencecheck_instance("allslreferencecheck"); - template<> template<> - void allslreferencecheck_object::test<1>() + TEST_CASE_METHOD(allslreferencecheck_data, "allslreferencecheck: LSO: Tailslide's image is LL's byte for byte; and what each refuses", "[allslreferencecheck]") { - set_test_name("LSO: Tailslide's image is LL's byte for byte; and what each refuses"); - ensure("builtins: " + error, loaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(loaded); // None, since the port's bitstream-move-to-end.patch: a bit stream // moved to its end grew a byte past it, which a last global left all // zeros kept, and all after it moved. @@ -282,17 +278,19 @@ namespace tut } where += name + " from byte " + std::to_string(at) + ", in its " + regionOf(ll.image, at) + "\n"; } - ensure("most of them: " + std::to_string(same), same >= 75); - ensure("the images that part: " + listed(parted) + "\n" + where, parted == PARTED); - ensure("LL's compiles alone: " + listed(only_ll), only_ll == ONLY_LL); - ensure("Tailslide compiles alone: " + listed(only_tailslide), only_tailslide == ONLY_TAILSLIDE); + CHECK(same >= 75); + UNSCOPED_INFO("the images that part: " + listed(parted) + "\n" + where); + CHECK(parted == PARTED); + UNSCOPED_INFO("LL's compiles alone: " + listed(only_ll)); + CHECK(only_ll == ONLY_LL); + UNSCOPED_INFO("Tailslide compiles alone: " + listed(only_tailslide)); + CHECK(only_tailslide == ONLY_TAILSLIDE); } - template<> template<> - void allslreferencecheck_object::test<2>() + TEST_CASE_METHOD(allslreferencecheck_data, "allslreferencecheck: CIL: Tailslide's matches LL's method for method up to spelling", "[allslreferencecheck]") { - set_test_name("CIL: Tailslide's is LL's, method for method, but for spelling an assembler reads alike"); - ensure("builtins: " + error, loaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(loaded); // What only LSO's image building refuses: a list in a global's list. // Its constants are LL's since the port's mono-constants-as-ll.patch: // an integer literal by ldc.i4 alone, and a minus before a number in @@ -331,17 +329,19 @@ namespace tut where += name + ": [" + (at < written.size() ? written[at] : "") + "] against LL's [" + (at < theirs.size() ? theirs[at] : "") + "]\n"; } } - ensure("most of them: " + std::to_string(both), both >= 75); - ensure("the same as LL's:\n" + where, parted.empty()); - ensure("LL's compiles alone: " + listed(only_ll), only_ll == ONLY_LL); - ensure("Tailslide compiles alone: " + listed(only_tailslide), only_tailslide == ONLY_TAILSLIDE); + CHECK(both >= 75); + UNSCOPED_INFO("the same as LL's:\n" + where); + CHECK(parted.empty()); + UNSCOPED_INFO("LL's compiles alone: " + listed(only_ll)); + CHECK(only_ll == ONLY_LL); + UNSCOPED_INFO("Tailslide compiles alone: " + listed(only_tailslide)); + CHECK(only_tailslide == ONLY_TAILSLIDE); } - template<> template<> - void allslreferencecheck_object::test<3>() + TEST_CASE_METHOD(allslreferencecheck_data, "allslreferencecheck: Mono: the weigher reads either compiler's CIL alike and both weigh the same", "[allslreferencecheck]") { - set_test_name("Mono: the weigher reads either compiler's CIL alike, and both weigh the same"); - ensure("builtins: " + error, loaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(loaded); std::string wrong; size_t both = 0; for (const auto& [name, text] : corpus()) @@ -368,15 +368,15 @@ namespace tut wrong += name + ": LL's weighs " + std::to_string(theirs.total) + ", Tailslide's " + std::to_string(tailslide.total) + "\n"; } } - ensure("most of them: " + std::to_string(both), both >= 75); - ensure(wrong, wrong.empty()); + CHECK(both >= 75); + UNSCOPED_INFO(wrong); + CHECK(wrong.empty()); } - template<> template<> - void allslreferencecheck_object::test<4>() + TEST_CASE_METHOD(allslreferencecheck_data, "allslreferencecheck: integer literals past 32 bits read -1 as on 32-bit hosts; both make one image", "[allslreferencecheck]") { - set_test_name("integer literals past 32 bits as the grid's 32-bit hosts read them, -1, decimal or hexadecimal: the same image from either"); - ensure("builtins: " + error, loaded); + UNSCOPED_INFO("builtins: " + error); + CHECK(loaded); const std::string text = "integer a = 4294967296;\n" "integer b = 0x100000000;\n" "integer c = 99999999999999999999999;\n" @@ -384,9 +384,10 @@ namespace tut "default { state_entry() { llOwnerSay((string)(a + b + c + d + 4294967297 - 0x1FFFFFFFF)); } }\n"; std::vector ours; std::string why; - ensure("Tailslide compiles it: " + why, ALScriptWeigh::tailslideLSO(text, ours, why)); + UNSCOPED_INFO("Tailslide compiles it: " + why); + CHECK(ALScriptWeigh::tailslideLSO(text, ours, why)); const ALLSLReference::Result ll = ALLSLReference::compile(text, Target::LSO); - ensure("LL's compiles it", ll.ok); - ensure("the same image", ours == ll.image); + CHECK(ll.ok); + CHECK(ours == ll.image); } } diff --git a/indra/test/altestmain.cpp b/indra/test/altestmain.cpp index 7869441d52..849c67cd37 100644 --- a/indra/test/altestmain.cpp +++ b/indra/test/altestmain.cpp @@ -145,6 +145,10 @@ int main(int argc, char* argv[]) // Declared order, as TUT ran a group's tests, until no test leans on the // ones before it. --order on the command line still wins. session.configData().runOrder = Catch::TestRunOrder::Declared; + // Every case tagged with its file's name, [#], as + // --filenames-as-tags tags them: a test file's run names its file with + // one tag, and a filter of several files is one argument. + session.configData().filenamesAsTags = true; if (const int result = session.applyCommandLine(argc, argv); result != 0) { diff --git a/scripts/testing/tut_parity.py b/scripts/testing/tut_parity.py index 9342074861..3067150886 100644 --- a/scripts/testing/tut_parity.py +++ b/scripts/testing/tut_parity.py @@ -15,7 +15,10 @@ under a Python peer), as check finds a Catch2 entry's binary. A test built at other x86-64 tiers too (an entry ending _baseline, _v2, _v3, _v4) is - kept a tier apart, under @. + kept a tier apart, under @; so is a second run of a + test's executable under another environment, an entry named for + the executable's test, an underscore and the run (alscript's + _newsolver), under @. check after converting and building: runs each converted file's CTest entry, /, as XML, and matches the cases to the TUT results through tut2catch.py's mapping. tut2catch.py must be @@ -25,6 +28,8 @@ tier; the tree's own binary is held to TUT's untiered run (or, where there is none, its run at the tree's tier, AL_ISA_TIER). A tier the host does not run skips on both sides, which is listed. + A file run a second time has an entry _/ + (al_add_test_binary's RERUN), held to TUT's run of that name. Parity holds for a file when: - its inputs are the ones the run is of: the converted file is no newer @@ -73,7 +78,8 @@ command, one under an #if that never ran is listed. So is one --unrun names, reviewed as never running in a passing case on this host: in a loop that should not turn, or a branch of an if constexpr the build - discards; + discards -- or, named for one run of the file, in a branch only the + other runs take; A test the mapping says TUT never ran (past its group's limit) or this host does not compile is listed, not counted against parity, as is an assertion @@ -183,13 +189,25 @@ def isa_tier(name): return m.group(1) if m else None -def tut_identity(name): - """The test file a TUT CTest entry runs, by stem, and its ISA tier.""" +def tut_identity(name, command=()): + """The test file a TUT CTest entry runs, by stem, and which of its runs + the entry is: its ISA tier, or the name of a second run of the test's + executable under another environment -- the entry is named for the + executable's test, an underscore and the run, which the command, where + given, tells from a test whose own name has an underscore. None for the + test's own run.""" m = re.search(r"TEST_RUNNER_(\w+)$", name) or re.search(r"_TEST_(\w+)$", name) if not m: return name, None - tier = isa_tier(m.group(1)) - return (m.group(1)[:-len(tier) - 1] if tier else m.group(1)) + "_test", tier + test = m.group(1) + tier = isa_tier(test) + if tier: + return test[:-len(tier) - 1] + "_test", tier + for word in command: + runs = re.search(r"_TEST_(\w+)$", os.path.splitext(os.path.basename(word))[0]) + if runs and test.startswith(runs.group(1) + "_"): + return runs.group(1) + "_test", test[len(runs.group(1)) + 1:] + return test + "_test", None def executable_in(command, target): @@ -237,7 +255,8 @@ def record(args): if test is None: print(f"{name}: no such CTest entry", file=sys.stderr) return 2 - stem, tier = tut_identity(name) + stem, variant = tut_identity(name, test["command"]) + tier = variant if variant in TIERS else None # What the results are of, so that check can tell they are of the # source the mapping was made from. source, digest = tut_source(stem) @@ -263,9 +282,11 @@ def record(args): "result": {"ok": "ok", "skipped known failure": "skip"}.get(result, "fail"), "said": line[m.start("result"):].strip()}) # Each tier's run is kept apart: one the host cannot run exits 125 - # with no results, which must not stand for the others. - out[f"{stem}@{tier}" if tier else stem] = {"ctest": name, "tier": tier, "exit": code, "tut_file": source, - "tut_sha256": digest, "results": results} + # with no results, which must not stand for the others. So is each + # second run, whose results are of its own environment. + out[f"{stem}@{variant}" if variant else stem] = {"ctest": name, "stem": stem, "variant": variant, + "tier": tier, "exit": code, "tut_file": source, + "tut_sha256": digest, "results": results} counts = {} for r in results: counts[r["result"]] = counts.get(r["result"], 0) + 1 @@ -561,6 +582,22 @@ def read_mapping(path): return mapping["tests"], {record["file"]: record for record in mapping.get("files", [])} +def catch2_variant(binary, command): + """Which run of a file a Catch2 entry under / is: the ISA tier + the binary's name ends in, or, where the name is the binary's own + executable's, an underscore and more, a second run of it under another + environment (al_add_test_binary's RERUN), that run's name. None for the + file's own run.""" + tier = isa_tier(binary) + if tier: + return tier + for word in command: + target = os.path.splitext(os.path.basename(word))[0] + if binary.startswith(target + "_") and os.path.isfile(word): + return binary[len(target) + 1:] + return None + + def tree_tier(build_dir): """The ISA tier the build tree is built for: AL_ISA_TIER in its cache.""" try: @@ -580,7 +617,11 @@ def check_run(args, label, path, entry_name, test, recorded, entries, file_cases first_problem = len(problems) # A converted file changed since its binary was built is not what the # binary runs: its cases, lines and counts would not be the run's. - executable = executable_in(test["command"], entry_name.split("/", 1)[0]) + binary = entry_name.split("/", 1)[0] + variant = catch2_variant(binary, test["command"]) + if variant and variant not in TIERS: + binary = binary[:-len(variant) - 1] + executable = executable_in(test["command"], binary) if executable and os.path.isfile(executable) and \ os.path.getmtime(os.path.join(ROOT, path)) > os.path.getmtime(executable): problems.append(f"{label}: changed since {executable} was built; build it again to check it") @@ -652,7 +693,9 @@ def compiled(first, last, conditional): continue here = compiled(first, last, bool(blocks - body["held"])) if here: - why = args.expect_unrun.get(path, {}).get(str(first)) + # Reviewed for this run of the file, or for every run. + why = args.expect_unrun.get(label, {}).get(str(first)) or \ + args.expect_unrun.get(path, {}).get(str(first)) if why: notes.append(f"{case['name']!r}: the {macro} on line {first} never ran, as reviewed: {why}") else: @@ -755,12 +798,16 @@ def check(args): args.expect_unrun = json.load(f) with open(args.tut) as f: tut = json.load(f) - # By stem and tier, the tier read from each record's CTest name: results - # recorded before they were kept by tier, the last tier's under the stem, - # still say which tier they are of. + # By stem and run, which each record names; one recorded before records + # did has its tier read from its CTest name: results recorded before they + # were kept by tier, the last tier's under the stem, still say which tier + # they are of. records = {} for key, recorded in tut.items(): - records[tut_identity(recorded["ctest"]) if recorded.get("ctest") else (key, None)] = recorded + if "variant" in recorded: + records[(recorded["stem"], recorded["variant"])] = recorded + else: + records[tut_identity(recorded["ctest"]) if recorded.get("ctest") else (key, None)] = recorded own_tier = tree_tier(args.build_dir) tests = ctest_tests(args.build_dir, args.config) compiled_lines = CompiledLines(args.build_dir, args.config) @@ -810,16 +857,17 @@ def check(args): # is built for: one at the tree's own, checked against TUT's # untiered entry (or, where there is none, its entry at the tree's # tier), and at most one, _, at each other tier, - # checked against TUT's entry at that tier. + # checked against TUT's entry at that tier. A second run of the + # binary, _, is checked against TUT's run of that name. runs = {} for name in tests: if name.endswith("/" + stem): - runs.setdefault(isa_tier(name[:-len(stem) - 1]), []).append(name) + runs.setdefault(catch2_variant(name[:-len(stem) - 1], tests[name]["command"]), []).append(name) if len(runs.get(None, [])) != 1: file_problems.append(f"{path}: {len(runs.get(None, []))} Catch2 CTest entries end in /{stem}") used = set() file_rows = [] - for tier in sorted(runs, key=lambda t: TIERS.index(t) + 1 if t else 0): + for tier in sorted(runs, key=lambda t: (t in TIERS, TIERS.index(t) if t in TIERS else 0, t or "")): label = f"{path} ({tier})" if tier else path if len(runs[tier]) != 1: if tier: @@ -829,7 +877,8 @@ def check(args): if tier is None and key not in records and (stem, own_tier) in records: key = (stem, own_tier) if key not in records: - file_problems.append(f"{label}: no TUT results recorded for {stem}" + (f" at {tier}" if tier else "")) + file_problems.append(f"{label}: no TUT results recorded for {stem}" + (f" at {tier}" if tier in TIERS else + f" in its {tier} run" if tier else "")) continue used.add(key) row = check_run(args, label, path, runs[tier][0], tests[runs[tier][0]], records[key], entries, @@ -843,7 +892,8 @@ def check(args): print(f"{path}: listed: TUT's run at {own_tier}, the tree's own tier: {runs[None][0]} runs that " f"tier, and was checked against TUT's untiered run") else: - file_problems.append(f"{path}: TUT ran {stem} " + (f"at {key[1]}" if key[1] else "untiered") + file_problems.append(f"{path}: TUT ran {stem} " + (f"at {key[1]}" if key[1] in TIERS else + f"in its {key[1]} run" if key[1] else "untiered") + f" ({records[key]['ctest']}), and no Catch2 entry does") problems.extend(file_problems) for row in file_rows: @@ -875,7 +925,9 @@ def main(): help="the files were edited by hand since conversion: list each case's assertion count " "rather than hold it to the mapping's (names and results still must match)") chk.add_argument("--unrun", help="JSON of assertion sites reviewed as never running in a passing case on " - "this host, {file: {line: why}}: each is listed rather than a mismatch") + "this host, {file: {line: why}}: each is listed rather than a mismatch. A key may name " + "one run of a file, as the table does (\" (newsolver)\"), for a site only that run " + "leaves out") chk.add_argument("files", nargs="*", help="only these converted files (default: all the mapping has)") args = parser.parse_args() return record(args) if args.step == "record" else check(args)