diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b4ca99cd5f..87b2aeb1c6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -514,6 +514,39 @@ jobs: esac fi + # Each Catch2 binary's run of all its tests in one process, while it is + # still combined_pending: the default run leaves those out until they + # are known to share a process, and this is where that is learned, on + # every row. It reports, and never fails the job. The runs go side by + # side, so the step takes about as long as the longest, which CTest + # stops at an hour; the step's own limit is past that, so a hang is + # recorded as CTest's timeout and reaches the JUnit report. + - name: Run pending combined tests + if: ${{ !cancelled() && matrix.build_variant == 'Tests' }} + continue-on-error: true + timeout-minutes: 75 + shell: bash + env: + BUILD_DIRECTORY: ${{ steps.configure.outputs.build_directory }} + run: | + case "$BUILD_CONFIG" in + release) cmake_config=Release ;; + relwithdebinfo) cmake_config=RelWithDebInfo ;; + optdebug) cmake_config=OptDebug ;; + *) cmake_config=Debug ;; + esac + # This run writes LastTest.log over the build's; the build's is kept + # under a name the upload below still takes. + log="$BUILD_DIRECTORY/Testing/Temporary/LastTest.log" + if [[ -f "$log" ]]; then + cp "$log" "$BUILD_DIRECTORY/Testing/Temporary/LastTest-build.log" + fi + jobs="$(nproc 2>/dev/null || sysctl -n hw.logicalcpu)" + # CTest reads a relative --output-junit from the test directory. + ctest --test-dir "$BUILD_DIRECTORY" -C "$cmake_config" -L combined_pending \ + --parallel "$jobs" --output-on-failure --no-tests=ignore \ + --output-junit Testing/combined-pending-results.xml + # What each test did, on a row whose tests failed as much as on one # whose tests passed: the JUnit file CTest writes and its own log. The # build step's artifact name is unset when a test fails, so this one @@ -525,6 +558,7 @@ jobs: name: "test-results-${{ runner.os }}-${{ matrix.arch }}" path: | ${{ steps.configure.outputs.build_directory }}/Testing/test-results.xml + ${{ steps.configure.outputs.build_directory }}/Testing/combined-pending-results.xml ${{ steps.configure.outputs.build_directory }}/Testing/Temporary/LastTest*.log if-no-files-found: warn diff --git a/CLAUDE.md b/CLAUDE.md index 6a682455d8..eb07719ace 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,10 +57,12 @@ The viewer executable lands at `build--/newview//` (e.g., `S Tests are off by default. To enable: add `-DAL_BUILD_TESTS=ON` to the configure command. Then run via CTest: ``` -ctest --test-dir build-- --output-on-failure +ctest --test-dir build-- --output-on-failure -LE manual ``` -Unit tests live alongside the library they test in `indra//tests/` directories. The test framework is TUT (Template Unit Test). Test targets are defined by the `LL_ADD_PROJECT_UNIT_TESTS` macro in `indra/cmake/LLAddBuildTest.cmake`. Integration tests are in `indra/integration_tests/`. +`-LE manual` leaves out the benchmarks and the pending one-process runs of Catch2 binaries (label `combined_pending`), as the build's own test target does. + +Unit tests live alongside the library they test in `indra//tests/` directories. They are moving from TUT (Template Unit Test) to Catch2 a library at a time. TUT test targets are defined by the `LL_ADD_PROJECT_UNIT_TESTS` macro in `indra/cmake/LLAddBuildTest.cmake`; a converted library builds its files into one Catch2 binary with `al_add_test_binary` (`indra/cmake/ALAddTestBinary.cmake`), on the harness in `indra/test/`. Integration tests are in `indra/integration_tests/`. ## Architecture diff --git a/doc/BUILD.md b/doc/BUILD.md index 2145c434d2..6e0d0547ad 100644 --- a/doc/BUILD.md +++ b/doc/BUILD.md @@ -430,10 +430,12 @@ Build, then run with CTest: ``` cmake --build --config RelWithDebInfo -ctest --test-dir --output-on-failure +ctest --test-dir --output-on-failure -LE manual ``` -Unit tests live alongside the library they cover in `indra//tests/`, written against the TUT (Template Unit Test) framework. Integration tests are in `indra/integration_tests/`. +`-LE manual` leaves out what the build's own test target leaves out: the benchmarks, and each Catch2 binary's run of all its files in one process while that run is still labelled `combined_pending` (`ctest -L combined_pending` runs those). + +Unit tests live alongside the library they cover in `indra//tests/`. They are moving from the TUT (Template Unit Test) framework to Catch2 a library at a time: a converted library builds its test files into one binary (`al_add_test_binary`, `indra/cmake/ALAddTestBinary.cmake`), each file still a CTest entry of its own, on the harness in `indra/test/`. Integration tests are in `indra/integration_tests/`. ## Editing XUI diff --git a/indra/CMakeLists.txt b/indra/CMakeLists.txt index e623c8080c..483e0b6a48 100644 --- a/indra/CMakeLists.txt +++ b/indra/CMakeLists.txt @@ -41,6 +41,13 @@ cmake_dependent_option( "AL_BUILD_TESTS" OFF ) +cmake_dependent_option( + AL_TEST_PER_CASE + "Register a Catch2 binary's test cases one CTest entry each, found when ctest runs, in place of one entry a test file: for finding a failure that comes from order or shared state" + OFF + "AL_BUILD_TESTS" + OFF +) option(AL_BUILD_SHARED_LLCOMMON "Build llcommon as a shared library" OFF) option(AL_BUILD_PACKAGE "Add the package target: the CPack archive of the installed tree" ON) @@ -291,6 +298,10 @@ endif() # vcpkg feature flags MUST be set before the first project() call below # ########################################################################## +if(AL_BUILD_TESTS) + list(APPEND VCPKG_MANIFEST_FEATURES "tests") +endif() + if(AL_USE_KDU) list(APPEND VCPKG_MANIFEST_FEATURES "kdu") endif() @@ -406,16 +417,19 @@ if(AL_BUILD_TESTS) include(ProcessorCount) ProcessorCount(NUM_CORES) - # Set parallel level to decrease test runtime. Benchmarks are registered - # as tests under their own label and left out of the default run, since - # their numbers are their output and a shared parallel run would ruin them. + # Set parallel level to decrease test runtime. What is labelled manual runs + # only when asked for by its own label: benchmarks, since their numbers are + # their output and a shared parallel run would ruin them, and a Catch2 + # binary's run of all its tests in one process until they are known to + # share one (combined_pending; see ALAddTestBinary.cmake). One label, since + # these arguments reach a shell, which would take a | in a regex for a pipe. # Each run also writes its results as JUnit, which CI keeps. set( CMAKE_CTEST_ARGUMENTS -j${NUM_CORES} --output-on-failure -LE - benchmark + manual --output-junit "${CMAKE_BINARY_DIR}/Testing/test-results.xml" ) @@ -477,6 +491,7 @@ endforeach() # Add our test macros if(AL_BUILD_TESTS) include(LLAddBuildTest) + include(ALAddTestBinary) endif() # The build system's own files, for the IDE. diff --git a/indra/cmake/ALAddTestBinary.cmake b/indra/cmake/ALAddTestBinary.cmake new file mode 100644 index 0000000000..655b43b4eb --- /dev/null +++ b/indra/cmake/ALAddTestBinary.cmake @@ -0,0 +1,295 @@ +# -*- cmake -*- +# +# Catch2 test binaries: many test files built into one executable, each file +# still run in a process of its own. + +include_guard() + +if(NOT AL_BUILD_TESTS) + return() +endif() + +# al_add_test_binary( PROJECT SOURCES ... +# [LIBRARIES ...] [SUPPORT ...] +# [INCLUDES ...] [DEFINES ...] [ENVIRONMENT ...] +# [GL] [PYTHON] [ISA_TIER ] [RESOURCE_LOCK ...] [COMBINED]) +# +# Builds the test files into one Catch2 executable, , with the harness +# (al_test_support: the main, the listeners and the matchers) and SUPPORT's +# objects, linked against LIBRARIES. Anything that registers a test case or a +# listener is a source of the executable or of an OBJECT library: in a static +# archive the linker drops what nothing references, and the registrations +# with it, so the tests would vanish without a word. DEFINES and INCLUDES are +# the binary's, for every file in it. +# +# 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. +# +# /combined runs all of them in one process. It is labelled +# combined_pending and manual, which the default run leaves out, until the +# binary's tests are known to share a process: then COMBINED labels it +# combined, and it runs with the rest. With AL_TEST_PER_CASE, each test case is +# an entry of its own instead, found when ctest runs, with the environment and +# labels a file's entry would have. +# +# ENVIRONMENT sets variables for the runs, VAR=value each. GL, PYTHON and +# ISA_TIER mean what they mean to al_add_test: a GL binary is labelled gl and +# registered disabled without AL_ENABLE_GL_TESTS, a PYTHON binary is given the +# interpreter and registered disabled without one that has llsd, and an +# ISA_TIER binary is built at that x86-64 tier, named _, labelled +# isa, and skips on a host below it -- or below the tree's own tier, since it +# links the tree's libraries, built at that one. RESOURCE_LOCK names what the binary's +# entries hold that no two may hold at once, such as a fixed port. +function(al_add_test_binary name) + cmake_parse_arguments( + PARSE_ARGV 1 + arg + "GL;PYTHON;COMBINED" + "PROJECT;ISA_TIER" + "SOURCES;LIBRARIES;SUPPORT;INCLUDES;DEFINES;ENVIRONMENT;RESOURCE_LOCK" + ) + if(NOT arg_PROJECT) + message(FATAL_ERROR "al_add_test_binary(${name}): PROJECT is required") + endif() + if(NOT arg_SOURCES) + message(FATAL_ERROR "al_add_test_binary(${name}): SOURCES is required") + endif() + if(arg_UNPARSED_ARGUMENTS) + message( + FATAL_ERROR + "al_add_test_binary(${name}): unexpected arguments: ${arg_UNPARSED_ARGUMENTS}" + ) + endif() + set(target ${name}) + if(arg_ISA_TIER) + if(NOT arg_ISA_TIER MATCHES "^(baseline|v2|v3|v4)$") + message( + FATAL_ERROR + "al_add_test_binary(${name}): ISA_TIER must be baseline, v2, v3 or v4, not ${arg_ISA_TIER}" + ) + endif() + set(target ${name}_${arg_ISA_TIER}) + endif() + + set(stems) + foreach(source IN LISTS arg_SOURCES) + get_filename_component(stem "${source}" NAME_WLE) + if(stem IN_LIST stems) + message( + FATAL_ERROR + "al_add_test_binary(${name}): two test files are named ${stem}; the stem names a file's entry and its tag" + ) + endif() + list(APPEND stems ${stem}) + + # A file is in one binary at each tier: the variants of an ISA_TIER + # binary build the same files at the others. + get_filename_component(path "${source}" ABSOLUTE) + string(MD5 key "${path}${arg_ISA_TIER}") + get_property(owner GLOBAL PROPERTY AL_TEST_SOURCE_${key}) + if(owner) + message( + FATAL_ERROR + "al_add_test_binary(${name}): ${source} is already in ${owner}; a test file in two binaries would run twice" + ) + endif() + set_property(GLOBAL PROPERTY AL_TEST_SOURCE_${key} ${target}) + 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. + set(VCPKG_APPLOCAL_DEPS OFF) + add_executable(${target} ${arg_SOURCES}) + target_link_libraries(${target} PRIVATE al::flags al_test_support ${arg_SUPPORT} ${arg_LIBRARIES}) + target_include_directories(${target} PRIVATE ${arg_INCLUDES} ${INDRA_SOURCE_DIR}/test) + if(arg_DEFINES) + target_compile_definitions(${target} PRIVATE ${arg_DEFINES}) + endif() + set_target_properties(${target} PROPERTIES FOLDER "Tests/${arg_PROJECT}") + if(arg_ISA_TIER) + set_target_properties(${target} PROPERTIES AL_ISA_TIER ${arg_ISA_TIER}) + endif() + if(WINDOWS) + set_target_properties(${target} PROPERTIES AL_SKIP_RELEASE_DEBUG_INFO ON) + elseif(DARWIN) + # A test binary is run straight from the build tree, so it is ad-hoc signed. + set_target_properties( + ${target} + PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "-" BUILD_RPATH "${SHARED_LIB_STAGING_DIR}" + ) + endif() + + set(arguments) + set(environment ${arg_ENVIRONMENT}) + set(labels ${arg_PROJECT}) + set(disabled FALSE) + # As al_add_test's: five minutes a test file, fifteen with a Python peer. + set(timeout 300) + if(arg_ISA_TIER) + al_isa_level(${arg_ISA_TIER} ${CMAKE_SYSTEM_NAME} ${ARCH} level) + al_isa_level(${AL_ISA_TIER} ${CMAKE_SYSTEM_NAME} ${ARCH} tree_level) + if(tree_level GREATER level) + set(level ${tree_level}) + endif() + # One argument, not two: catch_discover_tests hands EXTRA_ARGS to each + # case as one, joined with ;. + list(APPEND arguments --isa-level=${level}) + list(APPEND labels isa) + endif() + if(arg_PYTHON) + set(timeout 900) + list(APPEND labels python) + if(AL_PYTHON_PEERS) + list(APPEND environment "PYTHON=${Python3_EXECUTABLE}") + else() + set(disabled TRUE) + endif() + endif() + if(arg_GL) + list(APPEND labels gl) + if(NOT AL_ENABLE_GL_TESTS) + set(disabled TRUE) + endif() + endif() + + if(AL_TEST_PER_CASE) + # Beside Catch2's package config, where vcpkg puts it, and not on the + # module path. + include("${Catch2_DIR}/Catch.cmake") + # WORKING_DIRECTORY is catch_discover_tests' own: among the PROPERTIES it + # would end them, and what came after would go unset. + set( + per_case_properties + TIMEOUT + ${timeout} + SKIP_RETURN_CODE + 125 + DISABLED + ${disabled} + ) + # 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 $ --filenames-as-tags "[#${stem}]~[.]" ${arguments} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties( + ${target}/${stem} + PROPERTIES + ENVIRONMENT "${environment}" + ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" + LABELS "${labels}" + DISABLED ${disabled} + SKIP_RETURN_CODE 125 + TIMEOUT ${timeout} + ) + if(arg_RESOURCE_LOCK) + set_tests_properties(${target}/${stem} PROPERTIES RESOURCE_LOCK "${arg_RESOURCE_LOCK}") + endif() + endforeach() + + # Every case no file's tag names: a filter of nothing but exclusions + # leaves the hidden cases out, as the files' entries do, and one that + # matches nothing exits 2. The ISA gate is not asked: nothing runs, and a + # listing already needs the binary to start on any host. + set(strays) + foreach(stem IN LISTS stems) + string(APPEND strays "~[#${stem}]") + endforeach() + 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 + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties( + ${target}/stray_cases + PROPERTIES ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" LABELS "${labels}" TIMEOUT 60 + ) + + # As long as the files take, but no longer than an hour: a hang, which + # the combined run is there to find, holds one CI row only so long. + list(LENGTH stems file_count) + math(EXPR combined_timeout "${timeout} * ${file_count}") + if(combined_timeout GREATER 3600) + set(combined_timeout 3600) + endif() + set(combined_label "combined_pending;manual") + if(arg_COMBINED) + set(combined_label combined) + endif() + add_test( + NAME ${target}/combined + COMMAND $ ${arguments} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties( + ${target}/combined + PROPERTIES + ENVIRONMENT "${environment}" + ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" + LABELS "${labels};${combined_label}" + DISABLED ${disabled} + SKIP_RETURN_CODE 125 + TIMEOUT ${combined_timeout} + ) + if(arg_RESOURCE_LOCK) + set_tests_properties(${target}/combined PROPERTIES RESOURCE_LOCK "${arg_RESOURCE_LOCK}") + endif() + endif() + + add_dependencies(BUILD_TESTS ${target}) +endfunction() diff --git a/indra/cmake/AlIntrinsicCheck.cmake b/indra/cmake/AlIntrinsicCheck.cmake index 1fa2bb0eab..9e744a10d6 100644 --- a/indra/cmake/AlIntrinsicCheck.cmake +++ b/indra/cmake/AlIntrinsicCheck.cmake @@ -17,7 +17,9 @@ if(NOT DEFINED SOURCE_DIR) endif() # The files that are the backend, and the tests and bench that compare -# against the raw instruction on purpose. +# against the raw instruction on purpose. The test harness's state guard +# reads the floating-point control register, which nothing but the raw +# instruction does, one branch an architecture. set( allowed llcommon/alsimd.h @@ -26,6 +28,7 @@ set( llmath/alsimdkernels.inl llmath/tests/alsimdkernels_test.cpp llmath/tests/llsimd_bench.cpp + test/alteststateguard.cpp ) # _mm_, _mm256_ and _mm512_ for x86; for NEON the ACLE names, a mnemonic diff --git a/indra/cmake/ConfigurationReport.cmake b/indra/cmake/ConfigurationReport.cmake index 566eafc5af..9766879be0 100644 --- a/indra/cmake/ConfigurationReport.cmake +++ b/indra/cmake/ConfigurationReport.cmake @@ -21,6 +21,7 @@ set( AL_BUILD_APPEARANCE_UTILITY AL_BUILD_TESTS AL_ENABLE_GL_TESTS + AL_TEST_PER_CASE AL_BUILD_DOCS AL_VCPKG_INSTALL AL_BUILD_DULLAHAN_EXAMPLE @@ -346,11 +347,14 @@ function(al_configuration_report) if(AL_BUILD_TESTS) _al_count_tests("${INDRA_SOURCE_DIR}" test_count) - if(AL_ENABLE_GL_TESTS) - _al_report_row("Tests" "on, ${test_count} registered") - else() - _al_report_row("Tests" "on, ${test_count} registered; the GL suites disabled") + set(tests "on, ${test_count} registered") + if(NOT AL_ENABLE_GL_TESTS) + string(APPEND tests "; the GL suites disabled") + endif() + if(AL_TEST_PER_CASE) + string(APPEND tests "; Catch2 test cases one entry each, found when ctest runs") endif() + _al_report_row("Tests" "${tests}") else() _al_report_row("Tests" "off") endif() diff --git a/indra/cmake/Dependencies.cmake b/indra/cmake/Dependencies.cmake index 18a9f6ce52..d1636859a9 100644 --- a/indra/cmake/Dependencies.cmake +++ b/indra/cmake/Dependencies.cmake @@ -108,6 +108,7 @@ al_import(ll::tut INCLUDE ${INDRA_SOURCE_DIR}/externals/tut/) al_import(ll::nvapi PACKAGE unofficial-nvapi CONFIG TARGETS unofficial::nvapi::nvapi WHEN WINDOWS) al_import(ll::fontconfig PACKAGE Fontconfig TARGETS Fontconfig::Fontconfig WHEN LINUX) al_import(ll::faudio PACKAGE FAudio CONFIG TARGETS FAudio::FAudio DEFINES LL_FAUDIO=1 WHEN AL_USE_FAUDIO) +al_import(ll::catch2 PACKAGE Catch2 CONFIG TARGETS Catch2::Catch2 WHEN AL_BUILD_TESTS) al_import(ll::openal PACKAGE OpenAL CONFIG TARGETS OpenAL::OpenAL DEFINES LL_OPENAL=1 WHEN AL_USE_OPENAL) al_import(ll::openxr PACKAGE OpenXR CONFIG TARGETS OpenXR::headers OpenXR::openxr_loader WHEN AL_USE_OPENXR) al_import(ll::nsspellchecker LINK "-framework AppKit" "-framework Foundation" WHEN AL_USE_NSSPELLCHECKER) diff --git a/indra/cmake/LLAddBuildTest.cmake b/indra/cmake/LLAddBuildTest.cmake index 6cbc67ffb9..289e2d7fd6 100644 --- a/indra/cmake/LLAddBuildTest.cmake +++ b/indra/cmake/LLAddBuildTest.cmake @@ -194,8 +194,8 @@ endfunction() # [INCLUDES ...] [DEFINES ...]) # # Builds tests/_bench.cpp, which brings its own main, into -# BENCH_ and registers it as a test labelled benchmark. The default -# test run leaves that label out; BUILD_AND_RUN_BENCHMARKS runs it, verbose, +# BENCH_ and registers it as a test labelled benchmark and manual. The +# default test run leaves manual out; BUILD_AND_RUN_BENCHMARKS runs it, verbose, # since the numbers are the output. A benchmark exits 125, which CTest reads # as skipped, from a build whose numbers would say nothing. function(al_add_bench name) @@ -232,7 +232,7 @@ function(al_add_bench name) ${target} PROPERTIES ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" - LABELS benchmark + LABELS "benchmark;manual" SKIP_RETURN_CODE 125 ) diff --git a/indra/llmath/CMakeLists.txt b/indra/llmath/CMakeLists.txt index a5dbe13fd9..6b51c8dcd9 100644 --- a/indra/llmath/CMakeLists.txt +++ b/indra/llmath/CMakeLists.txt @@ -103,42 +103,54 @@ target_precompile_headers(llmath REUSE_FROM llprecompiled) # Add tests if(AL_BUILD_TESTS) - # Each links the library rather than compiling a copy of the file it - # tests, so the code under test is the code the viewer runs. - foreach( - test - llbboxlocal - llmodularmath - llrect - v2math - v3color - v4color - v4coloru - alignment - alplaneset - llsdutil_math - mathmisc - m3math - llmatrix3a - llmatrix4a - llvector4a - llbbox - llquaternion - llquaternion2 - v3dmath - v3math - v4math - xform + # One Catch2 binary for llmath's tests, each test file run in a process of + # its own. Each links the library rather than compiling a copy of the file + # it tests, so the code under test is the code the viewer runs. + al_add_test_binary( + llmath_tests + PROJECT llmath + SOURCES + tests/alignment_test.cpp + tests/alplaneset_test.cpp + tests/alsimdkernels_test.cpp + tests/llbbox_test.cpp + tests/llbboxlocal_test.cpp + tests/llmatrix3a_test.cpp + tests/llmatrix4a_test.cpp + tests/llmodularmath_test.cpp + tests/llquaternion2_test.cpp + tests/llquaternion_test.cpp + tests/llrect_test.cpp + tests/llsdutil_math_test.cpp + tests/llvector4a_test.cpp + tests/m3math_test.cpp + tests/mathmisc_test.cpp + tests/v2math_test.cpp + tests/v3color_test.cpp + tests/v3dmath_test.cpp + tests/v3math_test.cpp + tests/v4color_test.cpp + tests/v4coloru_test.cpp + tests/v4math_test.cpp + tests/xform_test.cpp + LIBRARIES llmath llcommon ) - al_add_test(${test} PROJECT llmath LIBRARIES llmath llcommon) - endforeach() - # The batch kernels at every width, at the tree's tier and, where the - # tiers differ, at each of them; a variant the host cannot run skips. - al_add_test(alsimdkernels PROJECT llmath LIBRARIES llmath llcommon) + # The batch kernels again at each other x86-64 tier, one binary a tier: code + # the linker folds across files would otherwise run one tier's instructions + # in another's test. The tree's own tier is llmath_tests'; a variant the + # host cannot run skips. if(BUILD_TARGET_IS_X86_64 AND NOT DARWIN) foreach(tier baseline v2 v3 v4) - al_add_test(alsimdkernels PROJECT llmath LIBRARIES llmath llcommon ISA_TIER ${tier}) + if(NOT tier STREQUAL AL_ISA_TIER) + al_add_test_binary( + simd_tests + PROJECT llmath + SOURCES tests/alsimdkernels_test.cpp + LIBRARIES llmath llcommon + ISA_TIER ${tier} + ) + endif() endforeach() endif() diff --git a/indra/llmath/tests/alignment_test.cpp b/indra/llmath/tests/alignment_test.cpp index 3e4a25c66f..c3c1f9ca7d 100644 --- a/indra/llmath/tests/alignment_test.cpp +++ b/indra/llmath/tests/alignment_test.cpp @@ -29,7 +29,7 @@ // Tests related to allocating objects with alignment constraints, particularly for SSE support. #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../llmath.h" #include "../llsimdmath.h" #include "../llvector4a.h" @@ -38,20 +38,12 @@ #include -namespace tut +namespace { #define is_aligned(ptr,alignment) ((reinterpret_cast(ptr))%(alignment)==0) #define is_aligned_relative(ptr,base_ptr,alignment) ((reinterpret_cast(ptr)-reinterpret_cast(base_ptr))%(alignment)==0) -namespace -{ -struct alignment_test {}; - -typedef test_group alignment_test_t; -typedef alignment_test_t::object alignment_test_object_t; -tut::alignment_test_t tut_alignment_test("LLAlignment"); - // The SIMD types declare their alignment and nothing else: no operator new. // A plain new must place them, which the language guarantees up to the // default new alignment and, past it, through the aligned operator new. @@ -65,121 +57,121 @@ struct alignas(64) MyCacheLine { LLQuad mQ[4]; }; -} // Verify that aligned allocators perform as advertised. -template<> template<> -void alignment_test_object_t::test<1>() +TEST_CASE("LLAlignment: the aligned allocators return aligned memory", "[LLAlignment]") { const int num_tests = 7; void *align_ptr; for (int i=0; i template<> -void alignment_test_object_t::test<2>() +TEST_CASE("LLAlignment: over-aligned types on the stack singly and in arrays", "[LLAlignment]") { MyVector4a vec1; - ensure("LLAlignment vec1 unaligned", is_aligned(&vec1,16)); + CHECK(is_aligned(&vec1,16)); MyVector4a veca[12]; - ensure("LLAlignment veca unaligned", is_aligned(veca,16)); + CHECK(is_aligned(veca,16)); MyCacheLine line; - ensure("LLAlignment line unaligned", is_aligned(&line,64)); + CHECK(is_aligned(&line,64)); } // Heap allocation of objects and arrays. -template<> template<> -void alignment_test_object_t::test<3>() +TEST_CASE("LLAlignment: plain new aligns over-aligned objects and arrays", "[LLAlignment]") { const int ARR_SIZE = 7; for(int i=0; i template<> -void alignment_test_object_t::test<4>() +TEST_CASE("LLAlignment: LLVector4a and LLMatrix4a align on the heap and in containers", "[LLAlignment]") { - ensure_equals("LLVector4a size", sizeof(LLVector4a), size_t(16)); - ensure_equals("LLVector4a alignment", alignof(LLVector4a), size_t(16)); - ensure_equals("LLMatrix4a size", sizeof(LLMatrix4a), size_t(64)); - ensure_equals("LLMatrix4a alignment", alignof(LLMatrix4a), size_t(16)); + CHECK(sizeof(LLVector4a) == size_t(16)); + CHECK(alignof(LLVector4a) == size_t(16)); + CHECK(sizeof(LLMatrix4a) == size_t(64)); + CHECK(alignof(LLMatrix4a) == size_t(16)); LLVector4a *vecp = new LLVector4a; - ensure("LLVector4a unaligned", is_aligned(vecp,16)); + CHECK(is_aligned(vecp,16)); delete vecp; LLMatrix4a *matp = new LLMatrix4a[5]; - ensure("LLMatrix4a array unaligned", is_aligned(matp,16)); + CHECK(is_aligned(matp,16)); delete [] matp; std::vector vecs(33); for (const LLVector4a& v : vecs) { - ensure("LLVector4a in a vector unaligned", is_aligned(&v,16)); + REQUIRE(is_aligned(&v,16)); } std::vector mats(9); for (const LLMatrix4a& m : mats) { - ensure("LLMatrix4a in a vector unaligned", is_aligned(&m,16)); + REQUIRE(is_aligned(&m,16)); } } // A face vertex holds its position and normal in the object itself: a // vector of them is one allocation, every vector in it placed. -template<> template<> -void alignment_test_object_t::test<5>() +TEST_CASE("LLAlignment: LLVolumeFace::VertexData aligns in a vector and copies whole", "[LLAlignment]") { - ensure_equals("VertexData size", sizeof(LLVolumeFace::VertexData), size_t(48)); - ensure_equals("VertexData alignment", alignof(LLVolumeFace::VertexData), size_t(16)); - ensure("VertexData trivially copyable", std::is_trivially_copyable::value); + CHECK(sizeof(LLVolumeFace::VertexData) == size_t(48)); + CHECK(alignof(LLVolumeFace::VertexData) == size_t(16)); + CHECK(std::is_trivially_copyable::value); std::vector verts(17); for (size_t i = 0; i < verts.size(); ++i) { - ensure("VertexData position in a vector unaligned", is_aligned(&verts[i].getPosition(),16)); - ensure("VertexData normal in a vector unaligned", is_aligned(&verts[i].getNormal(),16)); + CAPTURE(i); + REQUIRE(is_aligned(&verts[i].getPosition(),16)); + REQUIRE(is_aligned(&verts[i].getNormal(),16)); verts[i].setPosition(LLVector4a(F32(i), 1.f, 2.f)); verts[i].setNormal(LLVector4a(0.f, 0.f, 1.f)); verts[i].mTexCoord.set(F32(i) * 0.5f, 0.25f); @@ -188,13 +180,14 @@ void alignment_test_object_t::test<5>() std::vector copy = verts; for (size_t i = 0; i < copy.size(); ++i) { - ensure("copied position", copy[i].getPosition().equals3(LLVector4a(F32(i), 1.f, 2.f))); - ensure("copied normal", copy[i].getNormal().equals3(LLVector4a(0.f, 0.f, 1.f))); - ensure_equals("copied texcoord", copy[i].mTexCoord.mV[0], F32(i) * 0.5f); - ensure("copy equals source", copy[i] == verts[i]); + CAPTURE(i); + REQUIRE(copy[i].getPosition().equals3(LLVector4a(F32(i), 1.f, 2.f))); + REQUIRE(copy[i].getNormal().equals3(LLVector4a(0.f, 0.f, 1.f))); + REQUIRE(copy[i].mTexCoord.mV[0] == F32(i) * 0.5f); + REQUIRE(copy[i] == verts[i]); } - ensure("ordering", copy[0] < copy[1]); - ensure("ordering", !(copy[1] < copy[0])); + CHECK(copy[0] < copy[1]); + CHECK_FALSE(copy[1] < copy[0]); } } diff --git a/indra/llmath/tests/alplaneset_test.cpp b/indra/llmath/tests/alplaneset_test.cpp index f39e19482d..ce418629b3 100644 --- a/indra/llmath/tests/alplaneset_test.cpp +++ b/indra/llmath/tests/alplaneset_test.cpp @@ -30,7 +30,7 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../llmath.h" #include "../llsimdmath.h" #include "../llvector4a.h" @@ -41,8 +41,6 @@ #include #include -namespace tut -{ namespace { // A camera at the origin looking down +X, with a frustum from the @@ -95,11 +93,8 @@ namespace boxes.push_back(near_touch); return boxes; } -} -} // namespace tut +} // anonymous namespace -namespace tut -{ namespace { struct alplaneset_data @@ -122,89 +117,97 @@ namespace return no_far ? mCamera.AABBInFrustumNoFarClip(b.center, b.radius, planes) : mCamera.AABBInFrustum(b.center, b.radius, planes); } }; - typedef test_group alplaneset_test; - typedef alplaneset_test::object alplaneset_object; - tut::alplaneset_test alplaneset_testcase("ALPlaneSet"); -} // An empty set contains everything; a set with one plane splits space. - template<> template<> - void alplaneset_object::test<1>() + TEST_CASE_METHOD(alplaneset_data, "ALPlaneSet: an empty set contains everything and one plane splits space", "[ALPlaneSet]") { ALPlaneSet empty; const LLVector4a somewhere(1e6f, -1e6f, 3.f, 0.f); const LLVector4a big(1e5f, 1e5f, 1e5f, 0.f); - ensure_equals("nothing is outside an empty set", empty.aabbTest(somewhere, big), 2); + UNSCOPED_INFO("nothing is outside an empty set"); + CHECK(empty.aabbTest(somewhere, big) == 2); // x <= 10 is inside: normal +x, constant -10 ALPlaneSet one; LLPlane plane(LLVector3(10.f, 0.f, 0.f), LLVector3(1.f, 0.f, 0.f)); one.set(3, plane, plane.calcPlaneMask()); - ensure_equals("well inside", one.aabbTest(LLVector4a(0.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)), 2); - ensure_equals("well outside", one.aabbTest(LLVector4a(20.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)), 0); - ensure_equals("straddling", one.aabbTest(LLVector4a(10.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)), 1); - ensure_equals("the skipped lane does not count", one.aabbTest(LLVector4a(20.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f), 3), 2); + UNSCOPED_INFO("well inside"); + CHECK(one.aabbTest(LLVector4a(0.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)) == 2); + UNSCOPED_INFO("well outside"); + CHECK(one.aabbTest(LLVector4a(20.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)) == 0); + UNSCOPED_INFO("straddling"); + CHECK(one.aabbTest(LLVector4a(10.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)) == 1); + UNSCOPED_INFO("the skipped lane does not count"); + CHECK(one.aabbTest(LLVector4a(20.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f), 3) == 2); one.disable(3); - ensure_equals("a disabled lane does not count", one.aabbTest(LLVector4a(20.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)), 2); + UNSCOPED_INFO("a disabled lane does not count"); + CHECK(one.aabbTest(LLVector4a(20.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)) == 2); } // The camera's set against its loop, over thousands of boxes, with and // without the far plane. - template<> template<> - void alplaneset_object::test<2>() + TEST_CASE_METHOD(alplaneset_data, "ALPlaneSet: the camera's set agrees with its loop with and without the far plane", "[ALPlaneSet]") { int outside = 0, crossing = 0, inside = 0; for (const Box& b : some_boxes(2, FAR_X)) { + CAPTURE(b.center); const S32 expected = by_loop(b); const S32 got = mCamera.AABBInFrustum(b.center, b.radius); - ensure_equals("AABBInFrustum at " + std::to_string(b.center[0]) + " " + std::to_string(b.center[1]) + " " + std::to_string(b.center[2]), got, expected); + REQUIRE(got == expected); outside += expected == 0; crossing += expected == 1; inside += expected == 2; const S32 expected_no_far = by_loop(b, true); - ensure_equals("AABBInFrustumNoFarClip at " + std::to_string(b.center[0]), mCamera.AABBInFrustumNoFarClip(b.center, b.radius), expected_no_far); + REQUIRE(mCamera.AABBInFrustumNoFarClip(b.center, b.radius) == expected_no_far); } - ensure("the boxes fell on every side: " + std::to_string(outside) + " " + std::to_string(crossing) + " " + std::to_string(inside), - outside > 100 && crossing > 100 && inside > 100); + CHECK(outside > 100); + CHECK(crossing > 100); + CHECK(inside > 100); // the obvious three - ensure_equals("a box in the middle is inside", mCamera.AABBInFrustum(LLVector4a(50.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)), 2); - ensure_equals("a box behind the camera is outside", mCamera.AABBInFrustum(LLVector4a(-50.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)), 0); - ensure_equals("a box across the far plane crosses", mCamera.AABBInFrustum(LLVector4a(FAR_X, 0.f, 0.f), LLVector4a(5.f, 5.f, 5.f)), 1); - ensure_equals("and is inside without the far plane", mCamera.AABBInFrustumNoFarClip(LLVector4a(FAR_X, 0.f, 0.f), LLVector4a(5.f, 5.f, 5.f)), 2); + UNSCOPED_INFO("a box in the middle is inside"); + CHECK(mCamera.AABBInFrustum(LLVector4a(50.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)) == 2); + UNSCOPED_INFO("a box behind the camera is outside"); + CHECK(mCamera.AABBInFrustum(LLVector4a(-50.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f)) == 0); + UNSCOPED_INFO("a box across the far plane crosses"); + CHECK(mCamera.AABBInFrustum(LLVector4a(FAR_X, 0.f, 0.f), LLVector4a(5.f, 5.f, 5.f)) == 1); + UNSCOPED_INFO("and is inside without the far plane"); + CHECK(mCamera.AABBInFrustumNoFarClip(LLVector4a(FAR_X, 0.f, 0.f), LLVector4a(5.f, 5.f, 5.f)) == 2); } // A user clip plane joins the set when enabled and leaves it when // disabled; an ignored plane leaves it. - template<> template<> - void alplaneset_object::test<3>() + TEST_CASE_METHOD(alplaneset_data, "ALPlaneSet: user clip planes join and leave the set and ignored planes leave it", "[ALPlaneSet]") { // z <= 5 is inside LLPlane clip(LLVector3(0.f, 0.f, 5.f), LLVector3(0.f, 0.f, 1.f)); mCamera.setUserClipPlane(clip); const Box above = {LLVector4a(50.f, 0.f, 20.f, 0.f), LLVector4a(1.f, 1.f, 1.f, 0.f)}; - ensure_equals("above the clip plane is outside", mCamera.AABBInFrustum(above.center, above.radius), 0); - ensure_equals("and the loop agrees", by_loop(above), 0); + UNSCOPED_INFO("above the clip plane is outside"); + CHECK(mCamera.AABBInFrustum(above.center, above.radius) == 0); + CHECK(by_loop(above) == 0); for (const Box& b : some_boxes(3, FAR_X)) { - ensure_equals("with the clip plane", mCamera.AABBInFrustum(b.center, b.radius), by_loop(b)); + CAPTURE(b.center); + REQUIRE(mCamera.AABBInFrustum(b.center, b.radius) == by_loop(b)); } mCamera.disableUserClipPlane(); - ensure_equals("above the disabled clip plane is inside", mCamera.AABBInFrustum(above.center, above.radius), 2); - ensure_equals("and the loop agrees", by_loop(above), 2); + UNSCOPED_INFO("above the disabled clip plane is inside"); + CHECK(mCamera.AABBInFrustum(above.center, above.radius) == 2); + CHECK(by_loop(above) == 2); mCamera.ignoreAgentFrustumPlane(LLCamera::AGENT_PLANE_FAR); const Box beyond = {LLVector4a(FAR_X + 50.f, 0.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f, 0.f)}; - ensure_equals("beyond an ignored far plane is inside", mCamera.AABBInFrustum(beyond.center, beyond.radius), 2); - ensure_equals("and the loop agrees", by_loop(beyond), 2); + UNSCOPED_INFO("beyond an ignored far plane is inside"); + CHECK(mCamera.AABBInFrustum(beyond.center, beyond.radius) == 2); + CHECK(by_loop(beyond) == 2); } // The region set is the agent set shifted, far plane aside. - template<> template<> - void alplaneset_object::test<4>() + TEST_CASE_METHOD(alplaneset_data, "ALPlaneSet: the region set is the agent set shifted apart from the far plane", "[ALPlaneSet]") { const LLVector3 shift(-256.f, 128.f, 30.f); mCamera.calcRegionFrustumPlanes(shift, FAR_X); @@ -212,41 +215,45 @@ namespace { Box in_agent = b; in_agent.center.set(b.center[0] + shift.mV[0], b.center[1] + shift.mV[1], b.center[2] + shift.mV[2], 0.f); - ensure_equals("AABBInRegionFrustumNoFarClip at " + std::to_string(b.center[0]), - mCamera.AABBInRegionFrustumNoFarClip(b.center, b.radius), by_loop(in_agent, true)); + CAPTURE(b.center); + REQUIRE(mCamera.AABBInRegionFrustumNoFarClip(b.center, b.radius) == by_loop(in_agent, true)); } - ensure_equals("a region box past the region far clip is outside", - mCamera.AABBInRegionFrustum(LLVector4a(FAR_X + 50.f - shift.mV[0], -shift.mV[1], -shift.mV[2], 0.f), LLVector4a(1.f, 1.f, 1.f)), 0); + UNSCOPED_INFO("a region box past the region far clip is outside"); + CHECK(mCamera.AABBInRegionFrustum(LLVector4a(FAR_X + 50.f - shift.mV[0], -shift.mV[1], -shift.mV[2], 0.f), LLVector4a(1.f, 1.f, 1.f)) == 0); } // A plane replaced one at a time, the way the shadow passes move the // near plane back behind the casters, is what the set tests against. - template<> template<> - void alplaneset_object::test<5>() + TEST_CASE_METHOD(alplaneset_data, "ALPlaneSet: setAgentPlane replaces a plane the set tests against", "[ALPlaneSet]") { // between the eye and the near plane, within the side planes const Box behind = {LLVector4a(0.5f, 0.f, 0.f, 0.f), LLVector4a(0.1f, 0.1f, 0.1f, 0.f)}; - ensure_equals("behind the near plane is outside", mCamera.AABBInFrustum(behind.center, behind.radius), 0); + UNSCOPED_INFO("behind the near plane is outside"); + CHECK(mCamera.AABBInFrustum(behind.center, behind.radius) == 0); // the near plane moved back to x = -50, still facing back LLPlane moved(LLVector3(-50.f, 0.f, 0.f), LLVector3(-1.f, 0.f, 0.f)); mCamera.setAgentPlane(LLCamera::AGENT_PLANE_NEAR, moved); - ensure("the plane reads back", mCamera.getAgentPlane(LLCamera::AGENT_PLANE_NEAR).equal(moved)); - ensure_equals("behind the old near plane is now inside", mCamera.AABBInFrustum(behind.center, behind.radius), 2); - ensure_equals("and the loop agrees", by_loop(behind), 2); + CHECK(mCamera.getAgentPlane(LLCamera::AGENT_PLANE_NEAR).equal(moved)); + UNSCOPED_INFO("behind the old near plane is now inside"); + CHECK(mCamera.AABBInFrustum(behind.center, behind.radius) == 2); + CHECK(by_loop(behind) == 2); for (const Box& b : some_boxes(5, FAR_X)) { - ensure_equals("with the near plane moved", mCamera.AABBInFrustum(b.center, b.radius), by_loop(b)); - ensure_equals("with the near plane moved, far ignored", mCamera.AABBInFrustumNoFarClip(b.center, b.radius), by_loop(b, true)); + CAPTURE(b.center); + REQUIRE(mCamera.AABBInFrustum(b.center, b.radius) == by_loop(b)); + REQUIRE(mCamera.AABBInFrustumNoFarClip(b.center, b.radius) == by_loop(b, true)); } // a plane facing the other way takes its octant with it: x >= 50 is now outside LLPlane flipped(LLVector3(50.f, 0.f, 0.f), LLVector3(1.f, 0.f, 0.f)); mCamera.setAgentPlane(LLCamera::AGENT_PLANE_NEAR, flipped); const Box straddling = {LLVector4a(50.f, 0.f, 0.f, 0.f), LLVector4a(2.f, 1.f, 1.f, 0.f)}; - ensure_equals("a box across the flipped plane is partly in", mCamera.AABBInFrustum(straddling.center, straddling.radius), 1); - ensure_equals("and the loop agrees", by_loop(straddling), 1); - ensure_equals("beyond the flipped plane is outside", mCamera.AABBInFrustum(LLVector4a(70.f, 0.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f, 0.f)), 0); - ensure_equals("and the loop agrees", by_loop({LLVector4a(70.f, 0.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f, 0.f)}), 0); + UNSCOPED_INFO("a box across the flipped plane is partly in"); + CHECK(mCamera.AABBInFrustum(straddling.center, straddling.radius) == 1); + CHECK(by_loop(straddling) == 1); + UNSCOPED_INFO("beyond the flipped plane is outside"); + CHECK(mCamera.AABBInFrustum(LLVector4a(70.f, 0.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f, 0.f)) == 0); + CHECK(by_loop({LLVector4a(70.f, 0.f, 0.f, 0.f), LLVector4a(1.f, 1.f, 1.f, 0.f)}) == 0); } } diff --git a/indra/llmath/tests/alsimdkernels_test.cpp b/indra/llmath/tests/alsimdkernels_test.cpp index ccf24a3dea..d20a597d4b 100644 --- a/indra/llmath/tests/alsimdkernels_test.cpp +++ b/indra/llmath/tests/alsimdkernels_test.cpp @@ -35,7 +35,7 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "llthread.h" #include "../llmath.h" #include "../llsimdmath.h" @@ -57,8 +57,6 @@ using namespace alsimd; using namespace alsimd::kernels; -namespace tut -{ namespace { std::string precise(double v) @@ -80,20 +78,21 @@ namespace return true; } - void ensure_same_bits(const std::string& what, const LLVector4a& got, const LLVector4a& expected) + void require_same_bits(const std::string& what, const LLVector4a& got, const LLVector4a& expected) { - ensure(what + ": " + precise(got[0]) + " " + precise(got[1]) + " " + precise(got[2]) + " " + precise(got[3]) + - " vs " + precise(expected[0]) + " " + precise(expected[1]) + " " + precise(expected[2]) + " " + precise(expected[3]), - same_bits(got, expected)); + INFO(what << ": " << precise(got[0]) << " " << precise(got[1]) << " " << precise(got[2]) << " " << precise(got[3]) + << " vs " << precise(expected[0]) << " " << precise(expected[1]) << " " << precise(expected[2]) << " " << precise(expected[3])); + REQUIRE(same_bits(got, expected)); } // The error relative to the size of the terms that made the value, // which is where the rounding happened, not to the value itself. - void ensure_close(const std::string& what, F32 got, double expected, double relative, double magnitude = 0.0) + void require_close(const std::string& what, F32 got, double expected, double relative, double magnitude = 0.0) { const double scale = llmax(1.0, llmax(std::fabs(expected), magnitude)); const double err = std::fabs((double)got - expected) / scale; - ensure(what + ": " + precise(got) + " is off " + precise(err) + " from " + precise(expected), err <= relative); + INFO(what << ": " << precise(got) << " is off " << precise(err) << " from " << precise(expected)); + REQUIRE(err <= relative); } // The counts that leave every tail a width can have. @@ -170,26 +169,14 @@ namespace State mSaved; }; -} -} // namespace tut +} // anonymous namespace -namespace tut -{ namespace { - struct alsimdkernels_data - { - }; - typedef test_group alsimdkernels_test; - typedef alsimdkernels_test::object alsimdkernels_object; - tut::alsimdkernels_test alsimdkernels_testcase("alsimdkernels"); -} - // Points, with w from the rows and with w set, against // affineTransform, bit for bit, at every count and width; nothing // written past the count. - template<> template<> - void alsimdkernels_object::test<1>() + TEST_CASE("alsimdkernels: transform_points against affineTransform at every count and width", "[alsimdkernels]") { const LLMatrix4a m = some_matrix(); for (size_t n : COUNTS) @@ -210,14 +197,16 @@ namespace transform_impl(m, src.data(), dst.data(), n, 0.f); for (size_t i = 0; i <= n; ++i) { - ensure_same_bits(std::string("transform_points ") + width + " n " + std::to_string(n) + " at " + std::to_string(i), dst[i], expected[i]); + CAPTURE(width, n, i); + require_same_bits("transform_points", dst[i], expected[i]); } std::vector dst_w(n + 1, SENTINEL); transform_impl(m, src.data(), dst_w.data(), n, 7.5f); for (size_t i = 0; i <= n; ++i) { - ensure_same_bits(std::string("transform_points with w ") + width + " n " + std::to_string(n) + " at " + std::to_string(i), dst_w[i], expected_w[i]); + CAPTURE(width, n, i); + require_same_bits("transform_points with w", dst_w[i], expected_w[i]); } }); } @@ -233,14 +222,14 @@ namespace m.affineTransform(src[i], expected[i]); for (int lane = 0; lane < 4; ++lane) { - ensure_close("public transform_points at " + std::to_string(i) + " lane " + std::to_string(lane), dst[i][lane], expected[i][lane], 4e-7, 1000.0); + CAPTURE(i, lane); + require_close("public transform_points", dst[i][lane], expected[i][lane], 4e-7, 1000.0); } } } // Directions, with w from the rows and with w kept, against rotate. - template<> template<> - void alsimdkernels_object::test<2>() + TEST_CASE("alsimdkernels: transform_directions against rotate at every count and width", "[alsimdkernels]") { const LLMatrix4a m = some_matrix(); for (size_t n : COUNTS) @@ -261,14 +250,16 @@ namespace transform_impl(m, src.data(), dst.data(), n, 0.f); for (size_t i = 0; i <= n; ++i) { - ensure_same_bits(std::string("transform_directions ") + width + " n " + std::to_string(n) + " at " + std::to_string(i), dst[i], expected[i]); + CAPTURE(width, n, i); + require_same_bits("transform_directions", dst[i], expected[i]); } std::vector dst_w(n + 1, SENTINEL); transform_impl(m, src.data(), dst_w.data(), n, 0.f); for (size_t i = 0; i <= n; ++i) { - ensure_same_bits(std::string("transform_directions_keep_w ") + width + " n " + std::to_string(n) + " at " + std::to_string(i), dst_w[i], expected_w[i]); + CAPTURE(width, n, i); + require_same_bits("transform_directions_keep_w", dst_w[i], expected_w[i]); } }); } @@ -277,8 +268,7 @@ namespace // Texture coordinates: two per vector, against the scalar arithmetic // in double within the two roundings the fused form takes, and every // width bit for bit against the first. - template<> template<> - void alsimdkernels_object::test<3>() + TEST_CASE("alsimdkernels: texture coordinates against double and every width against the first", "[alsimdkernels]") { const F32 cos_ang = 0.8f, sin_ang = 0.6f; const LLVector4a trans(-0.5f); @@ -301,11 +291,12 @@ namespace const double t = (double)src[i][2 * pair + 1] - 0.5; const double es = (s * cos_ang + t * sin_ang) * 2.0 + 0.75; const double et = (s * -sin_ang + t * cos_ang) * 0.25 - 1.5; - ensure_close("texcoord s n " + std::to_string(n) + " at " + std::to_string(i), narrow[i][2 * pair], es, 1e-6, 400.0); - ensure_close("texcoord t n " + std::to_string(n) + " at " + std::to_string(i), narrow[i][2 * pair + 1], et, 1e-6, 400.0); + CAPTURE(n, i, pair); + require_close("texcoord s", narrow[i][2 * pair], es, 1e-6, 400.0); + require_close("texcoord t", narrow[i][2 * pair + 1], et, 1e-6, 400.0); } } - ensure_same_bits("texcoords write within n", narrow[n], SENTINEL); + require_same_bits("texcoords write within n", narrow[n], SENTINEL); for_each_width([&](const char* width) { @@ -313,7 +304,8 @@ namespace transform_texcoords_impl(reinterpret_cast(src.data()), reinterpret_cast(dst.data()), n, trans, rot0, rot1, scale, offset); for (size_t i = 0; i <= n; ++i) { - ensure_same_bits(std::string("texcoords ") + width + " n " + std::to_string(n) + " at " + std::to_string(i), dst[i], narrow[i]); + CAPTURE(width, n, i); + require_same_bits("texcoords", dst[i], narrow[i]); } }); } @@ -321,8 +313,7 @@ namespace // Index offsets: bit for bit, wrapping, unaligned on both sides, every // count. - template<> template<> - void alsimdkernels_object::test<4>() + TEST_CASE("alsimdkernels: offset_indices_u16 wraps and writes only its unaligned range", "[alsimdkernels]") { std::mt19937 rng(4); std::uniform_int_distribution any(0, 65535); @@ -343,7 +334,8 @@ namespace offset_indices_u16_impl(src.data() + 1, dst.data() + 1, n, offset); for (size_t i = 0; i < n + 3; ++i) { - ensure_equals(std::string("offset_indices_u16 ") + width + " n " + std::to_string(n) + " offset " + std::to_string(offset) + " at " + std::to_string(i), dst[i], expected[i]); + CAPTURE(width, n, offset, i); + REQUIRE(dst[i] == expected[i]); } }); } @@ -354,8 +346,7 @@ namespace // reference within the two roundings a tier without FMA takes, w from // bias, the last vertex read without its neighbour, every width bit // for bit. - template<> template<> - void alsimdkernels_object::test<5>() + TEST_CASE("alsimdkernels: dequantize_u16x3 against double with w from bias", "[alsimdkernels]") { std::mt19937 rng(5); std::uniform_int_distribution any(0, 65535); @@ -387,11 +378,13 @@ namespace { const double term = (double)values[3 * i + lane] * scale[lane]; const double expected = term + bias[lane]; - ensure_close("dequantize_u16x3 n " + std::to_string(n) + " at " + std::to_string(i) + " lane " + std::to_string(lane), narrow[i][lane], expected, 5e-7, std::fabs(term) + std::fabs(bias[lane])); + CAPTURE(n, i, lane); + require_close("dequantize_u16x3", narrow[i][lane], expected, 5e-7, std::fabs(term) + std::fabs(bias[lane])); } - ensure_equals("dequantize_u16x3 w is bias", narrow[i][3], bias[3]); + CAPTURE(n, i); + REQUIRE(narrow[i][3] == bias[3]); } - ensure_same_bits("dequantize_u16x3 writes within n", narrow[n], SENTINEL); + require_same_bits("dequantize_u16x3 writes within n", narrow[n], SENTINEL); for_each_width([&](const char* width) { @@ -399,7 +392,8 @@ namespace dequantize_u16x3_impl(bytes.data(), n, scale, bias, dst.data()); for (size_t i = 0; i <= n; ++i) { - ensure_same_bits(std::string("dequantize_u16x3 ") + width + " n " + std::to_string(n) + " at " + std::to_string(i), dst[i], narrow[i]); + CAPTURE(width, n, i); + require_same_bits("dequantize_u16x3", dst[i], narrow[i]); } }); } @@ -407,8 +401,7 @@ namespace // Two 16-bit values per vertex, two vertices per vector, the odd tail's // upper pair at bias. - template<> template<> - void alsimdkernels_object::test<6>() + TEST_CASE("alsimdkernels: dequantize_u16x2 packs two vertices a vector and an odd tail at bias", "[alsimdkernels]") { std::mt19937 rng(6); std::uniform_int_distribution any(0, 65535); @@ -436,22 +429,24 @@ namespace { const double term = (double)values[2 * i + lane] * scale[base + lane]; const double expected = term + bias[base + lane]; - ensure_close("dequantize_u16x2 n " + std::to_string(n) + " at " + std::to_string(i) + " lane " + std::to_string(lane), v[base + lane], expected, 5e-7, std::fabs(term) + std::fabs(bias[base + lane])); + CAPTURE(n, i, lane); + require_close("dequantize_u16x2", v[base + lane], expected, 5e-7, std::fabs(term) + std::fabs(bias[base + lane])); } } if (n & 1) { - ensure_equals("odd tail upper u is bias", dst[vectors - 1][2], bias[2]); - ensure_equals("odd tail upper v is bias", dst[vectors - 1][3], bias[3]); + UNSCOPED_INFO("odd tail upper u is bias"); + REQUIRE(dst[vectors - 1][2] == bias[2]); + UNSCOPED_INFO("odd tail upper v is bias"); + REQUIRE(dst[vectors - 1][3] == bias[3]); } - ensure_same_bits("dequantize_u16x2 writes within its vectors", dst[vectors], SENTINEL); + require_same_bits("dequantize_u16x2 writes within its vectors", dst[vectors], SENTINEL); } } // The skinning blend against a double reference, and the skinned points // against the blend and two transforms done one at a time. - template<> template<> - void alsimdkernels_object::test<7>() + TEST_CASE("alsimdkernels: skin_blend against double and skin_points against two transforms", "[alsimdkernels]") { constexpr U32 JOINTS = 6; LLMatrix4a palette[JOINTS]; @@ -497,7 +492,8 @@ namespace { expected += frac[k] / sum * palette[idx[k]].getF32ptr()[e]; } - ensure_close("skin_blend element " + std::to_string(e) + " of " + precise(w[0]), got[e], expected, 2e-6, 100.0); + INFO("element " << e << " of " << precise(w[0])); + require_close("skin_blend", got[e], expected, 2e-6, 100.0); } for_each_width([&](const char* width) @@ -506,7 +502,8 @@ namespace skin_blend_impl(w, palette, JOINTS, wide); for (int r = 0; r < 4; ++r) { - ensure_same_bits(std::string("skin_blend ") + width + " row " + std::to_string(r), wide.mMatrix[r], narrow.mMatrix[r]); + CAPTURE(width, r); + require_same_bits("skin_blend", wide.mMatrix[r], narrow.mMatrix[r]); } }); } @@ -536,7 +533,8 @@ namespace skin_points_impl(vertex_weights.data(), palette, JOINTS, bind, src.data(), dst.data(), n); for (size_t i = 0; i <= n; ++i) { - ensure_same_bits(std::string("skin_points ") + width + " n " + std::to_string(n) + " at " + std::to_string(i), dst[i], expected[i]); + CAPTURE(width, n, i); + require_same_bits("skin_points", dst[i], expected[i]); } }); } @@ -544,8 +542,7 @@ namespace // Extents against the scalar minimum and maximum, bit for bit, every // count and width; an empty range leaves them alone. - template<> template<> - void alsimdkernels_object::test<8>() + TEST_CASE("alsimdkernels: extents against the scalar minimum and maximum bit for bit", "[alsimdkernels]") { for (size_t n : COUNTS) { @@ -566,13 +563,15 @@ namespace extents_impl(src.data(), n, lo, hi); if (n == 0) { - ensure_same_bits(std::string("extents ") + width + " of nothing leaves min", lo, SENTINEL); - ensure_same_bits(std::string("extents ") + width + " of nothing leaves max", hi, SENTINEL); + CAPTURE(width); + require_same_bits("extents of nothing leaves min", lo, SENTINEL); + require_same_bits("extents of nothing leaves max", hi, SENTINEL); } else { - ensure_same_bits(std::string("extents ") + width + " min n " + std::to_string(n), lo, expected_min); - ensure_same_bits(std::string("extents ") + width + " max n " + std::to_string(n), hi, expected_max); + CAPTURE(width, n); + require_same_bits("extents min", lo, expected_min); + require_same_bits("extents max", hi, expected_max); } }); } @@ -584,8 +583,7 @@ namespace // fast estimate's twelve bits, since one ulp of input can cross a step // of its table. With and without a mask and clothing weights, a // degenerate binormal delta included. - template<> template<> - void alsimdkernels_object::test<9>() + TEST_CASE("alsimdkernels: morph_apply against the loop it replaces", "[alsimdkernels]") { constexpr size_t MORPH = 37; constexpr size_t MESH = 50; @@ -696,20 +694,22 @@ namespace apply.tex_coords = got.tex.data(); morph_apply_impl(apply); - const std::string tag = " variant " + std::to_string(variant); + CAPTURE(variant); for (size_t v = 0; v < MESH; ++v) { + CAPTURE(v); for (int lane = 0; lane < 4; ++lane) { - ensure_close("coords" + tag + " at " + std::to_string(v), got.coords[v][lane], expected.coords[v][lane], 2e-6, 4.0); - ensure_close("scaled normals" + tag, got.scaled_normals[v][lane], expected.scaled_normals[v][lane], 2e-6, 4.0); - ensure_close("normals" + tag, got.normals[v][lane], expected.normals[v][lane], 6e-4, 1.0); - ensure_close("scaled binormals" + tag, got.scaled_binormals[v][lane], expected.scaled_binormals[v][lane], 2e-6, 4.0); - ensure_close("binormals" + tag, got.binormals[v][lane], expected.binormals[v][lane], 6e-4, 1.0); - ensure_close("clothing" + tag, got.clothing[v][lane], expected.clothing[v][lane], 2e-6, 4.0); + CAPTURE(lane); + require_close("coords", got.coords[v][lane], expected.coords[v][lane], 2e-6, 4.0); + require_close("scaled normals", got.scaled_normals[v][lane], expected.scaled_normals[v][lane], 2e-6, 4.0); + require_close("normals", got.normals[v][lane], expected.normals[v][lane], 6e-4, 1.0); + require_close("scaled binormals", got.scaled_binormals[v][lane], expected.scaled_binormals[v][lane], 2e-6, 4.0); + require_close("binormals", got.binormals[v][lane], expected.binormals[v][lane], 6e-4, 1.0); + require_close("clothing", got.clothing[v][lane], expected.clothing[v][lane], 2e-6, 4.0); } - ensure_close("tex u" + tag, got.tex[v].mV[0], expected.tex[v].mV[0], 2e-6, 4.0); - ensure_close("tex v" + tag, got.tex[v].mV[1], expected.tex[v].mV[1], 2e-6, 4.0); + require_close("tex u", got.tex[v].mV[0], expected.tex[v].mV[0], 2e-6, 4.0); + require_close("tex v", got.tex[v].mV[1], expected.tex[v].mV[1], 2e-6, 4.0); } } } @@ -718,8 +718,7 @@ namespace // is a denormal float; under the flush-to-zero mode the render thread // runs in it has to come through every width and the public entry // untouched. - template<> template<> - void alsimdkernels_object::test<10>() + TEST_CASE("alsimdkernels: a denormal texture index in w survives flush-to-zero", "[alsimdkernels]") { FPModeRestorer restore_fp_mode; set_thread_fp_mode(); @@ -736,8 +735,8 @@ namespace transform_impl(m, src.data(), dst.data(), n, w); for (size_t i = 0; i < n; ++i) { - ensure_equals(std::string("index in w ") + width + " index " + std::to_string(index) + " n " + std::to_string(n) + " at " + std::to_string(i), - std::bit_cast(dst[i][3]), index); + CAPTURE(width, index, n, i); + REQUIRE(std::bit_cast(dst[i][3]) == index); } }); @@ -745,8 +744,9 @@ namespace alsimd::transform_points(m, src.data(), dst.data(), n, w); for (size_t i = 0; i < n; ++i) { - ensure_equals("index in w through the library index " + std::to_string(index) + " n " + std::to_string(n) + " at " + std::to_string(i), - std::bit_cast(dst[i][3]), index); + INFO("through the library"); + CAPTURE(index, n, i); + REQUIRE(std::bit_cast(dst[i][3]) == index); } } } diff --git a/indra/llmath/tests/llbbox_test.cpp b/indra/llmath/tests/llbbox_test.cpp index 8b2c2bfaa2..726bf6d8ff 100644 --- a/indra/llmath/tests/llbbox_test.cpp +++ b/indra/llmath/tests/llbbox_test.cpp @@ -28,7 +28,7 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../llbbox.h" @@ -36,28 +36,13 @@ #define ANGLE (3.14159265f / 2.0f) #define APPROX_EQUAL(a, b) (dist_vec_squared((a),(b)) < 1e-10) -namespace tut -{ namespace { - struct LLBBoxData - { - }; - - typedef test_group factory; - typedef factory::object object; -} } namespace { - tut::factory llbbox_test_factory("LLBBox"); -} - -namespace tut -{ - template<> template<> - void object::test<1>() + TEST_CASE("LLBBox: the default constructor gives an empty box at the origin", "[LLBBox]") { // // test the default constructor @@ -65,14 +50,13 @@ namespace tut LLBBox bbox1; - ensure_equals("Default bbox min", bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("Default bbox max", bbox1.getMaxLocal(), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("Default bbox pos agent", bbox1.getPositionAgent(), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("Default bbox rotation", bbox1.getRotation(), LLQuaternion(0.0f, 0.0f, 0.0f, 1.0f)); + CHECK(bbox1.getMinLocal() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getMaxLocal() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getPositionAgent() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getRotation() == LLQuaternion(0.0f, 0.0f, 0.0f, 1.0f)); } - template<> template<> - void object::test<2>() + TEST_CASE("LLBBox: the full constructor stores its arguments", "[LLBBox]") { // // test the non-default constructor @@ -81,96 +65,92 @@ namespace tut LLBBox bbox2(LLVector3(1.0f, 2.0f, 3.0f), LLQuaternion(), LLVector3(2.0f, 3.0f, 4.0f), LLVector3(4.0f, 5.0f, 6.0f)); - ensure_equals("Custom bbox min", bbox2.getMinLocal(), LLVector3(2.0f, 3.0f, 4.0f)); - ensure_equals("Custom bbox max", bbox2.getMaxLocal(), LLVector3(4.0f, 5.0f, 6.0f)); - ensure_equals("Custom bbox pos agent", bbox2.getPositionAgent(), LLVector3(1.0f, 2.0f, 3.0f)); - ensure_equals("Custom bbox rotation", bbox2.getRotation(), LLQuaternion(0.0f, 0.0f, 0.0f, 1.0f)); + CHECK(bbox2.getMinLocal() == LLVector3(2.0f, 3.0f, 4.0f)); + CHECK(bbox2.getMaxLocal() == LLVector3(4.0f, 5.0f, 6.0f)); + CHECK(bbox2.getPositionAgent() == LLVector3(1.0f, 2.0f, 3.0f)); + CHECK(bbox2.getRotation() == LLQuaternion(0.0f, 0.0f, 0.0f, 1.0f)); } - template<> template<> - void object::test<3>() + TEST_CASE("LLBBox: setMinLocal", "[LLBBox]") { // // test the setMinLocal() method // LLBBox bbox2; bbox2.setMinLocal(LLVector3(3.0f, 3.0f, 3.0f)); - ensure_equals("Custom bbox min (2)", bbox2.getMinLocal(), LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox2.getMinLocal() == LLVector3(3.0f, 3.0f, 3.0f)); } - template<> template<> - void object::test<4>() + TEST_CASE("LLBBox: setMaxLocal", "[LLBBox]") { // // test the setMaxLocal() method // LLBBox bbox2; bbox2.setMaxLocal(LLVector3(5.0f, 5.0f, 5.0f)); - ensure_equals("Custom bbox max (2)", bbox2.getMaxLocal(), LLVector3(5.0f, 5.0f, 5.0f)); + CHECK(bbox2.getMaxLocal() == LLVector3(5.0f, 5.0f, 5.0f)); } - template<> template<> - void object::test<5>() + TEST_CASE("LLBBox: getCenterLocal ignores position and rotation", "[LLBBox]") { // // test the getCenterLocal() method // - ensure_equals("Default bbox local center", LLBBox().getCenterLocal(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(LLBBox().getCenterLocal() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBox bbox1(LLVector3(1.0f, 2.0f, 3.0f), LLQuaternion(), LLVector3(2.0f, 4.0f, 6.0f), LLVector3(4.0f, 6.0f, 8.0f)); - ensure_equals("Custom bbox center local", bbox1.getCenterLocal(), LLVector3(3.0f, 5.0f, 7.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(3.0f, 5.0f, 7.0f)); LLBBox bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(ANGLE, LLVector3(0.0f, 0.0f, 1.0f)), LLVector3(2.0f, 2.0f, 2.0f), LLVector3(4.0f, 4.0f, 4.0f)); - ensure_equals("Custom bbox center local with rot", bbox2.getCenterLocal(), LLVector3(3.0f, 3.0f, 3.0f)); + UNSCOPED_INFO("rotated box"); + CHECK(bbox2.getCenterLocal() == LLVector3(3.0f, 3.0f, 3.0f)); } - template<> template<> - void object::test<6>() + TEST_CASE("LLBBox: getCenterAgent applies position and rotation", "[LLBBox]") { // // test the getCenterAgent() // - ensure_equals("Default bbox agent center", LLBBox().getCenterAgent(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(LLBBox().getCenterAgent() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBox bbox1(LLVector3(1.0f, 2.0f, 3.0f), LLQuaternion(), LLVector3(2.0f, 4.0f, 6.0f), LLVector3(4.0f, 6.0f, 8.0f)); - ensure_equals("Custom bbox center agent", bbox1.getCenterAgent(), LLVector3(4.0f, 7.0f, 10.0f)); + CHECK(bbox1.getCenterAgent() == LLVector3(4.0f, 7.0f, 10.0f)); LLBBox bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(ANGLE, LLVector3(0.0f, 0.0f, 1.0f)), LLVector3(2.0f, 2.0f, 2.0f), LLVector3(4.0f, 4.0f, 4.0f)); - ensure("Custom bbox center agent with rot", APPROX_EQUAL(bbox2.getCenterAgent(), LLVector3(-2.0f, 4.0f, 4.0f))); + UNSCOPED_INFO("rotated box"); + CHECK(APPROX_EQUAL(bbox2.getCenterAgent(), LLVector3(-2.0f, 4.0f, 4.0f))); } - template<> template<> - void object::test<7>() + TEST_CASE("LLBBox: getExtentLocal", "[LLBBox]") { // // test the getExtentLocal() method // - ensure_equals("Default bbox local extent", LLBBox().getExtentLocal(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(LLBBox().getExtentLocal() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBox bbox1(LLVector3(1.0f, 2.0f, 3.0f), LLQuaternion(), LLVector3(2.0f, 4.0f, 6.0f), LLVector3(4.0f, 6.0f, 8.0f)); - ensure_equals("Custom bbox extent local", bbox1.getExtentLocal(), LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox1.getExtentLocal() == LLVector3(2.0f, 2.0f, 2.0f)); LLBBox bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(ANGLE, LLVector3(0.0f, 0.0f, 1.0f)), LLVector3(2.0f, 2.0f, 2.0f), LLVector3(4.0f, 4.0f, 4.0f)); - ensure_equals("Custom bbox extent local with rot", bbox1.getExtentLocal(), LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox1.getExtentLocal() == LLVector3(2.0f, 2.0f, 2.0f)); } - template<> template<> - void object::test<8>() + TEST_CASE("LLBBox: addPointLocal grows the box", "[LLBBox]") { // // test the addPointLocal() method @@ -180,22 +160,21 @@ namespace tut bbox1.addPointLocal(LLVector3(1.0f, 1.0f, 1.0f)); bbox1.addPointLocal(LLVector3(3.0f, 3.0f, 3.0f)); - ensure_equals("addPointLocal center local (1)", bbox1.getCenterLocal(), LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("addPointLocal center agent (1)", bbox1.getCenterAgent(), LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("addPointLocal min (1)", bbox1.getMinLocal(), LLVector3(1.0f, 1.0f, 1.0f)); - ensure_equals("addPointLocal max (1)", bbox1.getMaxLocal(), LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox1.getCenterAgent() == LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox1.getMinLocal() == LLVector3(1.0f, 1.0f, 1.0f)); + CHECK(bbox1.getMaxLocal() == LLVector3(3.0f, 3.0f, 3.0f)); bbox1.addPointLocal(LLVector3(0.0f, 0.0f, 0.0f)); bbox1.addPointLocal(LLVector3(1.0f, 1.0f, 1.0f)); bbox1.addPointLocal(LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("addPointLocal center local (2)", bbox1.getCenterLocal(), LLVector3(1.5f, 1.5f, 1.5f)); - ensure_equals("addPointLocal min (2)", bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("addPointLocal max (2)", bbox1.getMaxLocal(), LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(1.5f, 1.5f, 1.5f)); + CHECK(bbox1.getMinLocal() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getMaxLocal() == LLVector3(3.0f, 3.0f, 3.0f)); } - template<> template<> - void object::test<9>() + TEST_CASE("LLBBox: addBBoxLocal grows the box", "[LLBBox]") { // // test the addBBoxLocal() method @@ -205,21 +184,20 @@ namespace tut bbox1.addBBoxLocal(LLBBox(LLVector3(), LLQuaternion(), LLVector3(0.0f, 0.0f, 0.0f), LLVector3(3.0f, 3.0f, 3.0f))); - ensure_equals("addPointLocal center local (3)", bbox1.getCenterLocal(), LLVector3(1.5f, 1.5f, 1.5f)); - ensure_equals("addPointLocal min (3)", bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("addPointLocal max (3)", bbox1.getMaxLocal(), LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(1.5f, 1.5f, 1.5f)); + CHECK(bbox1.getMinLocal() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getMaxLocal() == LLVector3(3.0f, 3.0f, 3.0f)); bbox1.addBBoxLocal(LLBBox(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(), LLVector3(5.0f, 5.0f, 5.0f), LLVector3(10.0f, 10.0f, 10.0f))); - ensure_equals("addPointLocal center local (4)", bbox1.getCenterLocal(), LLVector3(5.0f, 5.0f, 5.0f)); - ensure_equals("addPointLocal center agent (4)", bbox1.getCenterAgent(), LLVector3(5.0f, 5.0f, 5.0f)); - ensure_equals("addPointLocal min (4)", bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("addPointLocal max (4)", bbox1.getMaxLocal(), LLVector3(10.0f, 10.0f, 10.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(5.0f, 5.0f, 5.0f)); + CHECK(bbox1.getCenterAgent() == LLVector3(5.0f, 5.0f, 5.0f)); + CHECK(bbox1.getMinLocal() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getMaxLocal() == LLVector3(10.0f, 10.0f, 10.0f)); } - template<> template<> - void object::test<10>() + TEST_CASE("LLBBox: addPointAgent on a rotated box", "[LLBBox]") { // // test the addPointAgent() method @@ -231,14 +209,13 @@ namespace tut bbox1.addPointAgent(LLVector3(1.0f, 1.0f, 1.0f)); bbox1.addPointAgent(LLVector3(3.0f, 3.0f, 3.0f)); - ensure_equals("addPointAgent center local (1)", bbox1.getCenterLocal(), LLVector3(2.0f, 2.0f, -2.0f)); - ensure_equals("addPointAgent center agent (1)", bbox1.getCenterAgent(), LLVector3(3.0f, 3.0f, 7.0f)); - ensure_equals("addPointAgent min (1)", bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, -4.0f)); - ensure_equals("addPointAgent max (1)", bbox1.getMaxLocal(), LLVector3(4.0f, 4.0f, 0.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(2.0f, 2.0f, -2.0f)); + CHECK(bbox1.getCenterAgent() == LLVector3(3.0f, 3.0f, 7.0f)); + CHECK(bbox1.getMinLocal() == LLVector3(0.0f, 0.0f, -4.0f)); + CHECK(bbox1.getMaxLocal() == LLVector3(4.0f, 4.0f, 0.0f)); } - template<> template<> - void object::test<11>() + TEST_CASE("LLBBox: addBBoxLocal after addPointAgent on a rotated box", "[LLBBox]") { // // test the addBBoxAgent() method @@ -253,14 +230,13 @@ namespace tut bbox1.addBBoxLocal(LLBBox(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(), LLVector3(5.0f, 5.0f, 5.0f), LLVector3(10.0f, 10.0f, 10.0f))); - ensure_equals("addPointAgent center local (2)", bbox1.getCenterLocal(), LLVector3(5.0f, 5.0f, 3.0f)); - ensure_equals("addPointAgent center agent (2)", bbox1.getCenterAgent(), LLVector3(6.0f, -10.0f, 8.0f)); - ensure_equals("addPointAgent min (2)", bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, -4.0f)); - ensure_equals("addPointAgent max (2)", bbox1.getMaxLocal(), LLVector3(10.0f, 10.0f, 10.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(5.0f, 5.0f, 3.0f)); + CHECK(bbox1.getCenterAgent() == LLVector3(6.0f, -10.0f, 8.0f)); + CHECK(bbox1.getMinLocal() == LLVector3(0.0f, 0.0f, -4.0f)); + CHECK(bbox1.getMaxLocal() == LLVector3(10.0f, 10.0f, 10.0f)); } - template<> template<> - void object::test<12>() + TEST_CASE("LLBBox: expand by zero then a positive then a negative amount", "[LLBBox]") { // // test the expand() method @@ -269,32 +245,31 @@ namespace tut LLBBox bbox1; bbox1.expand(0.0); - ensure_equals("Zero-expanded Default BBox center", bbox1.getCenterLocal(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getCenterLocal() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBox bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(), LLVector3(1.0f, 1.0f, 1.0f), LLVector3(3.0f, 3.0f, 3.0f)); bbox2.expand(0.0); - ensure_equals("Zero-expanded center local", bbox2.getCenterLocal(), LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("Zero-expanded center agent", bbox2.getCenterAgent(), LLVector3(3.0f, 3.0f, 3.0f)); - ensure_equals("Zero-expanded min", bbox2.getMinLocal(), LLVector3(1.0f, 1.0f, 1.0f)); - ensure_equals("Zero-expanded max", bbox2.getMaxLocal(), LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox2.getCenterLocal() == LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox2.getCenterAgent() == LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox2.getMinLocal() == LLVector3(1.0f, 1.0f, 1.0f)); + CHECK(bbox2.getMaxLocal() == LLVector3(3.0f, 3.0f, 3.0f)); bbox2.expand(0.5); - ensure_equals("Positive-expanded center", bbox2.getCenterLocal(), LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("Positive-expanded min", bbox2.getMinLocal(), LLVector3(0.5f, 0.5f, 0.5f)); - ensure_equals("Positive-expanded max", bbox2.getMaxLocal(), LLVector3(3.5f, 3.5f, 3.5f)); + CHECK(bbox2.getCenterLocal() == LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox2.getMinLocal() == LLVector3(0.5f, 0.5f, 0.5f)); + CHECK(bbox2.getMaxLocal() == LLVector3(3.5f, 3.5f, 3.5f)); bbox2.expand(-1.0); - ensure_equals("Negative-expanded center", bbox2.getCenterLocal(), LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("Negative-expanded min", bbox2.getMinLocal(), LLVector3(1.5f, 1.5f, 1.5f)); - ensure_equals("Negative-expanded max", bbox2.getMaxLocal(), LLVector3(2.5f, 2.5f, 2.5f)); + CHECK(bbox2.getCenterLocal() == LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox2.getMinLocal() == LLVector3(1.5f, 1.5f, 1.5f)); + CHECK(bbox2.getMaxLocal() == LLVector3(2.5f, 2.5f, 2.5f)); } - template<> template<> - void object::test<13>() + TEST_CASE("LLBBox: localToAgent with and without rotation", "[LLBBox]") { // // test the localToAgent() method @@ -303,16 +278,16 @@ namespace tut LLBBox bbox1(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(), LLVector3(1.0f, 1.0f, 1.0f), LLVector3(3.0f, 3.0f, 3.0f)); - ensure_equals("localToAgent(1,2,3)", bbox1.localToAgent(LLVector3(1.0f, 2.0f, 3.0f)), LLVector3(2.0f, 3.0f, 4.0f)); + CHECK(bbox1.localToAgent(LLVector3(1.0f, 2.0f, 3.0f)) == LLVector3(2.0f, 3.0f, 4.0f)); LLBBox bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(ANGLE, LLVector3(1.0f, 0.0f, 0.0f)), LLVector3(1.0f, 1.0f, 1.0f), LLVector3(3.0f, 3.0f, 3.0f)); - ensure("localToAgent(1,2,3) rot", APPROX_EQUAL(bbox2.localToAgent(LLVector3(1.0f, 2.0f, 3.0f)), LLVector3(2.0f, -2.0f, 3.0f))); + UNSCOPED_INFO("rotated box"); + CHECK(APPROX_EQUAL(bbox2.localToAgent(LLVector3(1.0f, 2.0f, 3.0f)), LLVector3(2.0f, -2.0f, 3.0f))); } - template<> template<> - void object::test<14>() + TEST_CASE("LLBBox: agentToLocal and its round trip through localToAgent", "[LLBBox]") { // // test the agentToLocal() method @@ -321,20 +296,21 @@ namespace tut LLBBox bbox1(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(), LLVector3(1.0f, 1.0f, 1.0f), LLVector3(3.0f, 3.0f, 3.0f)); - ensure_equals("agentToLocal(1,2,3)", bbox1.agentToLocal(LLVector3(1.0f, 2.0f, 3.0f)), LLVector3(0.0f, 1.0f, 2.0f)); - ensure_equals("agentToLocal(localToAgent)", bbox1.agentToLocal(bbox1.localToAgent(LLVector3(1.0f, 2.0f, 3.0f))), + CHECK(bbox1.agentToLocal(LLVector3(1.0f, 2.0f, 3.0f)) == LLVector3(0.0f, 1.0f, 2.0f)); + CHECK(bbox1.agentToLocal(bbox1.localToAgent(LLVector3(1.0f, 2.0f, 3.0f))) == LLVector3(1.0f, 2.0f, 3.0f)); LLBBox bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(ANGLE, LLVector3(1.0f, 0.0f, 0.0f)), LLVector3(1.0f, 1.0f, 1.0f), LLVector3(3.0f, 3.0f, 3.0f)); - ensure("agentToLocal(1,2,3) rot", APPROX_EQUAL(bbox2.agentToLocal(LLVector3(1.0f, 2.0f, 3.0f)), LLVector3(0.0f, 2.0f, -1.0f))); - ensure("agentToLocal(localToAgent) rot", APPROX_EQUAL(bbox2.agentToLocal(bbox2.localToAgent(LLVector3(1.0f, 2.0f, 3.0f))), + UNSCOPED_INFO("rotated box"); + CHECK(APPROX_EQUAL(bbox2.agentToLocal(LLVector3(1.0f, 2.0f, 3.0f)), LLVector3(0.0f, 2.0f, -1.0f))); + UNSCOPED_INFO("rotated box"); + CHECK(APPROX_EQUAL(bbox2.agentToLocal(bbox2.localToAgent(LLVector3(1.0f, 2.0f, 3.0f))), LLVector3(1.0f, 2.0f, 3.0f))); } - template<> template<> - void object::test<15>() + TEST_CASE("LLBBox: containsPointLocal is inclusive at min and max", "[LLBBox]") { // // test the containsPointLocal() method @@ -343,15 +319,14 @@ namespace tut LLBBox bbox1(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(), LLVector3(1.0f, 2.0f, 3.0f), LLVector3(3.0f, 4.0f, 5.0f)); - ensure("containsPointLocal(0,0,0)", bbox1.containsPointLocal(LLVector3(0.0f, 0.0f, 0.0f)) == false); - ensure("containsPointLocal(1,2,3)", bbox1.containsPointLocal(LLVector3(1.0f, 2.0f, 3.0f)) == true); - ensure("containsPointLocal(0.999,2,3)", bbox1.containsPointLocal(LLVector3(0.999f, 2.0f, 3.0f)) == false); - ensure("containsPointLocal(3,4,5)", bbox1.containsPointLocal(LLVector3(3.0f, 4.0f, 5.0f)) == true); - ensure("containsPointLocal(3,4,5.001)", bbox1.containsPointLocal(LLVector3(3.0f, 4.0f, 5.001f)) == false); + CHECK_FALSE(bbox1.containsPointLocal(LLVector3(0.0f, 0.0f, 0.0f))); + CHECK(bbox1.containsPointLocal(LLVector3(1.0f, 2.0f, 3.0f))); + CHECK_FALSE(bbox1.containsPointLocal(LLVector3(0.999f, 2.0f, 3.0f))); + CHECK(bbox1.containsPointLocal(LLVector3(3.0f, 4.0f, 5.0f))); + CHECK_FALSE(bbox1.containsPointLocal(LLVector3(3.0f, 4.0f, 5.001f))); } - template<> template<> - void object::test<16>() + TEST_CASE("LLBBox: containsPointAgent is inclusive at min and max", "[LLBBox]") { // // test the containsPointAgent() method @@ -360,11 +335,11 @@ namespace tut LLBBox bbox1(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(), LLVector3(1.0f, 2.0f, 3.0f), LLVector3(3.0f, 4.0f, 5.0f)); - ensure("containsPointAgent(0,0,0)", bbox1.containsPointAgent(LLVector3(0.0f, 0.0f, 0.0f)) == false); - ensure("containsPointAgent(2,3,4)", bbox1.containsPointAgent(LLVector3(2.0f, 3.0f, 4.0f)) == true); - ensure("containsPointAgent(2,2.999,4)", bbox1.containsPointAgent(LLVector3(2.0f, 2.999f, 4.0f)) == false); - ensure("containsPointAgent(4,5,6)", bbox1.containsPointAgent(LLVector3(4.0f, 5.0f, 6.0f)) == true); - ensure("containsPointAgent(4,5.001,6)", bbox1.containsPointAgent(LLVector3(4.0f, 5.001f, 6.0f)) == false); + CHECK_FALSE(bbox1.containsPointAgent(LLVector3(0.0f, 0.0f, 0.0f))); + CHECK(bbox1.containsPointAgent(LLVector3(2.0f, 3.0f, 4.0f))); + CHECK_FALSE(bbox1.containsPointAgent(LLVector3(2.0f, 2.999f, 4.0f))); + CHECK(bbox1.containsPointAgent(LLVector3(4.0f, 5.0f, 6.0f))); + CHECK_FALSE(bbox1.containsPointAgent(LLVector3(4.0f, 5.001f, 6.0f))); } } diff --git a/indra/llmath/tests/llbboxlocal_test.cpp b/indra/llmath/tests/llbboxlocal_test.cpp index 41d6326bc5..df9694e905 100644 --- a/indra/llmath/tests/llbboxlocal_test.cpp +++ b/indra/llmath/tests/llbboxlocal_test.cpp @@ -27,31 +27,16 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../llbboxlocal.h" -namespace tut -{ namespace { - struct LLBBoxLocalData - { - }; - - typedef test_group factory; - typedef factory::object object; -} } namespace { - tut::factory llbboxlocal_test_factory("LLBBoxLocal"); -} - -namespace tut -{ - template<> template<> - void object::test<1>() + TEST_CASE("LLBBoxLocal: the default constructor gives an empty box at the origin", "[LLBBoxLocal]") { // // test the default constructor @@ -59,12 +44,11 @@ namespace tut LLBBoxLocal bbox1; - ensure_equals("Default bbox min", bbox1.getMin(), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("Default bbox max", bbox1.getMax(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getMin() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getMax() == LLVector3(0.0f, 0.0f, 0.0f)); } - template<> template<> - void object::test<2>() + TEST_CASE("LLBBoxLocal: the constructor stores min and max", "[LLBBoxLocal]") { // // test the non-default constructor @@ -72,12 +56,11 @@ namespace tut LLBBoxLocal bbox2(LLVector3(-1.0f, -2.0f, 0.0f), LLVector3(1.0f, 2.0f, 3.0f)); - ensure_equals("Custom bbox min", bbox2.getMin(), LLVector3(-1.0f, -2.0f, 0.0f)); - ensure_equals("Custom bbox max", bbox2.getMax(), LLVector3(1.0f, 2.0f, 3.0f)); + CHECK(bbox2.getMin() == LLVector3(-1.0f, -2.0f, 0.0f)); + CHECK(bbox2.getMax() == LLVector3(1.0f, 2.0f, 3.0f)); } - template<> template<> - void object::test<3>() + TEST_CASE("LLBBoxLocal: setMin", "[LLBBoxLocal]") { // // test the setMin() @@ -89,11 +72,10 @@ namespace tut LLBBoxLocal bbox2; bbox2.setMin(LLVector3(1.0f, 2.0f, 3.0f)); - ensure_equals("Custom bbox min (2)", bbox2.getMin(), LLVector3(1.0f, 2.0f, 3.0f)); + CHECK(bbox2.getMin() == LLVector3(1.0f, 2.0f, 3.0f)); } - template<> template<> - void object::test<4>() + TEST_CASE("LLBBoxLocal: setMax", "[LLBBoxLocal]") { // // test the setMax() @@ -105,29 +87,28 @@ namespace tut LLBBoxLocal bbox2; bbox2.setMax(LLVector3(10.0f, 20.0f, 30.0f)); - ensure_equals("Custom bbox max (2)", bbox2.getMax(), LLVector3(10.0f, 20.0f, 30.0f)); + CHECK(bbox2.getMax() == LLVector3(10.0f, 20.0f, 30.0f)); } - template<> template<> - void object::test<5>() + TEST_CASE("LLBBoxLocal: getCenter including a box with min above max", "[LLBBoxLocal]") { // // test the getCenter() method // - ensure_equals("Default bbox center", LLBBoxLocal().getCenter(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(LLBBoxLocal().getCenter() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBoxLocal bbox1(LLVector3(-1.0f, -1.0f, -1.0f), LLVector3(0.0f, 0.0f, 0.0f)); - ensure_equals("Custom bbox center", bbox1.getCenter(), LLVector3(-0.5f, -0.5f, -0.5f)); + CHECK(bbox1.getCenter() == LLVector3(-0.5f, -0.5f, -0.5f)); LLBBoxLocal bbox2(LLVector3(0.0f, 0.0f, 0.0f), LLVector3(-1.0f, -1.0f, -1.0f)); - ensure_equals("Invalid bbox center", bbox2.getCenter(), LLVector3(-0.5f, -0.5f, -0.5f)); + UNSCOPED_INFO("min above max"); + CHECK(bbox2.getCenter() == LLVector3(-0.5f, -0.5f, -0.5f)); } - template<> template<> - void object::test<6>() + TEST_CASE("LLBBoxLocal: getExtent", "[LLBBoxLocal]") { // // test the getExtent() method @@ -135,15 +116,14 @@ namespace tut LLBBoxLocal bbox2(LLVector3(0.0f, 0.0f, 0.0f), LLVector3(-1.0f, -1.0f, -1.0f)); - ensure_equals("Default bbox extent", LLBBoxLocal().getExtent(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(LLBBoxLocal().getExtent() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBoxLocal bbox3(LLVector3(-1.0f, -1.0f, -1.0f), LLVector3(1.0f, 2.0f, 0.0f)); - ensure_equals("Custom bbox extent", bbox3.getExtent(), LLVector3(2.0f, 3.0f, 1.0f)); + CHECK(bbox3.getExtent() == LLVector3(2.0f, 3.0f, 1.0f)); } - template<> template<> - void object::test<7>() + TEST_CASE("LLBBoxLocal: addPoint grows the box only for points outside it", "[LLBBoxLocal]") { // // test the addPoint() method @@ -157,27 +137,26 @@ namespace tut bbox1.addPoint(LLVector3(-1.0f, -2.0f, -3.0f)); bbox1.addPoint(LLVector3(3.0f, 4.0f, 5.0f)); - ensure_equals("Custom BBox center (1)", bbox1.getCenter(), LLVector3(1.0f, 1.0f, 1.0f)); - ensure_equals("Custom BBox min (1)", bbox1.getMin(), LLVector3(-1.0f, -2.0f, -3.0f)); - ensure_equals("Custom BBox max (1)", bbox1.getMax(), LLVector3(3.0f, 4.0f, 5.0f)); + CHECK(bbox1.getCenter() == LLVector3(1.0f, 1.0f, 1.0f)); + CHECK(bbox1.getMin() == LLVector3(-1.0f, -2.0f, -3.0f)); + CHECK(bbox1.getMax() == LLVector3(3.0f, 4.0f, 5.0f)); bbox1.addPoint(LLVector3(0.0f, 0.0f, 0.0f)); bbox1.addPoint(LLVector3(1.0f, 2.0f, 3.0f)); bbox1.addPoint(LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("Custom BBox center (2)", bbox1.getCenter(), LLVector3(1.0f, 1.0f, 1.0f)); - ensure_equals("Custom BBox min (2)", bbox1.getMin(), LLVector3(-1.0f, -2.0f, -3.0f)); - ensure_equals("Custom BBox max (2)", bbox1.getMax(), LLVector3(3.0f, 4.0f, 5.0f)); + CHECK(bbox1.getCenter() == LLVector3(1.0f, 1.0f, 1.0f)); + CHECK(bbox1.getMin() == LLVector3(-1.0f, -2.0f, -3.0f)); + CHECK(bbox1.getMax() == LLVector3(3.0f, 4.0f, 5.0f)); bbox1.addPoint(LLVector3(5.0f, 5.0f, 5.0f)); - ensure_equals("Custom BBox center (3)", bbox1.getCenter(), LLVector3(2.0f, 1.5f, 1.0f)); - ensure_equals("Custom BBox min (3)", bbox1.getMin(), LLVector3(-1.0f, -2.0f, -3.0f)); - ensure_equals("Custom BBox max (3)", bbox1.getMax(), LLVector3(5.0f, 5.0f, 5.0f)); + CHECK(bbox1.getCenter() == LLVector3(2.0f, 1.5f, 1.0f)); + CHECK(bbox1.getMin() == LLVector3(-1.0f, -2.0f, -3.0f)); + CHECK(bbox1.getMax() == LLVector3(5.0f, 5.0f, 5.0f)); } - template<> template<> - void object::test<8>() + TEST_CASE("LLBBoxLocal: addBBox grows the box to cover both boxes", "[LLBBoxLocal]") { // // test the addBBox() methods @@ -190,19 +169,18 @@ namespace tut LLBBoxLocal bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLVector3(2.0f, 2.0f, 2.0f)); bbox2.addBBox(LLBBoxLocal(LLVector3(1.5f, 1.5f, 1.5f), LLVector3(3.0f, 3.0f, 3.0f))); - ensure_equals("Custom BBox center (4)", bbox2.getCenter(), LLVector3(2.0f, 2.0f, 2.0f)); - ensure_equals("Custom BBox min (4)", bbox2.getMin(), LLVector3(1.0f, 1.0f, 1.0f)); - ensure_equals("Custom BBox max (4)", bbox2.getMax(), LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox2.getCenter() == LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox2.getMin() == LLVector3(1.0f, 1.0f, 1.0f)); + CHECK(bbox2.getMax() == LLVector3(3.0f, 3.0f, 3.0f)); bbox2.addBBox(LLBBoxLocal(LLVector3(-1.0f, -1.0f, -1.0f), LLVector3(0.0f, 0.0f, 0.0f))); - ensure_equals("Custom BBox center (5)", bbox2.getCenter(), LLVector3(1.0f, 1.0f, 1.0f)); - ensure_equals("Custom BBox min (5)", bbox2.getMin(), LLVector3(-1.0f, -1.0f, -1.0f)); - ensure_equals("Custom BBox max (5)", bbox2.getMax(), LLVector3(3.0f, 3.0f, 3.0f)); + CHECK(bbox2.getCenter() == LLVector3(1.0f, 1.0f, 1.0f)); + CHECK(bbox2.getMin() == LLVector3(-1.0f, -1.0f, -1.0f)); + CHECK(bbox2.getMax() == LLVector3(3.0f, 3.0f, 3.0f)); } - template<> template<> - void object::test<9>() + TEST_CASE("LLBBoxLocal: expand by zero then a positive then a negative amount", "[LLBBoxLocal]") { // // test the expand() method @@ -211,25 +189,25 @@ namespace tut LLBBoxLocal bbox1; bbox1.expand(0.0f); - ensure_equals("Zero-expanded Default BBox center", bbox1.getCenter(), LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getCenter() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBoxLocal bbox2(LLVector3(1.0f, 2.0f, 3.0f), LLVector3(3.0f, 4.0f, 5.0f)); bbox2.expand(0.0f); - ensure_equals("Zero-expanded BBox center", bbox2.getCenter(), LLVector3(2.0f, 3.0f, 4.0f)); - ensure_equals("Zero-expanded BBox min", bbox2.getMin(), LLVector3(1.0f, 2.0f, 3.0f)); - ensure_equals("Zero-expanded BBox max", bbox2.getMax(), LLVector3(3.0f, 4.0f, 5.0f)); + CHECK(bbox2.getCenter() == LLVector3(2.0f, 3.0f, 4.0f)); + CHECK(bbox2.getMin() == LLVector3(1.0f, 2.0f, 3.0f)); + CHECK(bbox2.getMax() == LLVector3(3.0f, 4.0f, 5.0f)); bbox2.expand(0.5f); - ensure_equals("Positive-expanded BBox center", bbox2.getCenter(), LLVector3(2.0f, 3.0f, 4.0f)); - ensure_equals("Positive-expanded BBox min", bbox2.getMin(), LLVector3(0.5f, 1.5f, 2.5f)); - ensure_equals("Positive-expanded BBox max", bbox2.getMax(), LLVector3(3.5f, 4.5f, 5.5f)); + CHECK(bbox2.getCenter() == LLVector3(2.0f, 3.0f, 4.0f)); + CHECK(bbox2.getMin() == LLVector3(0.5f, 1.5f, 2.5f)); + CHECK(bbox2.getMax() == LLVector3(3.5f, 4.5f, 5.5f)); bbox2.expand(-1.0f); - ensure_equals("Negative-expanded BBox center", bbox2.getCenter(), LLVector3(2.0f, 3.0f, 4.0f)); - ensure_equals("Negative-expanded BBox min", bbox2.getMin(), LLVector3(1.5f, 2.5f, 3.5f)); - ensure_equals("Negative-expanded BBox max", bbox2.getMax(), LLVector3(2.5f, 3.5f, 4.5f)); + CHECK(bbox2.getCenter() == LLVector3(2.0f, 3.0f, 4.0f)); + CHECK(bbox2.getMin() == LLVector3(1.5f, 2.5f, 3.5f)); + CHECK(bbox2.getMax() == LLVector3(2.5f, 3.5f, 4.5f)); } } diff --git a/indra/llmath/tests/llmatrix3a_test.cpp b/indra/llmath/tests/llmatrix3a_test.cpp index b0da1591dd..6f3b709edc 100644 --- a/indra/llmath/tests/llmatrix3a_test.cpp +++ b/indra/llmath/tests/llmatrix3a_test.cpp @@ -30,7 +30,7 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../llmath.h" #include "../llsimdmath.h" #include "../llvector4a.h" @@ -41,22 +41,12 @@ #include #include -#include #include #include #include -namespace tut -{ namespace { - std::string precise(double v) - { - char buffer[32]; - std::snprintf(buffer, sizeof(buffer), "%.9g", v); - return buffer; - } - // The 3x3 as doubles, m[row][col] struct Rows { @@ -76,17 +66,17 @@ namespace return r; } - void ensure_rows(const std::string& what, const LLMatrix3a& got, const Rows& expected, double tolerance) + void require_rows_near(const std::string& what, const LLMatrix3a& got, const Rows& expected, double tolerance) { + INFO(what); for (int row = 0; row < 3; ++row) { for (int col = 0; col < 3; ++col) { const double g = got.getRow(row)[col]; const double e = expected.m[row][col]; - ensure(what + " row " + std::to_string(row) + " column " + std::to_string(col) + ": " + - precise(g) + " vs " + precise(e), - std::fabs(g - e) <= tolerance * llmax(1.0, std::fabs(e))); + CAPTURE(row, col, g, e); + REQUIRE(std::fabs(g - e) <= tolerance * llmax(1.0, std::fabs(e))); } } } @@ -143,48 +133,39 @@ namespace LLQuaternion(0.9f, LLVector3(0.577f, 0.577f, 0.577f)), LLQuaternion(3.14159f, LLVector3(-0.3f, 0.8f, 0.5f)), }; -} -} // namespace tut +} // anonymous namespace -namespace tut -{ namespace { - struct llmatrix3a_data - { - }; - typedef test_group llmatrix3a_test; - typedef llmatrix3a_test::object llmatrix3a_object; - tut::llmatrix3a_test llmatrix3a_testcase("LLMatrix3a"); -} - // Plain data of three registers; the identity; set and get by row and // by column. - template<> template<> - void llmatrix3a_object::test<1>() + TEST_CASE("LLMatrix3a: plain data of three registers; the identity; rows and columns", "[LLMatrix3a]") { - ensure_equals("sizeof LLMatrix3a", sizeof(LLMatrix3a), (size_t)48); - ensure_equals("alignof LLMatrix3a", alignof(LLMatrix3a), (size_t)16); - ensure_equals("sizeof LLRotation", sizeof(LLRotation), (size_t)48); - ensure("trivially copyable", std::is_trivially_copyable::value); + CHECK(sizeof(LLMatrix3a) == (size_t)48); + CHECK(alignof(LLMatrix3a) == (size_t)16); + CHECK(sizeof(LLRotation) == (size_t)48); + CHECK(std::is_trivially_copyable::value); const LLMatrix3a& identity = LLMatrix3a::getIdentity(); for (int row = 0; row < 3; ++row) { for (int col = 0; col < 3; ++col) { - ensure_equals("identity", identity.getRow(row)[col], row == col ? 1.f : 0.f); + CAPTURE(row, col); + REQUIRE(identity.getRow(row)[col] == (row == col ? 1.f : 0.f)); } } const LLMatrix3a m = odd_matrix(); - ensure_equals("getRow 1 y", m.getRow(1)[1], 5.5f); - ensure_equals("getRow 2 w", m.getRow(2)[3], 300.f); - ensure("getRow is getRow(N)", same_bits(m.getRow<0>(), m.getRow(0)) && same_bits(m.getRow<1>(), m.getRow(1)) && same_bits(m.getRow<2>(), m.getRow(2))); + CHECK(m.getRow(1)[1] == 5.5f); + CHECK(m.getRow(2)[3] == 300.f); + CHECK(same_bits(m.getRow<0>(), m.getRow(0))); + CHECK(same_bits(m.getRow<1>(), m.getRow(1))); + CHECK(same_bits(m.getRow<2>(), m.getRow(2))); LLMatrix3a by_rows; by_rows.setRows(m.getRow<0>(), m.getRow<1>(), m.getRow<2>()); - ensure("setRows", same_bits(by_rows, m)); + CHECK(same_bits(by_rows, m)); LLMatrix3a by_columns; by_columns.setColumns(m.getRow<0>(), m.getRow<1>(), m.getRow<2>()); @@ -192,14 +173,13 @@ namespace { for (int col = 0; col < 3; ++col) { - ensure_equals("setColumns is the transpose of setRows", by_columns.getRow(row)[col], m.getRow(col)[row]); + CAPTURE(row, col); + REQUIRE(by_columns.getRow(row)[col] == m.getRow(col)[row]); } } } - // loadu takes the scalar matrix row by row. - template<> template<> - void llmatrix3a_object::test<2>() + TEST_CASE("LLMatrix3a: loadu takes the scalar matrix row by row", "[LLMatrix3a]") { const F32 values[9] = {1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f}; const LLMatrix3 m3(values); @@ -207,18 +187,19 @@ namespace m.loadu(m3); for (int i = 0; i < 3; ++i) { + CAPTURE(i); for (int j = 0; j < 3; ++j) { - ensure_equals("loadu", m.getRow(i)[j], m3.mMatrix[i][j]); + CAPTURE(j); + REQUIRE(m.getRow(i)[j] == m3.mMatrix[i][j]); } - ensure_equals("loadu w is zero", m.getRow(i)[3], 0.f); + REQUIRE(m.getRow(i)[3] == 0.f); } } // The transpose swaps rows and columns, twice is the identity, and // the fourth lanes are what the header says they are. - template<> template<> - void llmatrix3a_object::test<3>() + TEST_CASE("LLMatrix3a: setTranspose swaps rows and columns and twice gives the matrix back", "[LLMatrix3a]") { const LLMatrix3a m = odd_matrix(); LLMatrix3a t; @@ -227,26 +208,26 @@ namespace { for (int col = 0; col < 3; ++col) { - ensure_equals("setTranspose", t.getRow(row)[col], m.getRow(col)[row]); + CAPTURE(row, col); + REQUIRE(t.getRow(row)[col] == m.getRow(col)[row]); } } - ensure_equals("transpose row 0 w", t.getRow(0)[3], m.getRow(2)[1]); - ensure_equals("transpose row 1 w", t.getRow(1)[3], m.getRow(2)[0]); - ensure_equals("transpose row 2 w", t.getRow(2)[3], m.getRow(2)[0]); + CHECK(t.getRow(0)[3] == m.getRow(2)[1]); + CHECK(t.getRow(1)[3] == m.getRow(2)[0]); + CHECK(t.getRow(2)[3] == m.getRow(2)[0]); LLMatrix3a back; back.setTranspose(t); - ensure("transpose twice", same_bits3(back, m)); + CHECK(same_bits3(back, m)); // in place LLMatrix3a self = m; self.setTranspose(self); - ensure("setTranspose in place", same_bits(self, t)); + CHECK(same_bits(self, t)); } // rotate is the scalar type's v * M, and setRotated is rotate. - template<> template<> - void llmatrix3a_object::test<4>() + TEST_CASE("LLMatrix3a: rotate is the scalar type's v * M and setRotated is rotate", "[LLMatrix3a]") { const LLMatrix3a m = odd_matrix(); const Rows r = rows_of(m); @@ -257,20 +238,20 @@ namespace for (int col = 0; col < 3; ++col) { const double expected = v[0] * r.m[0][col] + v[1] * r.m[1][col] + v[2] * r.m[2][col]; - ensure("rotate lane " + std::to_string(col) + ": " + precise(rotated[col]) + " vs " + precise(expected), - std::fabs(rotated[col] - expected) <= 1e-6 * llmax(1.0, std::fabs(expected))); + CAPTURE(col, rotated[col], expected); + REQUIRE(std::fabs(rotated[col] - expected) <= 1e-6 * llmax(1.0, std::fabs(expected))); } LLRotation rot; rot.setRows(m.getRow<0>(), m.getRow<1>(), m.getRow<2>()); LLVector4a set_rotated; set_rotated.setRotated(rot, v); - ensure("setRotated is rotate", same_bits(set_rotated, rotated)); + CHECK(same_bits(set_rotated, rotated)); // in place LLVector4a self = v; m.rotate(self, self); - ensure("rotate in place", same_bits(self, rotated)); + CHECK(same_bits(self, rotated)); for (const LLQuaternion& q : ROTATIONS) { @@ -281,15 +262,14 @@ namespace const LLVector3 expected = v3 * m3; LLVector4a got; a.rotate(v, got); - ensure("rotate is v * LLMatrix3", got.equals3(LLVector4a(expected.mV[0], expected.mV[1], expected.mV[2]), 1e-5f)); + REQUIRE(got.equals3(LLVector4a(expected.mV[0], expected.mV[1], expected.mV[2]), 1e-5f)); } } // The product against double, the identity on both sides, and the // property that names it: rotating by the product is rotating by the // left one and then the right. - template<> template<> - void llmatrix3a_object::test<5>() + TEST_CASE("LLMatrix3a: setMul against double and the identity and as a then b", "[LLMatrix3a]") { const LLMatrix3a a = odd_matrix(); const LLMatrix3a b(LLVector4a(0.5f, -1.f, 2.f, 1.f), @@ -309,7 +289,7 @@ namespace LLMatrix3a product; product.setMul(a, b); - ensure_rows("setMul", product, expected, 1e-6); + require_rows_near("setMul", product, expected, 1e-6); // the scalar type agrees on the order LLMatrix3 a3, b3; @@ -324,33 +304,34 @@ namespace const LLMatrix3 product3 = a3 * b3; LLMatrix3a from_scalar; from_scalar.loadu(product3); - ensure("setMul(a, b) is LLMatrix3 a * b", product.isApproximatelyEqual(from_scalar, 1e-4f)); + UNSCOPED_INFO("setMul(a, b) is LLMatrix3 a * b"); + CHECK(product.isApproximatelyEqual(from_scalar, 1e-4f)); LLMatrix3a left, right; left.setMul(LLMatrix3a::getIdentity(), a); right.setMul(a, LLMatrix3a::getIdentity()); - ensure("identity on the left", same_bits(left, a)); - ensure("identity on the right", same_bits3(right, a)); + CHECK(same_bits(left, a)); + CHECK(same_bits3(right, a)); // aliasing either operand LLMatrix3a alias = a; alias.setMul(alias, b); - ensure("setMul aliasing a", same_bits(alias, product)); + CHECK(same_bits(alias, product)); alias = b; alias.setMul(a, alias); - ensure("setMul aliasing b", same_bits(alias, product)); + CHECK(same_bits(alias, product)); const LLVector4a v(1.f, -2.f, 0.5f, 0.f); LLVector4a by_a, then_b, by_product; a.rotate(v, by_a); b.rotate(by_a, then_b); product.rotate(v, by_product); - ensure("v * (ab) is (v * a) * b", by_product.equals3(then_b, 1e-4f)); + UNSCOPED_INFO("v * (ab) is (v * a) * b"); + CHECK(by_product.equals3(then_b, 1e-4f)); } // The determinant, both forms, against double. - template<> template<> - void llmatrix3a_object::test<6>() + TEST_CASE("LLMatrix3a: getDeterminant in both forms against double", "[LLMatrix3a]") { const LLMatrix3a m = odd_matrix(); const Rows r = rows_of(m); @@ -360,62 +341,64 @@ namespace r.m[0][2] * (r.m[1][0] * r.m[2][1] - r.m[1][1] * r.m[2][0]); const F32 det = m.getDeterminant().getF32(); - ensure("getDeterminant: " + precise(det) + " vs " + precise(expected), std::fabs(det - expected) <= 1e-5 * std::fabs(expected)); + CAPTURE(det, expected); + CHECK(std::fabs(det - expected) <= 1e-5 * std::fabs(expected)); LLVector4a all; m.getDeterminant(all); for (int i = 0; i < 4; ++i) { - ensure_equals("getDeterminant(LLVector4a) lane " + std::to_string(i), all[i], det); + CAPTURE(i); + REQUIRE(all[i] == det); } - ensure_equals("identity determinant", LLMatrix3a::getIdentity().getDeterminant().getF32(), 1.f); + CHECK(LLMatrix3a::getIdentity().getDeterminant().getF32() == 1.f); } // A rotation is one; a scale, a shear and a reflection are not. - template<> template<> - void llmatrix3a_object::test<7>() + TEST_CASE("LLMatrix3a: a rotation is one; a scale or a shear or a reflection is not", "[LLMatrix3a]") { for (const LLQuaternion& q : ROTATIONS) { LLRotation r; r.loadu(q.getMatrix3()); - ensure("a rotation isOkRotation", r.isOkRotation()); - ensure("a rotation is finite", r.isFinite()); + REQUIRE(r.isOkRotation()); + REQUIRE(r.isFinite()); } LLRotation scaled; scaled.setRows(LLVector4a(2.f, 0.f, 0.f), LLVector4a(0.f, 2.f, 0.f), LLVector4a(0.f, 0.f, 2.f)); - ensure("a scale is not a rotation", !scaled.isOkRotation()); + CHECK_FALSE(scaled.isOkRotation()); LLRotation sheared; sheared.setRows(LLVector4a(1.f, 0.f, 0.f), LLVector4a(0.5f, 1.f, 0.f), LLVector4a(0.f, 0.f, 1.f)); - ensure("a shear is not a rotation", !sheared.isOkRotation()); + CHECK_FALSE(sheared.isOkRotation()); LLRotation reflected; reflected.setRows(LLVector4a(-1.f, 0.f, 0.f), LLVector4a(0.f, 1.f, 0.f), LLVector4a(0.f, 0.f, 1.f)); - ensure("a reflection is not a rotation", !reflected.isOkRotation()); + CHECK_FALSE(reflected.isOkRotation()); LLRotation nan; nan.setRows(LLVector4a(std::numeric_limits::quiet_NaN(), 0.f, 0.f), LLVector4a(0.f, 1.f, 0.f), LLVector4a(0.f, 0.f, 1.f)); - ensure("a NaN is not finite", !nan.isFinite()); - ensure("a NaN is not a rotation", !nan.isOkRotation()); + CHECK_FALSE(nan.isFinite()); + CHECK_FALSE(nan.isOkRotation()); LLRotation inf_in_w; inf_in_w.setRows(LLVector4a(1.f, 0.f, 0.f, std::numeric_limits::infinity()), LLVector4a(0.f, 1.f, 0.f), LLVector4a(0.f, 0.f, 1.f)); - ensure("the fourth lane is not looked at", inf_in_w.isFinite()); + UNSCOPED_INFO("the fourth lane is not looked at"); + CHECK(inf_in_w.isFinite()); } // batchTransform is rotate over an array, every count, including the // odd one the loop peels. - template<> template<> - void llmatrix3a_object::test<8>() + TEST_CASE("LLMatrix3a: batchTransform is rotate over an array of every count", "[LLMatrix3a]") { LLRotation r; r.loadu(ROTATIONS[4].getMatrix3()); for (int count : {0, 1, 2, 3, 7, 16}) { + CAPTURE(count); std::vector src(count + 1), dst(count + 1); for (int i = 0; i < count; ++i) { @@ -429,16 +412,15 @@ namespace { LLVector4a one; r.rotate(src[i], one); - ensure("batchTransform count " + std::to_string(count) + " element " + std::to_string(i), - same_bits(dst[i], one)); + CAPTURE(i); + REQUIRE(same_bits(dst[i], one)); } - ensure("batchTransform stays within count", same_bits(dst[count], LLVector4a(9.f, 9.f, 9.f, 9.f))); + UNSCOPED_INFO("batchTransform stays within count"); + REQUIRE(same_bits(dst[count], LLVector4a(9.f, 9.f, 9.f, 9.f))); } } - // setLerp and the tolerance compare. - template<> template<> - void llmatrix3a_object::test<9>() + TEST_CASE("LLMatrix3a: setLerp and the tolerance compare", "[LLMatrix3a]") { const LLMatrix3a a = odd_matrix(); const LLMatrix3a b(LLVector4a(0.5f, -1.f, 2.f, 1.f), @@ -449,8 +431,8 @@ namespace at_zero.setLerp(a, b, 0.f); at_one.setLerp(a, b, 1.f); at_quarter.setLerp(a, b, 0.25f); - ensure("setLerp at 0", same_bits(at_zero, a)); - ensure("setLerp at 1", at_one.isApproximatelyEqual(b, 1e-6f)); + CHECK(same_bits(at_zero, a)); + CHECK(at_one.isApproximatelyEqual(b, 1e-6f)); const Rows ra = rows_of(a); const Rows rb = rows_of(b); @@ -462,13 +444,14 @@ namespace expected.m[row][col] = ra.m[row][col] + (rb.m[row][col] - ra.m[row][col]) * 0.25; } } - ensure_rows("setLerp at a quarter", at_quarter, expected, 1e-6); + require_rows_near("setLerp at a quarter", at_quarter, expected, 1e-6); LLMatrix3a nearby = a; LLVector4a nudged = nearby.getRow<1>(); nudged.add(LLVector4a(0.f, 1e-6f, 0.f, 50.f)); nearby.setRows(nearby.getRow<0>(), nudged, nearby.getRow<2>()); - ensure("isApproximatelyEqual within tolerance, w ignored", nearby.isApproximatelyEqual(a)); - ensure("isApproximatelyEqual outside tolerance", !nearby.isApproximatelyEqual(a, 1e-7f)); + UNSCOPED_INFO("isApproximatelyEqual within tolerance, w ignored"); + CHECK(nearby.isApproximatelyEqual(a)); + CHECK_FALSE(nearby.isApproximatelyEqual(a, 1e-7f)); } } diff --git a/indra/llmath/tests/llmatrix4a_test.cpp b/indra/llmath/tests/llmatrix4a_test.cpp index 32192faead..c85aec8462 100644 --- a/indra/llmath/tests/llmatrix4a_test.cpp +++ b/indra/llmath/tests/llmatrix4a_test.cpp @@ -24,7 +24,9 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include +#include +#include "altestmatchers.h" #include "../llmath.h" #include "../llsimdmath.h" #include "../llvector4a.h" @@ -37,7 +39,6 @@ #include #include -#include #include namespace @@ -69,17 +70,8 @@ namespace }; } -namespace tut -{ namespace { - struct llmatrix4a_data - { - }; - typedef test_group llmatrix4a_test; - typedef llmatrix4a_test::object llmatrix4a_object; - tut::llmatrix4a_test llmatrix4a_testcase("LLMatrix4a"); - // A four by four in double, the rows of a row-vector matrix: the // reference the single-precision class is held to, with every product, // inverse and transform written out longhand. @@ -239,8 +231,9 @@ namespace // two, or of one: single precision against the double reference, and // two single-precision implementations against each other, agree to a // few ulps under /fp:fast, not bit for bit. - void ensure_matrix_close(const std::string& what, const LLMatrix4a& actual, const Ref& expected, F32 tolerance) + void require_matrix_close(const std::string& what, const LLMatrix4a& actual, const Ref& expected, F32 tolerance) { + INFO(what); for (S32 i = 0; i < 4; ++i) { for (S32 j = 0; j < 4; ++j) @@ -248,40 +241,40 @@ namespace const F32 a = actual.mMatrix[i][j]; const F32 e = F32(expected.m[i][j]); const F32 scale = llmax(1.f, fabsf(a), fabsf(e)); - std::ostringstream msg; - msg << what << " at [" << i << "][" << j << "]: " << a << " vs " << e; - ensure_approximately_equals_range(msg.str().c_str(), a, e, tolerance * scale); + CAPTURE(i, j); + REQUIRE_THAT(a, Catch::Matchers::WithinAbs(e, tolerance * scale)); } } } - void ensure_matrix_close(const std::string& what, const LLMatrix4a& actual, const LLMatrix4a& expected, F32 tolerance) + void require_matrix_close(const std::string& what, const LLMatrix4a& actual, const LLMatrix4a& expected, F32 tolerance) { - ensure_matrix_close(what, actual, Ref::of(expected), tolerance); + require_matrix_close(what, actual, Ref::of(expected), tolerance); } - void ensure_vector_close(const std::string& what, const LLVector4a& actual, const double expected[4], F32 tolerance, S32 lanes = 4) + void require_vector_close(const std::string& what, const LLVector4a& actual, const double expected[4], F32 tolerance, S32 lanes = 4) { + INFO(what); for (S32 i = 0; i < lanes; ++i) { const F32 e = F32(expected[i]); const F32 scale = llmax(1.f, fabsf(actual[i]), fabsf(e)); - std::ostringstream msg; - msg << what << " at lane " << i << ": " << actual[i] << " vs " << e; - ensure_approximately_equals_range(msg.str().c_str(), actual[i], e, tolerance * scale); + CAPTURE(i); + REQUIRE_THAT(actual[i], Catch::Matchers::WithinAbs(e, tolerance * scale)); } } - void ensure_vector_close(const std::string& what, const LLVector4a& actual, const LLVector4a& expected, F32 tolerance, S32 lanes = 4) + void require_vector_close(const std::string& what, const LLVector4a& actual, const LLVector4a& expected, F32 tolerance, S32 lanes = 4) { const double e[4] = { expected[0], expected[1], expected[2], expected[3] }; - ensure_vector_close(what, actual, e, tolerance, lanes); + require_vector_close(what, actual, e, tolerance, lanes); } // The product a * b is the identity to within tolerance of the size of // the terms that cancel in each element. - void ensure_product_is_identity(const std::string& what, const LLMatrix4a& a, const LLMatrix4a& b, F32 tolerance) + void require_product_is_identity(const std::string& what, const LLMatrix4a& a, const LLMatrix4a& b, F32 tolerance) { + INFO(what); LLMatrix4a product; product.setMul(a, b); for (S32 row = 0; row < 4; ++row) @@ -293,9 +286,8 @@ namespace { terms += fabsf(a.mMatrix[row][k] * b.mMatrix[k][col]); } - std::ostringstream msg; - msg << what << " at [" << row << "][" << col << "]"; - ensure_approximately_equals_range(msg.str().c_str(), product.mMatrix[row][col], row == col ? 1.f : 0.f, tolerance * (1.f + terms)); + CAPTURE(row, col); + REQUIRE_THAT(product.mMatrix[row][col], Catch::Matchers::WithinAbs(row == col ? 1.f : 0.f, tolerance * (1.f + terms))); } } } @@ -309,10 +301,8 @@ namespace { return LLVector4a(v.mV[0], v.mV[1], v.mV[2], 0.f); } -} - template<> template<> - void llmatrix4a_object::test<1>() + TEST_CASE("LLMatrix4a: initAll agrees with LLMatrix4::initAll", "[LLMatrix4a]") { // initAll must agree with LLMatrix4::initAll, since the joint // transform path swapped one for the other. Not bit for bit: /fp:fast @@ -337,24 +327,19 @@ namespace { for (S32 col = 0; col < 4; ++col) { - std::ostringstream msg; - msg << "initAll mismatch at [" << row << "][" << col - << "] for case " << cases; - ensure_approximately_equals(msg.str().c_str(), - actual4.mMatrix[row][col], - expected.mMatrix[row][col], - 20); + CAPTURE(cases, row, col); + REQUIRE_THAT(actual4.mMatrix[row][col], ll_test::WithinBits(expected.mMatrix[row][col], 20)); } } ++cases; } } } - ensure("every combination was covered", cases == 6 * 5 * 3); + UNSCOPED_INFO("every combination was covered"); + CHECK(cases == 6 * 5 * 3); } - template<> template<> - void llmatrix4a_object::test<2>() + TEST_CASE("LLMatrix4a: initAll clears the fourth column of the upper rows", "[LLMatrix4a]") { // The fourth column of the upper rows must be zero, which is what the // scalar version leaves in place rather than writing. Start from a @@ -369,30 +354,45 @@ namespace m.initAll(LLVector3(1.f, 1.f, 1.f), LLQuaternion(), LLVector3(1.f, 2.f, 3.f)); const LLMatrix4 out = m.toMatrix4(); - ensure_equals("row 0 pad cleared", out.mMatrix[0][3], 0.f); - ensure_equals("row 1 pad cleared", out.mMatrix[1][3], 0.f); - ensure_equals("row 2 pad cleared", out.mMatrix[2][3], 0.f); - ensure_equals("row 3 w is one", out.mMatrix[3][3], 1.f); + CHECK(out.mMatrix[0][3] == 0.f); + CHECK(out.mMatrix[1][3] == 0.f); + CHECK(out.mMatrix[2][3] == 0.f); + CHECK(out.mMatrix[3][3] == 1.f); } - template<> template<> - void llmatrix4a_object::test<3>() + TEST_CASE("LLMatrix4a: an identity rotation with unit scale is a translation matrix", "[LLMatrix4a]") { // An identity rotation with unit scale is the translation matrix. LLMatrix4a m; m.initAll(LLVector3(1.f, 1.f, 1.f), LLQuaternion(), LLVector3(4.f, 5.f, 6.f)); const LLMatrix4 out = m.toMatrix4(); - ensure_equals("m00", out.mMatrix[0][0], 1.f); - ensure_equals("m11", out.mMatrix[1][1], 1.f); - ensure_equals("m22", out.mMatrix[2][2], 1.f); - ensure_equals("tx", out.mMatrix[3][0], 4.f); - ensure_equals("ty", out.mMatrix[3][1], 5.f); - ensure_equals("tz", out.mMatrix[3][2], 6.f); + CHECK(out.mMatrix[0][0] == 1.f); + CHECK(out.mMatrix[1][1] == 1.f); + CHECK(out.mMatrix[2][2] == 1.f); + CHECK(out.mMatrix[3][0] == 4.f); + CHECK(out.mMatrix[3][1] == 5.f); + CHECK(out.mMatrix[3][2] == 6.f); } - template<> template<> - void llmatrix4a_object::test<5>() + TEST_CASE("LLMatrix4a: each basis row's length is its axis' scale", "[LLMatrix4a]") + { + // Scale lands on the corresponding basis row, not the diagonal, so a + // rotated matrix keeps each row's length equal to that axis' scale. + const LLVector3 scale(2.f, 3.f, 4.f); + LLMatrix4a m; + m.initAll(scale, LLQuaternion(0.7f, LLVector3(0.f, 0.f, 1.f)), LLVector3()); + + const LLMatrix4 out = m.toMatrix4(); + for (S32 row = 0; row < 3; ++row) + { + const F32 len = LLVector3(out.mMatrix[row]).length(); + CAPTURE(row); + REQUIRE_THAT(len, Catch::Matchers::WithinAbs(scale.mV[row], 1e-4f)); + } + } + + TEST_CASE("LLMatrix4a: set(LLMatrix4) is the inverse of toMatrix4", "[LLMatrix4a]") { // set(LLMatrix4) is the inverse of toMatrix4(). LLMatrix4 src; @@ -407,13 +407,13 @@ namespace { for (S32 col = 0; col < 4; ++col) { - ensure_equals("round trip", back.mMatrix[row][col], src.mMatrix[row][col]); + CAPTURE(row, col); + REQUIRE(back.mMatrix[row][col] == src.mMatrix[row][col]); } } } - template<> template<> - void llmatrix4a_object::test<6>() + TEST_CASE("LLMatrix4a: set(LLMatrix3) fills the basis and leaves an identity fourth row", "[LLMatrix4a]") { // set(LLMatrix3) fills the basis and leaves an identity fourth row. LLMatrix3 src; @@ -424,15 +424,14 @@ namespace LLMatrix4a m; m.set(src); const LLMatrix4 out = m.toMatrix4(); - ensure_equals("m00", out.mMatrix[0][0], 1.f); - ensure_equals("m12", out.mMatrix[1][2], 6.f); - ensure_equals("m22", out.mMatrix[2][2], 9.f); - ensure_equals("row 3 is the origin", out.mMatrix[3][0], 0.f); - ensure_equals("row 3 w is one", out.mMatrix[3][3], 1.f); + CHECK(out.mMatrix[0][0] == 1.f); + CHECK(out.mMatrix[1][2] == 6.f); + CHECK(out.mMatrix[2][2] == 9.f); + CHECK(out.mMatrix[3][0] == 0.f); + CHECK(out.mMatrix[3][3] == 1.f); } - template<> template<> - void llmatrix4a_object::test<7>() + TEST_CASE("LLMatrix4a: setMul by the identity and with aliasing matches setMulNoAlias", "[LLMatrix4a]") { LLMatrix4a a; a.initAll(LLVector3(2.f, 1.f, 3.f), @@ -442,9 +441,9 @@ namespace // Multiplying by the identity in either position is a no-op. LLMatrix4a r; r.setMul(a, LLMatrix4a::identity()); - ensure("a * identity == a", r == a); + CHECK(r == a); r.setMul(LLMatrix4a::identity(), a); - ensure("identity * a == a", r == a); + CHECK(r == a); LLMatrix4a b; b.initAll(LLVector3(1.f, 1.f, 1.f), @@ -455,21 +454,20 @@ namespace LLMatrix4a safe, fast; safe.setMul(a, b); fast.setMulNoAlias(a, b); - ensure("setMulNoAlias matches setMul", safe == fast); + CHECK(safe == fast); // setMul tolerates its result aliasing either input, which is why it // exists alongside setMulNoAlias. LLMatrix4a lhs = a; lhs.setMul(lhs, b); - ensure("result may alias the left operand", lhs == safe); + CHECK(lhs == safe); LLMatrix4a rhs = b; rhs.setMul(a, rhs); - ensure("result may alias the right operand", rhs == safe); + CHECK(rhs == safe); } - template<> template<> - void llmatrix4a_object::test<8>() + TEST_CASE("LLMatrix4a: affineTransform applies the translation and rotate does not", "[LLMatrix4a]") { // affineTransform applies the translation, rotate() does not. LLMatrix4a m; @@ -480,19 +478,18 @@ namespace LLVector4a moved; m.affineTransform(point, moved); - ensure_equals("affineTransform adds x", moved[0], 11.f); - ensure_equals("affineTransform adds y", moved[1], 22.f); - ensure_equals("affineTransform adds z", moved[2], 33.f); + CHECK(moved[0] == 11.f); + CHECK(moved[1] == 22.f); + CHECK(moved[2] == 33.f); LLVector4a turned; m.rotate(point, turned); - ensure_equals("rotate leaves x", turned[0], 1.f); - ensure_equals("rotate leaves y", turned[1], 2.f); - ensure_equals("rotate leaves z", turned[2], 3.f); + CHECK(turned[0] == 1.f); + CHECK(turned[1] == 2.f); + CHECK(turned[2] == 3.f); } - template<> template<> - void llmatrix4a_object::test<9>() + TEST_CASE("LLMatrix4a: setTranslation writes the translation and keeps row 0, and setRow writes its row", "[LLMatrix4a]") { LLMatrix4a m; m.initAll(LLVector3(2.f, 2.f, 2.f), @@ -502,21 +499,21 @@ namespace const LLVector4a basis = m.getRow<0>(); m.setTranslation(LLVector3(5.f, 6.f, 7.f)); - ensure("setTranslation leaves the basis alone", m.getRow<0>() == basis); + CHECK(m.getRow<0>() == basis); const LLMatrix4 out = m.toMatrix4(); - ensure_equals("tx", out.mMatrix[3][0], 5.f); - ensure_equals("ty", out.mMatrix[3][1], 6.f); - ensure_equals("tz", out.mMatrix[3][2], 7.f); - ensure_equals("stays affine", out.mMatrix[3][3], 1.f); + CHECK(out.mMatrix[3][0] == 5.f); + CHECK(out.mMatrix[3][1] == 6.f); + CHECK(out.mMatrix[3][2] == 7.f); + UNSCOPED_INFO("stays affine"); + CHECK(out.mMatrix[3][3] == 1.f); LLVector4a replacement; replacement.set(1.f, 2.f, 3.f, 4.f); m.setRow<2>(replacement); - ensure("setRow round trips", m.getRow<2>() == replacement); + CHECK(m.getRow<2>() == replacement); } - template<> template<> - void llmatrix4a_object::test<10>() + TEST_CASE("LLMatrix4a: matMulBoundBox spans the same box as the eight corners", "[LLMatrix4a]") { // matMulBoundBox used to transform all eight corners and take their // min and max. It now grows the transformed centre by the half @@ -569,41 +566,19 @@ namespace for (U32 side = 0; side < 2; ++side) for (U32 axis = 0; axis < 3; ++axis) { - std::ostringstream msg; - msg << "box " << (&box - BOXES) << " scale " << scale << " pos " << pos - << (side ? " max" : " min") << " axis " << axis; // Absolute slack scaled to the magnitudes in play: the // corner walk and the centre form round differently, and the // 1000x scale row amplifies whichever rounds first. const F32 magnitude = llmax(1.f, fabsf(expected[side][axis])); - ensure_approximately_equals_range(msg.str().c_str(), - out[side][axis], expected[side][axis], - magnitude * 1e-4f); + UNSCOPED_INFO("box " << (&box - BOXES) << " scale " << scale << " pos " << pos + << (side ? " max" : " min") << " axis " << axis); + REQUIRE_THAT(out[side][axis], Catch::Matchers::WithinAbs(expected[side][axis], + magnitude * 1e-4f)); } } } - template<> template<> - void llmatrix4a_object::test<4>() - { - // Scale lands on the corresponding basis row, not the diagonal, so a - // rotated matrix keeps each row's length equal to that axis' scale. - const LLVector3 scale(2.f, 3.f, 4.f); - LLMatrix4a m; - m.initAll(scale, LLQuaternion(0.7f, LLVector3(0.f, 0.f, 1.f)), LLVector3()); - - const LLMatrix4 out = m.toMatrix4(); - for (S32 row = 0; row < 3; ++row) - { - const F32 len = LLVector3(out.mMatrix[row]).length(); - std::ostringstream msg; - msg << "row " << row << " length matches its scale"; - ensure_approximately_equals_range(msg.str().c_str(), len, scale.mV[row], 1e-4f); - } - } - - template<> template<> - void llmatrix4a_object::test<11>() + TEST_CASE("LLMatrix4a: add and scale and setLerp element by element against LLMatrix4", "[LLMatrix4a]") { // The whole-matrix arithmetic, element by element against the // scalar matrix: add, scale, lerp at its ends and in between. @@ -635,31 +610,30 @@ namespace { const F32 av = a4.mMatrix[row][col]; const F32 bv = b4.mMatrix[row][col]; - std::ostringstream msg; - msg << " at [" << row << "][" << col << "]"; - ensure_equals("add" + msg.str(), sum4.mMatrix[row][col], av + bv); - ensure_equals("setMul(F32)" + msg.str(), scaled4.mMatrix[row][col], av * -1.5f); - ensure_equals("setLerp at 0" + msg.str(), zero4.mMatrix[row][col], av); - ensure_approximately_equals_range(("setLerp at 1" + msg.str()).c_str(), one4.mMatrix[row][col], bv, 1e-6f * llmax(1.f, fabsf(bv))); + CAPTURE(row, col); + REQUIRE(sum4.mMatrix[row][col] == av + bv); + REQUIRE(scaled4.mMatrix[row][col] == av * -1.5f); + REQUIRE(zero4.mMatrix[row][col] == av); + REQUIRE_THAT(one4.mMatrix[row][col], Catch::Matchers::WithinAbs(bv, 1e-6f * llmax(1.f, fabsf(bv)))); const F32 expected = (F32)((double)av + ((double)bv - av) * 0.25); - ensure_approximately_equals_range(("setLerp at a quarter" + msg.str()).c_str(), quarter4.mMatrix[row][col], expected, 1e-6f * llmax(1.f, fabsf(expected))); + REQUIRE_THAT(quarter4.mMatrix[row][col], Catch::Matchers::WithinAbs(expected, 1e-6f * llmax(1.f, fabsf(expected)))); } } // the identity, both ways - ensure("identity equals itself", LLMatrix4a::identity() == LLMatrix4a::identity()); + CHECK(LLMatrix4a::identity() == LLMatrix4a::identity()); LLMatrix4a fresh; fresh.setIdentity(); - ensure("setIdentity is the identity", fresh == LLMatrix4a::identity()); - ensure("a translation is not the identity", a != LLMatrix4a::identity()); + CHECK(fresh == LLMatrix4a::identity()); + UNSCOPED_INFO("a translation is not the identity"); + CHECK(a != LLMatrix4a::identity()); } // THE CONVENTIONS: the matrix takes a row vector on the left, so // affineTransform is the scalar matrix's v * M, a product applies its // first factor first, and a builder set before a matrix acts on the // point before the matrix does. - template<> template<> - void llmatrix4a_object::test<12>() + TEST_CASE("LLMatrix4a: row vectors on the left and a product applies its first factor first", "[LLMatrix4a]") { const F32 tolerance = 1e-5f; const LLVector3 p3(0.7f, -2.5f, 11.f); @@ -680,22 +654,23 @@ namespace const double p[4] = { p3.mV[0], p3.mV[1], p3.mV[2], 1.0 }; double expected[4]; r.transform(p, expected); - ensure_vector_close("affineTransform is the row vector through every row", point, expected, tolerance, 3); + require_vector_close("affineTransform is the row vector through every row", point, expected, tolerance, 3); const LLVector3 scalar = p3 * m.toMatrix4(); - ensure_vector_close("and the scalar matrix's v * M", point, as_point(scalar), tolerance, 3); + require_vector_close("and the scalar matrix's v * M", point, as_point(scalar), tolerance, 3); // a direction leaves the translation out LLVector4a direction; m.rotate(as_direction(p3), direction); const double d[4] = { p3.mV[0], p3.mV[1], p3.mV[2], 0.0 }; r.transform(d, expected); - ensure_vector_close("rotate is the direction through the upper rows", direction, expected, tolerance, 3); + require_vector_close("rotate is the direction through the upper rows", direction, expected, tolerance, 3); ++cases; } } } - ensure("every combination was covered", cases == 6 * 5 * 3); + UNSCOPED_INFO("every combination was covered"); + CHECK(cases == 6 * 5 * 3); // setMul(a, b) applies a first, then b LLMatrix4a a, b; @@ -703,17 +678,17 @@ namespace b.initAll(SCALES[3], ROTATIONS[4], POSITIONS[2]); LLMatrix4a ab; ab.setMul(a, b); - ensure_matrix_close("setMul is the rows of a through b", ab, Ref::of(a).then(Ref::of(b)), tolerance); + require_matrix_close("setMul is the rows of a through b", ab, Ref::of(a).then(Ref::of(b)), tolerance); LLMatrix4 scalar_ab = a.toMatrix4(); scalar_ab *= b.toMatrix4(); - ensure_matrix_close("and the scalar matrix's a *= b", ab, LLMatrix4a(scalar_ab), tolerance); + require_matrix_close("and the scalar matrix's a *= b", ab, LLMatrix4a(scalar_ab), tolerance); LLVector4a through_ab, via_a, via_b; const LLVector4a p = as_point(LLVector3(1.f, 2.f, 3.f)); ab.affineTransform(p, through_ab); a.affineTransform(p, via_a); b.affineTransform(via_a, via_b); - ensure_vector_close("setMul(a, b) is a's transform then b's", through_ab, via_b, tolerance, 3); + require_vector_close("setMul(a, b) is a's transform then b's", through_ab, via_b, tolerance, 3); // a builder ahead of a matrix acts on the point first const LLVector3 t3(4.f, -5.f, 6.f); @@ -722,7 +697,7 @@ namespace LLVector4a moved_then_a, through_translated; a.affineTransform(as_point(LLVector3(1.f, 2.f, 3.f) + t3), moved_then_a); translated.affineTransform(p, through_translated); - ensure_vector_close("setMul(translation, m) moves the point before m", through_translated, moved_then_a, tolerance, 3); + require_vector_close("setMul(translation, m) moves the point before m", through_translated, moved_then_a, tolerance, 3); const LLVector3 s3(2.f, 3.f, 0.5f); LLMatrix4a scaled; @@ -730,7 +705,7 @@ namespace LLVector4a scaled_then_a, through_scaled; a.affineTransform(as_point(LLVector3(1.f, 2.f, 3.f).scaledVec(s3)), scaled_then_a); scaled.affineTransform(p, through_scaled); - ensure_vector_close("setMul(scaling, m) scales the point before m", through_scaled, scaled_then_a, tolerance, 3); + require_vector_close("setMul(scaling, m) scales the point before m", through_scaled, scaled_then_a, tolerance, 3); const F32 angle = 0.8f; const LLVector3 axis(0.267f, -0.535f, 0.802f); @@ -739,14 +714,13 @@ namespace LLVector4a rotated_then_a, through_rotated; a.affineTransform(as_point(LLVector3(1.f, 2.f, 3.f) * LLQuaternion(angle, axis)), rotated_then_a); rotated.affineTransform(p, through_rotated); - ensure_vector_close("setMul(rotation, m) is the tree's rotation of the point before m", through_rotated, rotated_then_a, tolerance, 3); + require_vector_close("setMul(rotation, m) is the tree's rotation of the point before m", through_rotated, rotated_then_a, tolerance, 3); } // transform4, transpose, determinant, the inverses and the normal // matrix, on affine and projective matrices, against the double // reference. - template<> template<> - void llmatrix4a_object::test<13>() + TEST_CASE("LLMatrix4a: transform4/transpose/determinant/inverses/normal matrix vs double", "[LLMatrix4a]") { const F32 tolerance = 1e-5f; @@ -770,7 +744,7 @@ namespace const double vd[4] = { 0.3, -7.0, 2.5, 0.75 }; double expected[4]; r.transform(vd, expected); - ensure_vector_close("transform4 is v through every row", out, expected, tolerance); + require_vector_close("transform4 is v through every row", out, expected, tolerance); // perspectiveTransform is a point through every row, over w LLVector4a ndc; @@ -778,7 +752,7 @@ namespace const double pd[4] = { 0.3, -7.0, 2.5, 1.0 }; r.transform(pd, expected); const double divided[4] = { expected[0] / expected[3], expected[1] / expected[3], expected[2] / expected[3], 1.0 }; - ensure_vector_close("perspectiveTransform divides by w", ndc, divided, tolerance); + require_vector_close("perspectiveTransform divides by w", ndc, divided, tolerance); // transpose is a data movement: bit for bit LLMatrix4a t; @@ -787,14 +761,15 @@ namespace { for (S32 j = 0; j < 4; ++j) { - ensure_equals("setTranspose swaps the indices", t.mMatrix[i][j], m.mMatrix[j][i]); + CAPTURE(i, j); + REQUIRE(t.mMatrix[i][j] == m.mMatrix[j][i]); } } LLMatrix4a tt = m; tt.transpose(); - ensure("transpose in place", same_bytes(tt, t)); + REQUIRE(same_bytes(tt, t)); tt.transpose(); - ensure("transposed twice is itself", tt == m); + REQUIRE(tt == m); // the determinant, to within the size of the terms that cancel in // it, and the general inverse @@ -803,16 +778,16 @@ namespace { terms *= fabsf(m.mMatrix[row][0]) + fabsf(m.mMatrix[row][1]) + fabsf(m.mMatrix[row][2]) + fabsf(m.mMatrix[row][3]); } - ensure_approximately_equals_range("determinant", m.determinant(), F32(r.determinant()), 1e-6f * (1.f + terms)); + REQUIRE_THAT(m.determinant(), Catch::Matchers::WithinAbs(F32(r.determinant()), 1e-6f * (1.f + terms))); LLMatrix4a inv; - ensure("setInverse succeeds", inv.setInverse(m)); - ensure_matrix_close("setInverse matches the reference inverse", inv, r.inverse(), tolerance); + REQUIRE(inv.setInverse(m)); + require_matrix_close("setInverse matches the reference inverse", inv, r.inverse(), tolerance); LLMatrix4a in_place = m; - ensure("invert succeeds", in_place.invert()); - ensure("invert in place", in_place == inv); - ensure_product_is_identity("M * inverse(M)", m, inv, 1e-6f); - ensure_product_is_identity("inverse(M) * M", inv, m, 1e-6f); + REQUIRE(in_place.invert()); + REQUIRE(in_place == inv); + require_product_is_identity("M * inverse(M)", m, inv, 1e-6f); + require_product_is_identity("inverse(M) * M", inv, m, 1e-6f); } // A region-sized translation is a matrix whose inverse two @@ -824,9 +799,9 @@ namespace LLMatrix4a far_away; far_away.initAll(LLVector3(1.f, 1.f, 1.f), ROTATIONS[5], LLVector3(-256.f, 4096.f, 0.125f)); LLMatrix4a far_inv; - ensure("setInverse of the far matrix succeeds", far_inv.setInverse(far_away)); - ensure_product_is_identity("far M * inverse(M)", far_away, far_inv, 1e-6f); - ensure_product_is_identity("far inverse(M) * M", far_inv, far_away, 1e-6f); + REQUIRE(far_inv.setInverse(far_away)); + require_product_is_identity("far M * inverse(M)", far_away, far_inv, 1e-6f); + require_product_is_identity("far inverse(M) * M", far_inv, far_away, 1e-6f); // the affine inverse and the normal matrix, on the affine ones for (const LLMatrix4a* mp : { &affine, &rigid }) @@ -836,24 +811,25 @@ namespace const Ref inverse = r.inverse(); LLMatrix4a inv; - ensure("setAffineInverse succeeds", inv.setAffineInverse(m)); - ensure_matrix_close("setAffineInverse is the inverse of an affine matrix", inv, inverse, tolerance); + REQUIRE(inv.setAffineInverse(m)); + require_matrix_close("setAffineInverse is the inverse of an affine matrix", inv, inverse, tolerance); LLMatrix4a n; - ensure("setNormalMatrix succeeds", n.setNormalMatrix(m)); + REQUIRE(n.setNormalMatrix(m)); const Ref inverse_transpose = inverse.transposed(); for (S32 row = 0; row < 3; ++row) { + CAPTURE(row); for (S32 col = 0; col < 3; ++col) { - std::ostringstream msg; - msg << "normal matrix at [" << row << "][" << col << "]"; + CAPTURE(col); const F32 e = F32(inverse_transpose.m[row][col]); - ensure_approximately_equals_range(msg.str().c_str(), n.mMatrix[row][col], e, tolerance * llmax(1.f, fabsf(e))); + REQUIRE_THAT(n.mMatrix[row][col], Catch::Matchers::WithinAbs(e, tolerance * llmax(1.f, fabsf(e)))); } - ensure_equals("normal matrix row pad", n.mMatrix[row][3], 0.f); + REQUIRE(n.mMatrix[row][3] == 0.f); } - ensure("normal matrix has no translation", n.mMatrix[3] == LLVector4a(0.f, 0.f, 0.f, 1.f)); + UNSCOPED_INFO("normal matrix has no translation"); + REQUIRE(n.mMatrix[3] == LLVector4a(0.f, 0.f, 0.f, 1.f)); // a normal stays perpendicular to a transformed surface LLVector4a tangent(1.f, 2.f, -0.5f, 0.f), normal; @@ -861,7 +837,8 @@ namespace LLVector4a tangent_out, normal_out; m.rotate(tangent, tangent_out); n.rotate(normal, normal_out); - ensure_approximately_equals_range("perpendicular after transform", tangent_out.dot3(normal_out).getF32(), 0.f, 1e-4f * tangent_out.getLength3().getF32() * normal_out.getLength3().getF32()); + UNSCOPED_INFO("perpendicular after transform"); + REQUIRE_THAT(tangent_out.dot3(normal_out).getF32(), Catch::Matchers::WithinAbs(0.f, 1e-4f * tangent_out.getLength3().getF32() * normal_out.getLength3().getF32())); } // singular: refused, untouched @@ -869,17 +846,17 @@ namespace singular.initAll(SCALES[4], ROTATIONS[1], POSITIONS[1]); LLMatrix4a untouched; untouched.setIdentity(); - ensure("setInverse of a singular matrix is refused", !untouched.setInverse(singular)); - ensure("and leaves the target alone", untouched == LLMatrix4a::identity()); - ensure("setAffineInverse of a singular matrix is refused", !untouched.setAffineInverse(singular)); - ensure("setNormalMatrix of a singular matrix is refused", !untouched.setNormalMatrix(singular)); - ensure("still untouched", untouched == LLMatrix4a::identity()); - ensure_equals("singular determinant", singular.determinant(), 0.f); + CHECK_FALSE(untouched.setInverse(singular)); + UNSCOPED_INFO("a refused inverse leaves the target alone"); + CHECK(untouched == LLMatrix4a::identity()); + CHECK_FALSE(untouched.setAffineInverse(singular)); + CHECK_FALSE(untouched.setNormalMatrix(singular)); + UNSCOPED_INFO("still untouched"); + CHECK(untouched == LLMatrix4a::identity()); + CHECK(singular.determinant() == 0.f); } - // The builders against the formulas, written out in double. - template<> template<> - void llmatrix4a_object::test<14>() + TEST_CASE("LLMatrix4a: builders against the formulas written out in double", "[LLMatrix4a]") { const F32 tolerance = 2e-6f; @@ -887,15 +864,16 @@ namespace LLMatrix4a expected; expected.setIdentity(); expected.mMatrix[3].set(4.f, -5.f, 6.f, 1.f); - ensure("translation", same_bytes(LLMatrix4a::translation(4.f, -5.f, 6.f), expected)); - ensure("translation from a vector ignores w", same_bytes(LLMatrix4a::translation(LLVector4a(4.f, -5.f, 6.f, 9.f)), expected)); + CHECK(same_bytes(LLMatrix4a::translation(4.f, -5.f, 6.f), expected)); + UNSCOPED_INFO("translation from a vector ignores w"); + CHECK(same_bytes(LLMatrix4a::translation(LLVector4a(4.f, -5.f, 6.f, 9.f)), expected)); expected.setIdentity(); expected.mMatrix[0].set(2.f, 0.f, 0.f, 0.f); expected.mMatrix[1].set(0.f, 3.f, 0.f, 0.f); expected.mMatrix[2].set(0.f, 0.f, 0.5f, 0.f); - ensure("scaling", same_bytes(LLMatrix4a::scaling(2.f, 3.f, 0.5f), expected)); - ensure("scaling from a vector", same_bytes(LLMatrix4a::scaling(LLVector4a(2.f, 3.f, 0.5f, 9.f)), expected)); + CHECK(same_bytes(LLMatrix4a::scaling(2.f, 3.f, 0.5f), expected)); + CHECK(same_bytes(LLMatrix4a::scaling(LLVector4a(2.f, 3.f, 0.5f, 9.f)), expected)); } // a rotation about an axis: Rodrigues in double, and the tree's own @@ -911,15 +889,15 @@ namespace r.m[1][0] = t * ax * ay - s * az; r.m[1][1] = c + t * ay * ay; r.m[1][2] = t * ay * az + s * ax; r.m[2][0] = t * ax * az + s * ay; r.m[2][1] = t * ay * az - s * ax; r.m[2][2] = c + t * az * az; const LLMatrix4a rotation = LLMatrix4a::rotation(angle, as_direction(axis3)); - ensure_matrix_close("rotation about an axis", rotation, r, tolerance); - ensure_matrix_close("and from the quaternion of the same rotation", rotation, LLMatrix4a::rotation(LLQuaternion2(LLQuaternion(angle, axis3))), tolerance); + require_matrix_close("rotation about an axis", rotation, r, tolerance); + require_matrix_close("and from the quaternion of the same rotation", rotation, LLMatrix4a::rotation(LLQuaternion2(LLQuaternion(angle, axis3))), tolerance); } for (const LLQuaternion& q : ROTATIONS) { LLMatrix4a expected; expected.initAll(LLVector3(1.f, 1.f, 1.f), q, LLVector3()); - ensure_matrix_close("rotation from a quaternion is initAll's", LLMatrix4a::rotation(LLQuaternion2(q)), expected, tolerance); + require_matrix_close("rotation from a quaternion is initAll's", LLMatrix4a::rotation(LLQuaternion2(q)), expected, tolerance); } // the projections @@ -933,10 +911,10 @@ namespace r.m[2][3] = -1.0; r.m[3][2] = -(2.0 * f * n) / (f - n); r.m[3][3] = 0.0; - ensure_matrix_close("perspective", LLMatrix4a::perspective(1.1f, 1.777f, 0.25f, 1024.f), r, tolerance); + require_matrix_close("perspective", LLMatrix4a::perspective(1.1f, 1.777f, 0.25f, 1024.f), r, tolerance); r.m[2][2] = f / (n - f); r.m[3][2] = -(f * n) / (f - n); - ensure_matrix_close("perspectiveZO", LLMatrix4a::perspectiveZO(1.1f, 1.777f, 0.25f, 1024.f), r, tolerance); + require_matrix_close("perspectiveZO", LLMatrix4a::perspectiveZO(1.1f, 1.777f, 0.25f, 1024.f), r, tolerance); } { const double l = -3.0, rt = 5.0, b = -2.0, t = 7.0, n = 0.5, f = 100.0; @@ -947,10 +925,10 @@ namespace r.m[3][0] = -(rt + l) / (rt - l); r.m[3][1] = -(t + b) / (t - b); r.m[3][2] = -(f + n) / (f - n); - ensure_matrix_close("ortho", LLMatrix4a::ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), r, tolerance); + require_matrix_close("ortho", LLMatrix4a::ortho(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), r, tolerance); r.m[2][2] = -1.0 / (f - n); r.m[3][2] = -n / (f - n); - ensure_matrix_close("orthoZO", LLMatrix4a::orthoZO(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), r, tolerance); + require_matrix_close("orthoZO", LLMatrix4a::orthoZO(-3.f, 5.f, -2.f, 7.f, 0.5f, 100.f), r, tolerance); } // the pick matrix: the region centred and blown up to the viewport @@ -961,8 +939,9 @@ namespace r.m[1][1] = 720.0 / 7.0; r.m[3][0] = (1280.0 - 2.0 * (300.0 - 10.0)) / 5.0; r.m[3][1] = (720.0 - 2.0 * (400.0 - 20.0)) / 7.0; - ensure_matrix_close("pick", LLMatrix4a::pick(300.f, 400.f, 5.f, 7.f, viewport), r, tolerance); - ensure("pick of nothing is the identity", LLMatrix4a::pick(300.f, 400.f, 0.f, 7.f, viewport) == LLMatrix4a::identity()); + require_matrix_close("pick", LLMatrix4a::pick(300.f, 400.f, 5.f, 7.f, viewport), r, tolerance); + UNSCOPED_INFO("pick of nothing is the identity"); + CHECK(LLMatrix4a::pick(300.f, 400.f, 0.f, 7.f, viewport) == LLMatrix4a::identity()); } // the view: the camera's basis as columns, the eye's position in it @@ -993,27 +972,26 @@ namespace const LLVector4a at4(130.f, 60.f, 29.f, 1.f); const LLVector4a up4(0.f, 0.f, 1.f, 0.f); const LLMatrix4a look = LLMatrix4a::lookAt(eye4, at4, up4); - ensure_matrix_close("lookAt", look, r, tolerance); + require_matrix_close("lookAt", look, r, tolerance); LLVector4a dir; dir.setSub(at4, eye4); dir.mul(2.5f); - ensure_matrix_close("lookDir, any length of direction", LLMatrix4a::lookDir(eye4, dir, up4), r, tolerance); + require_matrix_close("lookDir, any length of direction", LLMatrix4a::lookDir(eye4, dir, up4), r, tolerance); // the eye lands at the origin and the point looked at on -z LLVector4a origin, ahead; look.affineTransform(eye4, origin); look.affineTransform(at4, ahead); const double zero[4] = { 0.0, 0.0, 0.0, 1.0 }; - ensure_vector_close("the eye is the origin of the view", origin, zero, 1e-4f, 3); + require_vector_close("the eye is the origin of the view", origin, zero, 1e-4f, 3); const double down_z[4] = { 0.0, 0.0, -fl, 1.0 }; - ensure_vector_close("the point looked at is straight ahead", ahead, down_z, 1e-4f, 3); + require_vector_close("the point looked at is straight ahead", ahead, down_z, 1e-4f, 3); } } // Quaternions from axes and matrices, the rotation from a quaternion, // and a scene node's TRS taken apart and put back. - template<> template<> - void llmatrix4a_object::test<15>() + TEST_CASE("LLMatrix4a: quaternions from axes and matrices and TRS taken apart and put back", "[LLMatrix4a]") { const F32 tolerance = 2e-6f; @@ -1024,11 +1002,13 @@ namespace LLQuaternion2 q; q.setAxisAngle(as_direction(axis3), angle); const LLQuaternion2 expected(LLQuaternion(angle, axis3)); - ensure("setAxisAngle is LLQuaternion(angle, axis)", q.equals(expected, 1e-6f)); + CAPTURE(angle); + REQUIRE(q.equals(expected, 1e-6f)); } LLQuaternion2 none; none.setAxisAngle(LLVector4a(0.f, 0.f, 0.f, 0.f), 1.f); - ensure("no axis is the identity", none.equals(LLQuaternion2::identity(), 1e-7f)); + UNSCOPED_INFO("no axis is the identity"); + CHECK(none.equals(LLQuaternion2::identity(), 1e-7f)); // setFromMatrix reads back what setRotation wrote, and agrees with the // scalar matrix's reading, up to the sign every rotation's two @@ -1044,12 +1024,14 @@ namespace LLVector4a negated; negated.setNeg(back.getVector4a()); const LLQuaternion2 flipped(negated); - ensure("setFromMatrix inverts setRotation", back.equals(LLQuaternion2(q), 1e-5f) || flipped.equals(LLQuaternion2(q), 1e-5f)); + UNSCOPED_INFO("setFromMatrix inverts setRotation"); + REQUIRE((back.equals(LLQuaternion2(q), 1e-5f) || flipped.equals(LLQuaternion2(q), 1e-5f))); LLMatrix3 m3; m3.setRows(LLVector3(m.mMatrix[0].getF32ptr()), LLVector3(m.mMatrix[1].getF32ptr()), LLVector3(m.mMatrix[2].getF32ptr())); const LLQuaternion2 scalar(m3.quaternion()); - ensure("and reads as LLMatrix3::quaternion does", back.equals(scalar, 1e-5f)); + UNSCOPED_INFO("and reads as LLMatrix3::quaternion does"); + REQUIRE(back.equals(scalar, 1e-5f)); } // TRS both ways @@ -1067,25 +1049,26 @@ namespace LLMatrix4a expected; expected.initAll(LLVector3(sc.getF32ptr()), q, LLVector3(tr.getF32ptr())); - ensure_matrix_close("setTRS is initAll", m, expected, tolerance); + require_matrix_close("setTRS is initAll", m, expected, tolerance); LLVector4a scale_out, translation_out; LLQuaternion2 rotation_out; - ensure("decompose succeeds", m.decompose(scale_out, rotation_out, translation_out)); + REQUIRE(m.decompose(scale_out, rotation_out, translation_out)); LLMatrix4a rebuilt; rebuilt.setTRS(translation_out, rotation_out, scale_out); - ensure_matrix_close("decompose then setTRS is the matrix", rebuilt, m, 1e-4f); - ensure("translation comes back", translation_out.equals3(tr, 1e-5f * llmax(1.f, fabsf(tr[1])))); + require_matrix_close("decompose then setTRS is the matrix", rebuilt, m, 1e-4f); + REQUIRE(translation_out.equals3(tr, 1e-5f * llmax(1.f, fabsf(tr[1])))); // a right-handed scale comes back as itself; a left-handed // one as its negation, the rotation absorbing the flip const bool right_handed = sc[0] * sc[1] * sc[2] > 0.f; if (right_handed && sc[0] > 0.f && sc[1] > 0.f && sc[2] > 0.f) { - ensure("scale comes back", scale_out.equals3(sc, 1e-4f * llmax(1.f, fabsf(sc[1])))); + REQUIRE(scale_out.equals3(sc, 1e-4f * llmax(1.f, fabsf(sc[1])))); LLVector4a negated; negated.setNeg(rotation_out.getVector4a()); - ensure("rotation comes back, up to sign", rotation_out.equals(rotation, 1e-4f) || LLQuaternion2(negated).equals(rotation, 1e-4f)); + UNSCOPED_INFO("rotation comes back, up to sign"); + REQUIRE((rotation_out.equals(rotation, 1e-4f) || LLQuaternion2(negated).equals(rotation, 1e-4f))); } } } @@ -1094,18 +1077,17 @@ namespace // a projective or singular matrix is refused LLVector4a s, t; LLQuaternion2 r; - ensure("a projection does not decompose", !LLMatrix4a::perspective(1.f, 1.f, 1.f, 10.f).decompose(s, r, t)); + CHECK_FALSE(LLMatrix4a::perspective(1.f, 1.f, 1.f, 10.f).decompose(s, r, t)); LLMatrix4a flat; flat.setTRS(translations[1], LLQuaternion2(ROTATIONS[2]), LLVector4a(1.f, 0.f, 1.f, 0.f)); - ensure("a flattened matrix does not decompose", !flat.decompose(s, r, t)); + CHECK_FALSE(flat.decompose(s, r, t)); } // Through a projection to the window and back, both depth conventions, // against the formulas in double. The way back carries the window's // resolution scaled by the depth, so it is held to the scene's size // rather than the point's. - template<> template<> - void llmatrix4a_object::test<16>() + TEST_CASE("LLMatrix4a: project and unproject in both depth conventions against double", "[LLMatrix4a]") { const F32 tolerance = 1e-4f; const F32 back_tolerance = 5e-4f; @@ -1133,21 +1115,21 @@ namespace const double expected_zo[4] = { expected[0], expected[1], ndc[2], 1.0 }; const LLVector4a win = alprojection::project(obj, modelview, projection, viewport); - ensure_vector_close("project", win, expected, tolerance); + require_vector_close("project", win, expected, tolerance); const LLVector4a win_zo = alprojection::project_zo(obj, modelview, projection, viewport); - ensure_vector_close("project_zo", win_zo, expected_zo, tolerance); + require_vector_close("project_zo", win_zo, expected_zo, tolerance); const LLVector4a back = alprojection::unproject(win, modelview, projection, viewport); - ensure_vector_close("unproject of project is the point", back, obj, back_tolerance); + require_vector_close("unproject of project is the point", back, obj, back_tolerance); const LLVector4a back_zo = alprojection::unproject_zo(win_zo, modelview, projection, viewport); - ensure_vector_close("unproject_zo of project_zo is the point", back_zo, obj, back_tolerance); + require_vector_close("unproject_zo of project_zo is the point", back_zo, obj, back_tolerance); // the same through an inverse taken once LLMatrix4a inverse; inverse.setMul(modelview, projection); - ensure("the camera inverts", inverse.invert()); - ensure_vector_close("unproject through the inverse", alprojection::unproject(win, inverse, viewport), back, 1e-6f); - ensure_vector_close("unproject_zo through the inverse", alprojection::unproject_zo(win_zo, inverse, viewport), back_zo, 1e-6f); + REQUIRE(inverse.invert()); + require_vector_close("unproject through the inverse", alprojection::unproject(win, inverse, viewport), back, 1e-6f); + require_vector_close("unproject_zo through the inverse", alprojection::unproject_zo(win_zo, inverse, viewport), back_zo, 1e-6f); } } } diff --git a/indra/llmath/tests/llmodularmath_test.cpp b/indra/llmath/tests/llmodularmath_test.cpp index c9d23c07e0..d9d7dc6a03 100644 --- a/indra/llmath/tests/llmodularmath_test.cpp +++ b/indra/llmath/tests/llmodularmath_test.cpp @@ -30,50 +30,37 @@ #include "../llmodularmath.h" -#include "../test/lltut.h" +#include -namespace tut -{ namespace { - struct modularmath_data - { - }; - typedef test_group modularmath_test; - typedef modularmath_test::object modularmath_object; - tut::modularmath_test modularmath_testcase("LLModularMath"); -} - - template<> template<> - void modularmath_object::test<1>() + TEST_CASE("LLModularMath: subtract wraps when lhs is less than rhs", "[LLModularMath]") { // lhs < rhs const U32 lhs = 0x000001; const U32 rhs = 0xFFFFFF; const U32 width = 24; U32 result = LLModularMath::subtract(lhs, rhs); - ensure_equals("diff(0x000001, 0xFFFFFF, 24)", result, 2); + CHECK(result == 2); } - template<> template<> - void modularmath_object::test<2>() + TEST_CASE("LLModularMath: subtract with lhs greater than rhs", "[LLModularMath]") { // lhs > rhs const U32 lhs = 0x000002; const U32 rhs = 0x000001; const U32 width = 24; U32 result = LLModularMath::subtract(lhs, rhs); - ensure_equals("diff(0x000002, 0x000001, 24)", result, 1); + CHECK(result == 1); } - template<> template<> - void modularmath_object::test<3>() + TEST_CASE("LLModularMath: subtract of equal values is zero", "[LLModularMath]") { // lhs == rhs const U32 lhs = 0xABCDEF; const U32 rhs = 0xABCDEF; const U32 width = 24; U32 result = LLModularMath::subtract(lhs, rhs); - ensure_equals("diff(0xABCDEF, 0xABCDEF, 24)", result, 0); + CHECK(result == 0); } } diff --git a/indra/llmath/tests/llquaternion2_test.cpp b/indra/llmath/tests/llquaternion2_test.cpp index db80e82076..3d34a8c0e0 100644 --- a/indra/llmath/tests/llquaternion2_test.cpp +++ b/indra/llmath/tests/llquaternion2_test.cpp @@ -24,7 +24,8 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include +#include #include "../llmath.h" #include "../llsimdmath.h" #include "../llvector4a.h" @@ -69,56 +70,51 @@ namespace return out; } - void ensure_quat_equals(const std::string& msg, const LLQuaternion2& actual, const LLQuaternion& expected) + void require_quat_near(const std::string& msg, const LLQuaternion2& actual, const LLQuaternion& expected) { + INFO(msg); LLQuaternion got; actual.store(got); for (S32 i = 0; i < 4; ++i) { - tut::ensure_approximately_equals_range((msg + " component").c_str(), got.mQ[i], expected.mQ[i], TOLERANCE); + CAPTURE(i); + REQUIRE_THAT(got.mQ[i], Catch::Matchers::WithinAbs(expected.mQ[i], TOLERANCE)); } } // A rotation and its negation are the same rotation. - void ensure_same_rotation(const std::string& msg, const LLQuaternion2& actual, const LLQuaternion& expected) + void require_same_rotation(const std::string& msg, const LLQuaternion2& actual, const LLQuaternion& expected) { LLQuaternion got; actual.store(got); const F32 d = got.mQ[0] * expected.mQ[0] + got.mQ[1] * expected.mQ[1] + got.mQ[2] * expected.mQ[2] + got.mQ[3] * expected.mQ[3]; - tut::ensure_approximately_equals_range(msg.c_str(), fabsf(d), 1.f, TOLERANCE); + INFO(msg); + REQUIRE_THAT(fabsf(d), Catch::Matchers::WithinAbs(1.f, TOLERANCE)); } } -namespace tut -{ namespace { struct llquaternion2_data { std::vector mRotations = rotations(); }; - typedef test_group llquaternion2_test; - typedef llquaternion2_test::object llquaternion2_object; - tut::llquaternion2_test llquaternion2_testcase("LLQuaternion2"); -} - template<> template<> - void llquaternion2_object::test<1>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: LLQuaternion round trip and the identity", "[LLQuaternion2]") { // Round trip through the scalar type, and the identity. for (const LLQuaternion& q : mRotations) { LLQuaternion2 simd(q); - ensure_quat_equals("round trip", simd, q); + require_quat_near("round trip", simd, q); } LLQuaternion ident; - ensure_quat_equals("identity", LLQuaternion2::identity(), ident); + require_quat_near("identity", LLQuaternion2::identity(), ident); } - template<> template<> - void llquaternion2_object::test<2>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setMul matches LLQuaternion operator* for every pair", "[LLQuaternion2]") { // setMul against LLQuaternion's operator*, for every ordered pair. for (const LLQuaternion& a : mRotations) @@ -127,13 +123,12 @@ namespace { LLQuaternion2 result; result.setMul(LLQuaternion2(a), LLQuaternion2(b)); - ensure_quat_equals("a * b", result, a * b); + require_quat_near("a * b", result, a * b); } } } - template<> template<> - void llquaternion2_object::test<3>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: rotating by a product rotates by a and then by b", "[LLQuaternion2]") { // The product is what rotating by a and then by b does, which is the // property the joint hierarchy relies on. @@ -153,14 +148,14 @@ namespace const LLVector3 expected = (v * a) * b; for (S32 i = 0; i < 3; ++i) { - ensure_approximately_equals_range("rotate by the product", out[i], expected.mV[i], 1e-4f); + CAPTURE(i); + REQUIRE_THAT(out[i], Catch::Matchers::WithinAbs(expected.mV[i], 1e-4f)); } } } } - template<> template<> - void llquaternion2_object::test<4>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: rotate matches LLVector3 operator* and carries w through", "[LLQuaternion2]") { // rotate() against LLVector3's operator*, and the w it was handed // comes back untouched. @@ -188,15 +183,16 @@ namespace const F32 scale = llmax(1.f, expected.magVec()); for (S32 i = 0; i < 3; ++i) { - ensure_approximately_equals_range("v * q", out[i], expected.mV[i], 1e-5f * scale); + CAPTURE(i); + REQUIRE_THAT(out[i], Catch::Matchers::WithinAbs(expected.mV[i], 1e-5f * scale)); } - ensure_approximately_equals_range("w is carried through", out[3], 7.f, TOLERANCE); + UNSCOPED_INFO("w is carried through"); + REQUIRE_THAT(out[3], Catch::Matchers::WithinAbs(7.f, TOLERANCE)); } } } - template<> template<> - void llquaternion2_object::test<5>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: dot matches the scalar dot including its sign", "[LLQuaternion2]") { // dot() against the scalar dot, sign included: the sign is what says // which way round the shorter interpolation goes. @@ -205,13 +201,12 @@ namespace for (const LLQuaternion& b : mRotations) { const LLSimdScalar d = LLQuaternion2(a).dot(LLQuaternion2(b)); - ensure_approximately_equals_range("dot", d.getF32(), dot(a, b), TOLERANCE); + REQUIRE_THAT(d.getF32(), Catch::Matchers::WithinAbs(dot(a, b), TOLERANCE)); } } } - template<> template<> - void llquaternion2_object::test<6>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setLerp ends at its inputs and stays a rotation between", "[LLQuaternion2]") { // The ends of the interpolation are the ends, and everything in // between comes out normalized. @@ -221,24 +216,24 @@ namespace { LLQuaternion2 at_start; at_start.setLerp(LLQuaternion2(a), LLQuaternion2(b), 0.f); - ensure_same_rotation("u = 0 is a", at_start, a); + require_same_rotation("u = 0 is a", at_start, a); LLQuaternion2 at_end; at_end.setLerp(LLQuaternion2(a), LLQuaternion2(b), 1.f); - ensure_same_rotation("u = 1 is b", at_end, b); + require_same_rotation("u = 1 is b", at_end, b); for (F32 u = 0.125f; u < 1.f; u += 0.125f) { LLQuaternion2 mid; mid.setLerp(LLQuaternion2(a), LLQuaternion2(b), u); - ensure("interpolated rotation is a rotation", mid.isOkRotation()); + CAPTURE(u); + REQUIRE(mid.isOkRotation()); } } } } - template<> template<> - void llquaternion2_object::test<7>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setLerp takes the shorter way round", "[LLQuaternion2]") { // The interpolation takes the shorter way round. Half way between two // rotations a quarter turn apart is an eighth of a turn from each, @@ -259,16 +254,32 @@ namespace from_negated.store(got_negated); const F32 agreement = got_b.mQ[0] * got_negated.mQ[0] + got_b.mQ[1] * got_negated.mQ[1] + got_b.mQ[2] * got_negated.mQ[2] + got_b.mQ[3] * got_negated.mQ[3]; - ensure_approximately_equals_range("the negated end interpolates the same way", - fabsf(agreement), 1.f, TOLERANCE); + UNSCOPED_INFO("the negated end interpolates the same way"); + CHECK_THAT(fabsf(agreement), Catch::Matchers::WithinAbs(1.f, TOLERANCE)); // and it really is the eighth turn, not the seven eighths one const LLQuaternion eighth(F_PI * 0.25f, axis); - ensure_same_rotation("half way is an eighth of a turn", from_b, eighth); + require_same_rotation("half way is an eighth of a turn", from_b, eighth); + } + + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: a quaternion times its conjugate is the identity", "[LLQuaternion2]") + { + // Conjugate undoes the rotation, and the product with it is identity. + for (const LLQuaternion& q : mRotations) + { + LLQuaternion2 simd(q); + LLQuaternion2 conj; + conj.setConjugate(simd); + + LLQuaternion2 product; + product.setMul(simd, conj); + + LLQuaternion ident; + require_same_rotation("q times its conjugate is identity", product, ident); + } } - template<> template<> - void llquaternion2_object::test<9>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: rotate scales by an off-unit quaternion as LLQuaternion does", "[LLQuaternion2]") { // A quaternion that has drifted off unit length scales what it // rotates, and callers get the same answer from either type. The @@ -292,13 +303,13 @@ namespace const F32 scale = llmax(1.f, expected.magVec()); for (S32 i = 0; i < 3; ++i) { - ensure_approximately_equals_range("off-unit v * q", out[i], expected.mV[i], 1e-5f * scale); + CAPTURE(i); + REQUIRE_THAT(out[i], Catch::Matchers::WithinAbs(expected.mV[i], 1e-5f * scale)); } } } - template<> template<> - void llquaternion2_object::test<10>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setInverse undoes a rotation whatever its length", "[LLQuaternion2]") { // The inverse undoes the rotation whatever length it carries, where // the conjugate only does for one of unit length. A rotation that has @@ -327,37 +338,23 @@ namespace product.store(got); for (S32 i = 0; i < 3; ++i) { - ensure_approximately_equals_range("q times its inverse has no axis", got.mQ[i], 0.f, 1e-5f); + CAPTURE(i); + UNSCOPED_INFO("q times its inverse has no axis"); + REQUIRE_THAT(got.mQ[i], Catch::Matchers::WithinAbs(0.f, 1e-5f)); } - ensure_approximately_equals_range("q times its inverse is the identity", got.mQ[3], 1.f, 1e-5f); - } - } - - template<> template<> - void llquaternion2_object::test<8>() - { - // Conjugate undoes the rotation, and the product with it is identity. - for (const LLQuaternion& q : mRotations) - { - LLQuaternion2 simd(q); - LLQuaternion2 conj; - conj.setConjugate(simd); - - LLQuaternion2 product; - product.setMul(simd, conj); - - LLQuaternion ident; - ensure_same_rotation("q times its conjugate is identity", product, ident); + UNSCOPED_INFO("q times its inverse is the identity"); + REQUIRE_THAT(got.mQ[3], Catch::Matchers::WithinAbs(1.f, 1e-5f)); } } - template<> template<> - void llquaternion2_object::test<11>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setSlerp follows the free slerp along the arc", "[LLQuaternion2]") { // Against the free slerp, which is the second implementation this is // compared with rather than the same one written twice. - auto check = [](const char* what, const LLQuaternion& a, const LLQuaternion& b, F32 u) + auto require_follows_slerp = [](const char* what, const LLQuaternion& a, const LLQuaternion& b, F32 u) { + INFO(what); + CAPTURE(u); LLQuaternion2 blended; blended.setSlerp(LLQuaternion2(a), LLQuaternion2(b), u); LLQuaternion got; @@ -365,7 +362,7 @@ namespace LLQuaternion expected = slerp(u, a, b); // q and -q are the same rotation. - tut::ensure_approximately_equals_range(what, fabsf(dot(got, expected)), 1.f, 1e-4f); + REQUIRE_THAT(fabsf(dot(got, expected)), Catch::Matchers::WithinAbs(1.f, 1e-4f)); }; // A half turn apart, which is where a lerp is furthest out and where @@ -374,14 +371,14 @@ namespace const LLQuaternion half_turn(F_PI * 0.99f, LLVector3::z_axis); for (F32 u = 0.f; u <= 1.f; u += 0.125f) { - check("a half turn follows the arc", at_zero, half_turn, u); + require_follows_slerp("a half turn follows the arc", at_zero, half_turn, u); } // A quarter turn, comfortably inside the range a lerp used to take. const LLQuaternion quarter(F_PI_BY_TWO, LLVector3::y_axis); for (F32 u = 0.f; u <= 1.f; u += 0.125f) { - check("a quarter turn follows the arc", at_zero, quarter, u); + require_follows_slerp("a quarter turn follows the arc", at_zero, quarter, u); } // About an axis that is not a basis vector. @@ -390,12 +387,11 @@ namespace const LLQuaternion oblique(2.f, axis); for (F32 u = 0.f; u <= 1.f; u += 0.125f) { - check("an oblique axis follows the arc", at_zero, oblique, u); + require_follows_slerp("an oblique axis follows the arc", at_zero, oblique, u); } } - template<> template<> - void llquaternion2_object::test<12>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setSlerp ends at its inputs and handles equal or opposite pairs", "[LLQuaternion2]") { // The ends are the ends, whatever the pair. const LLQuaternion a(0.3f, LLVector3::x_axis); @@ -405,13 +401,15 @@ namespace at_start.setSlerp(LLQuaternion2(a), LLQuaternion2(b), 0.f); LLQuaternion start; at_start.store(start); - ensure_approximately_equals_range("nought is the first", fabsf(dot(start, a)), 1.f, 1e-5f); + UNSCOPED_INFO("nought is the first"); + CHECK_THAT(fabsf(dot(start, a)), Catch::Matchers::WithinAbs(1.f, 1e-5f)); LLQuaternion2 at_end; at_end.setSlerp(LLQuaternion2(a), LLQuaternion2(b), 1.f); LLQuaternion end; at_end.store(end); - ensure_approximately_equals_range("one is the second", fabsf(dot(end, b)), 1.f, 1e-5f); + UNSCOPED_INFO("one is the second"); + CHECK_THAT(fabsf(dot(end, b)), Catch::Matchers::WithinAbs(1.f, 1e-5f)); // Two rotations that are the same rotation stay put rather than // dividing by the sine of nothing. @@ -419,8 +417,10 @@ namespace same.setSlerp(LLQuaternion2(a), LLQuaternion2(a), 0.5f); LLQuaternion held; same.store(held); - ensure_approximately_equals_range("the same pair holds", fabsf(dot(held, a)), 1.f, 1e-5f); - ensure("and stays a rotation", same.isOkRotation()); + UNSCOPED_INFO("the same pair holds"); + CHECK_THAT(fabsf(dot(held, a)), Catch::Matchers::WithinAbs(1.f, 1e-5f)); + UNSCOPED_INFO("and stays a rotation"); + CHECK(same.isOkRotation()); // Handed the far side of the same rotation, it comes back the short // way rather than through the rotations neither end asked for. @@ -433,12 +433,11 @@ namespace shortest.setSlerp(LLQuaternion2(a), LLQuaternion2(negated), 0.5f); LLQuaternion midway; shortest.store(midway); - ensure_approximately_equals_range("a rotation and its negation are one place", - fabsf(dot(midway, a)), 1.f, 1e-4f); + UNSCOPED_INFO("a rotation and its negation are one place"); + CHECK_THAT(fabsf(dot(midway, a)), Catch::Matchers::WithinAbs(1.f, 1e-4f)); } - template<> template<> - void llquaternion2_object::test<13>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setSlerp of random pairs is a rotation matching slerp", "[LLQuaternion2]") { // Every result is a rotation, over a spread of pairs and positions. std::mt19937 rng(20260906u); @@ -446,6 +445,7 @@ namespace for (S32 trial = 0; trial < 200; ++trial) { + CAPTURE(trial); LLVector3 axis_a(spread(rng), spread(rng), spread(rng)); LLVector3 axis_b(spread(rng), spread(rng), spread(rng)); if (axis_a.magVecSquared() < 1e-6f || axis_b.magVecSquared() < 1e-6f) @@ -461,18 +461,16 @@ namespace LLQuaternion2 blended; blended.setSlerp(LLQuaternion2(a), LLQuaternion2(b), u); - ensure("a blend of two rotations is a rotation", blended.isOkRotation()); + REQUIRE(blended.isOkRotation()); LLQuaternion got; blended.store(got); LLQuaternion expected = slerp(u, a, b); - ensure_approximately_equals_range("and it is the one slerp gives", - fabsf(dot(got, expected)), 1.f, 1e-3f); + REQUIRE_THAT(fabsf(dot(got, expected)), Catch::Matchers::WithinAbs(1.f, 1e-3f)); } } - template<> template<> - void llquaternion2_object::test<14>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: quantize8 and quantize16 keep a rotation within a step", "[LLQuaternion2]") { // Quantizing to eight or sixteen bits keeps a rotation a rotation, // within the step the bits allow. @@ -480,30 +478,32 @@ namespace { LLQuaternion2 q8(q); q8.quantize8(); - ensure("quantize8 is a rotation", q8.isOkRotation()); + REQUIRE(q8.isOkRotation()); LLQuaternion got8; q8.store(got8); - ensure_approximately_equals_range("quantize8 is the rotation it was given, to eight bits", fabsf(dot(got8, q)), 1.f, 1e-4f); + UNSCOPED_INFO("quantize8 keeps the rotation it was given"); + REQUIRE_THAT(fabsf(dot(got8, q)), Catch::Matchers::WithinAbs(1.f, 1e-4f)); for (S32 i = 0; i < 4; ++i) { - ensure("quantize8 component within a step", fabsf(got8.mQ[i] - q.mQ[i]) <= 2.f / 255.f); + CAPTURE(i); + REQUIRE(fabsf(got8.mQ[i] - q.mQ[i]) <= 2.f / 255.f); } LLQuaternion2 q16(q); q16.quantize16(); - ensure("quantize16 is a rotation", q16.isOkRotation()); - ensure_same_rotation("quantize16 is the rotation it was given", q16, q); + REQUIRE(q16.isOkRotation()); + require_same_rotation("quantize16 is the rotation it was given", q16, q); LLQuaternion got16; q16.store(got16); for (S32 i = 0; i < 4; ++i) { - ensure("quantize16 component within a step", fabsf(got16.mQ[i] - q.mQ[i]) <= 2.f / 65535.f); + CAPTURE(i); + REQUIRE(fabsf(got16.mQ[i] - q.mQ[i]) <= 2.f / 65535.f); } } } - template<> template<> - void llquaternion2_object::test<15>() + TEST_CASE_METHOD(llquaternion2_data, "LLQuaternion2: setConjugate flips the vector part alone", "[LLQuaternion2]") { // The conjugate flips the vector part alone, and the identity is // its own conjugate. @@ -512,13 +512,14 @@ namespace conj.setConjugate(LLQuaternion2(q)); LLQuaternion got; conj.store(got); - ensure_equals("conjugate x", got.mQ[VX], -q.mQ[VX]); - ensure_equals("conjugate y", got.mQ[VY], -q.mQ[VY]); - ensure_equals("conjugate z", got.mQ[VZ], -q.mQ[VZ]); - ensure_equals("conjugate w", got.mQ[VW], q.mQ[VW]); + CHECK(got.mQ[VX] == -q.mQ[VX]); + CHECK(got.mQ[VY] == -q.mQ[VY]); + CHECK(got.mQ[VZ] == -q.mQ[VZ]); + CHECK(got.mQ[VW] == q.mQ[VW]); LLQuaternion2 ident; ident.setConjugate(LLQuaternion2::identity()); - ensure("the identity is its own conjugate", ident.equals(LLQuaternion2::identity())); + UNSCOPED_INFO("the identity is its own conjugate"); + CHECK(ident.equals(LLQuaternion2::identity())); } } diff --git a/indra/llmath/tests/llquaternion_test.cpp b/indra/llmath/tests/llquaternion_test.cpp index a826eafa13..5a5bac459c 100644 --- a/indra/llmath/tests/llquaternion_test.cpp +++ b/indra/llmath/tests/llquaternion_test.cpp @@ -27,7 +27,8 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include +#include #include "../v4math.h" #include "../v3math.h" @@ -36,32 +37,20 @@ #include "../m3math.h" #include "../llquaternion.h" -namespace tut -{ namespace { - struct llquat_test - { - }; - typedef test_group llquat_test_t; - typedef llquat_test_t::object llquat_test_object_t; - tut::llquat_test_t tut_llquat_test("LLQuaternion"); -} - //test case for LLQuaternion::LLQuaternion(void) fn. - template<> template<> - void llquat_test_object_t::test<1>() + TEST_CASE("LLQuaternion: default constructor gives the identity quaternion", "[LLQuaternion]") { LLQuaternion llquat; - ensure("LLQuaternion::LLQuaternion() failed", 0.f == llquat.mQ[0] && - 0.f == llquat.mQ[1] && - 0.f == llquat.mQ[2] && - 1.f == llquat.mQ[3]); + CHECK(llquat.mQ[0] == 0.f); + CHECK(llquat.mQ[1] == 0.f); + CHECK(llquat.mQ[2] == 0.f); + CHECK(llquat.mQ[3] == 1.f); } //test case for explicit LLQuaternion(const LLMatrix4 &mat) fn. - template<> template<> - void llquat_test_object_t::test<2>() + TEST_CASE("LLQuaternion: LLMatrix4 initRows fills the matrix row by row", "[LLQuaternion]") { LLMatrix4 llmat; LLVector4 vector1(2.0f, 1.0f, 3.0f, 6.0f); @@ -70,26 +59,25 @@ namespace LLVector4 vector4(3.0f, 8.0f, 1.0f, 5.0f); llmat.initRows(vector1, vector2, vector3, vector4); - ensure("explicit LLQuaternion(const LLMatrix4 &mat) failed", 2.0f == llmat.mMatrix[0][0] && - 1.0f == llmat.mMatrix[0][1] && - 3.0f == llmat.mMatrix[0][2] && - 6.0f == llmat.mMatrix[0][3] && - 5.0f == llmat.mMatrix[1][0] && - 6.0f == llmat.mMatrix[1][1] && - 0.0f == llmat.mMatrix[1][2] && - 1.0f == llmat.mMatrix[1][3] && - 2.0f == llmat.mMatrix[2][0] && - 1.0f == llmat.mMatrix[2][1] && - 2.0f == llmat.mMatrix[2][2] && - 9.0f == llmat.mMatrix[2][3] && - 3.0f == llmat.mMatrix[3][0] && - 8.0f == llmat.mMatrix[3][1] && - 1.0f == llmat.mMatrix[3][2] && - 5.0f == llmat.mMatrix[3][3]); + CHECK(llmat.mMatrix[0][0] == 2.0f); + CHECK(llmat.mMatrix[0][1] == 1.0f); + CHECK(llmat.mMatrix[0][2] == 3.0f); + CHECK(llmat.mMatrix[0][3] == 6.0f); + CHECK(llmat.mMatrix[1][0] == 5.0f); + CHECK(llmat.mMatrix[1][1] == 6.0f); + CHECK(llmat.mMatrix[1][2] == 0.0f); + CHECK(llmat.mMatrix[1][3] == 1.0f); + CHECK(llmat.mMatrix[2][0] == 2.0f); + CHECK(llmat.mMatrix[2][1] == 1.0f); + CHECK(llmat.mMatrix[2][2] == 2.0f); + CHECK(llmat.mMatrix[2][3] == 9.0f); + CHECK(llmat.mMatrix[3][0] == 3.0f); + CHECK(llmat.mMatrix[3][1] == 8.0f); + CHECK(llmat.mMatrix[3][2] == 1.0f); + CHECK(llmat.mMatrix[3][3] == 5.0f); } - template<> template<> - void llquat_test_object_t::test<3>() + TEST_CASE("LLQuaternion: LLMatrix3 setRows keeps each float exactly", "[LLQuaternion]") { LLMatrix3 llmat; @@ -98,20 +86,19 @@ namespace LLVector3 vect3(566.003034f, 12.98705f, 234.764423f); llmat.setRows(vect1, vect2, vect3); - ensure("LLMatrix3::setRows fn failed.", 3.4028234660000000f == llmat.mMatrix[0][0] && - 234.56f == llmat.mMatrix[0][1] && - 4234.442234f == llmat.mMatrix[0][2] && - 741.434f == llmat.mMatrix[1][0] && - 23.00034f == llmat.mMatrix[1][1] && - 6567.223423f == llmat.mMatrix[1][2] && - 566.003034f == llmat.mMatrix[2][0] && - 12.98705f == llmat.mMatrix[2][1] && - 234.764423f == llmat.mMatrix[2][2]); + CHECK(llmat.mMatrix[0][0] == 3.4028234660000000f); + CHECK(llmat.mMatrix[0][1] == 234.56f); + CHECK(llmat.mMatrix[0][2] == 4234.442234f); + CHECK(llmat.mMatrix[1][0] == 741.434f); + CHECK(llmat.mMatrix[1][1] == 23.00034f); + CHECK(llmat.mMatrix[1][2] == 6567.223423f); + CHECK(llmat.mMatrix[2][0] == 566.003034f); + CHECK(llmat.mMatrix[2][1] == 12.98705f); + CHECK(llmat.mMatrix[2][2] == 234.764423f); } //test case for LLQuaternion(F32 x, F32 y, F32 z, F32 w), setQuatInit() and normQuat() fns. - template<> template<> - void llquat_test_object_t::test<4>() + TEST_CASE("LLQuaternion: normQuat normalises and turns a zero quaternion into identity", "[LLQuaternion]") { F32 x_val = 3.0f; F32 y_val = 2.0f; @@ -122,11 +109,10 @@ namespace res_quat.setQuatInit(x_val, y_val, z_val, w_val); res_quat.normQuat(); - ensure("LLQuaternion::normQuat() fn failed", - is_approx_equal(0.42426407f, res_quat.mQ[0]) && - is_approx_equal(0.28284273f, res_quat.mQ[1]) && - is_approx_equal(0.84852815f, res_quat.mQ[2]) && - is_approx_equal(0.14142136f, res_quat.mQ[3])); + CHECK(is_approx_equal(0.42426407f, res_quat.mQ[0])); + CHECK(is_approx_equal(0.28284273f, res_quat.mQ[1])); + CHECK(is_approx_equal(0.84852815f, res_quat.mQ[2])); + CHECK(is_approx_equal(0.14142136f, res_quat.mQ[3])); x_val = 0.0f; y_val = 0.0f; @@ -136,23 +122,20 @@ namespace res_quat.setQuatInit(x_val, y_val, z_val, w_val); res_quat.normQuat(); - ensure("LLQuaternion::normQuat() fn. failed.", - is_approx_equal(0.0f, res_quat.mQ[0]) && - is_approx_equal(0.0f, res_quat.mQ[1]) && - is_approx_equal(0.0f, res_quat.mQ[2]) && - is_approx_equal(1.0f, res_quat.mQ[3])); + CHECK(is_approx_equal(0.0f, res_quat.mQ[0])); + CHECK(is_approx_equal(0.0f, res_quat.mQ[1])); + CHECK(is_approx_equal(0.0f, res_quat.mQ[2])); + CHECK(is_approx_equal(1.0f, res_quat.mQ[3])); - ensure("LLQuaternion::normQuat() fn. failed.", - is_approx_equal(0.0f, res_quat.mQ[0]) && - is_approx_equal(0.0f, res_quat.mQ[1]) && - is_approx_equal(0.0f, res_quat.mQ[2]) && - is_approx_equal(1.0f, res_quat.mQ[3])); + CHECK(is_approx_equal(0.0f, res_quat.mQ[0])); + CHECK(is_approx_equal(0.0f, res_quat.mQ[1])); + CHECK(is_approx_equal(0.0f, res_quat.mQ[2])); + CHECK(is_approx_equal(1.0f, res_quat.mQ[3])); } //test case for conjQuat() and transQuat() fns. - template<> template<> - void llquat_test_object_t::test<5>() + TEST_CASE("LLQuaternion: conjQuat and transQuat agree on the vector part", "[LLQuaternion]") { F32 x_val = 3.0f; F32 y_val = 2.0f; @@ -166,52 +149,46 @@ namespace result.conjQuat(); result1.transQuat(); - ensure("LLQuaternion::conjQuat and LLQuaternion::transQuat failed ", - is_approx_equal(result1.mQ[0], result.mQ[0]) && - is_approx_equal(result1.mQ[1], result.mQ[1]) && - is_approx_equal(result1.mQ[2], result.mQ[2])); + CHECK(is_approx_equal(result1.mQ[0], result.mQ[0])); + CHECK(is_approx_equal(result1.mQ[1], result.mQ[1])); + CHECK(is_approx_equal(result1.mQ[2], result.mQ[2])); } //test case for dot(const LLQuaternion &a, const LLQuaternion &b) fn. - template<> template<> - void llquat_test_object_t::test<6>() + TEST_CASE("LLQuaternion: dot sums the products of the four components", "[LLQuaternion]") { LLQuaternion quat1(3.0f, 2.0f, 6.0f, 0.0f), quat2(1.0f, 1.0f, 1.0f, 1.0f); - ensure("1. The two values are different", ll_round(12.000000f, 2) == ll_round(dot(quat1, quat2), 2)); + CHECK(ll_round(12.000000f, 2) == ll_round(dot(quat1, quat2), 2)); LLQuaternion quat0(3.0f, 9.334f, 34.5f, 23.0f), quat(34.5f, 23.23f, 2.0f, 45.5f); - ensure("2. The two values are different", ll_round(1435.828807f, 2) == ll_round(dot(quat0, quat), 2)); + CHECK(ll_round(1435.828807f, 2) == ll_round(dot(quat0, quat), 2)); } //test case for LLQuaternion &LLQuaternion::constrain(F32 radians) fn. - template<> template<> - void llquat_test_object_t::test<7>() + TEST_CASE("LLQuaternion: constrain clamps only rotations wider than the cone angle", "[LLQuaternion]") { F32 radian = 60.0f; LLQuaternion quat(3.0f, 2.0f, 6.0f, 0.0f); LLQuaternion quat1; quat1 = quat.constrain(radian); - ensure("1. LLQuaternion::constrain(F32 radians) failed", - is_approx_equal_fraction(-0.423442f, quat1.mQ[0], 8) && - is_approx_equal_fraction(-0.282295f, quat1.mQ[1], 8) && - is_approx_equal_fraction(-0.846884f, quat1.mQ[2], 8) && - is_approx_equal_fraction(0.154251f, quat1.mQ[3], 8)); + CHECK(is_approx_equal_fraction(-0.423442f, quat1.mQ[0], 8)); + CHECK(is_approx_equal_fraction(-0.282295f, quat1.mQ[1], 8)); + CHECK(is_approx_equal_fraction(-0.846884f, quat1.mQ[2], 8)); + CHECK(is_approx_equal_fraction(0.154251f, quat1.mQ[3], 8)); radian = 30.0f; LLQuaternion quat0(37.50f, 12.0f, 86.023f, 40.32f); quat1 = quat0.constrain(radian); - ensure("2. LLQuaternion::constrain(F32 radians) failed", - is_approx_equal_fraction(37.500000f, quat1.mQ[0], 8) && - is_approx_equal_fraction(12.0000f, quat1.mQ[1], 8) && - is_approx_equal_fraction(86.0230f, quat1.mQ[2], 8) && - is_approx_equal_fraction(40.320000f, quat1.mQ[3], 8)); + CHECK(is_approx_equal_fraction(37.500000f, quat1.mQ[0], 8)); + CHECK(is_approx_equal_fraction(12.0000f, quat1.mQ[1], 8)); + CHECK(is_approx_equal_fraction(86.0230f, quat1.mQ[2], 8)); + CHECK(is_approx_equal_fraction(40.320000f, quat1.mQ[3], 8)); } - template<> template<> - void llquat_test_object_t::test<8>() + TEST_CASE("LLQuaternion: lerp and slerp and nlerp overloads interpolate", "[LLQuaternion]") { F32 value1 = 15.0f; LLQuaternion quat1(1.0f, 2.0f, 4.0f, 1.0f); @@ -226,43 +203,38 @@ namespace // Previous "expected" values were derived from a typo that read // q.mQ[VZ] for W instead of q.mQ[VW]. res_lerp = lerp(value1, quat1); - ensure("1. LLQuaternion lerp(F32 t, const LLQuaternion &q) failed", - is_approx_equal_fraction(0.218195f, res_lerp.mQ[0], 16) && - is_approx_equal_fraction(0.436390f, res_lerp.mQ[1], 16) && - is_approx_equal_fraction(0.872779f, res_lerp.mQ[2], 16) && - is_approx_equal_fraction(0.014546f, res_lerp.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.218195f, res_lerp.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.436390f, res_lerp.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.872779f, res_lerp.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.014546f, res_lerp.mQ[3], 16)); //test case for lerp(F32 t, const LLQuaternion &p, const LLQuaternion &q) fn. res_lerp = lerp(value1, quat1, quat2); - ensure("2. LLQuaternion lerp(F32 t, const LLQuaternion &p, const LLQuaternion &q) failed", - is_approx_equal_fraction(0.314306f, res_lerp.mQ[0], 16) && - is_approx_equal_fraction(0.116156f, res_lerp.mQ[1], 16) && - is_approx_equal_fraction(0.283559f, res_lerp.mQ[2], 16) && - is_approx_equal_fraction(0.898506f, res_lerp.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.314306f, res_lerp.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.116156f, res_lerp.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.283559f, res_lerp.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.898506f, res_lerp.mQ[3], 16)); //test case for slerp( F32 u, const LLQuaternion &a, const LLQuaternion &b ) fn. res_slerp = slerp(value1, quat1, quat2); - ensure("3. LLQuaternion slerp( F32 u, const LLQuaternion &a, const LLQuaternion &b) failed", - is_approx_equal_fraction(46.000f, res_slerp.mQ[0], 16) && - is_approx_equal_fraction(17.00f, res_slerp.mQ[1], 16) && - is_approx_equal_fraction(41.5f, res_slerp.mQ[2], 16) && - is_approx_equal_fraction(131.5f, res_slerp.mQ[3], 16)); + CHECK(is_approx_equal_fraction(46.000f, res_slerp.mQ[0], 16)); + CHECK(is_approx_equal_fraction(17.00f, res_slerp.mQ[1], 16)); + CHECK(is_approx_equal_fraction(41.5f, res_slerp.mQ[2], 16)); + CHECK(is_approx_equal_fraction(131.5f, res_slerp.mQ[3], 16)); //test case for nlerp(F32 t, const LLQuaternion &a, const LLQuaternion &b) fn. res_nlerp = nlerp(value1, quat1, quat2); - ensure("4. LLQuaternion nlerp(F32 t, const LLQuaternion &a, const LLQuaternion &b) failed", - is_approx_equal_fraction(0.314306f, res_nlerp.mQ[0], 16) && - is_approx_equal_fraction(0.116157f, res_nlerp.mQ[1], 16) && - is_approx_equal_fraction(0.283559f, res_nlerp.mQ[2], 16) && - is_approx_equal_fraction(0.898506f, res_nlerp.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.314306f, res_nlerp.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.116157f, res_nlerp.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.283559f, res_nlerp.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.898506f, res_nlerp.mQ[3], 16)); //test case for nlerp(F32 t, const LLQuaternion &q) fn. res_slerp = slerp(value1, quat1); - ensure("5. LLQuaternion slerp(F32 t, const LLQuaternion &q) failed", - is_approx_equal_fraction(1.0f, res_slerp.mQ[0], 16) && - is_approx_equal_fraction(2.0f, res_slerp.mQ[1], 16) && - is_approx_equal_fraction(4.0000f, res_slerp.mQ[2], 16) && - is_approx_equal_fraction(1.000f, res_slerp.mQ[3], 16)); + CHECK(is_approx_equal_fraction(1.0f, res_slerp.mQ[0], 16)); + CHECK(is_approx_equal_fraction(2.0f, res_slerp.mQ[1], 16)); + CHECK(is_approx_equal_fraction(4.0000f, res_slerp.mQ[2], 16)); + CHECK(is_approx_equal_fraction(1.000f, res_slerp.mQ[3], 16)); LLQuaternion quat3(2.0f, 1.0f, 5.5f, 10.5f); LLQuaternion res_nlerp1; @@ -273,11 +245,10 @@ namespace // pre-normalize r = (200, 100, 550, 951) (W = 1 + 100*(10.5 - 1) = 951) // |r| = sqrt(1256901) ~= 1121.116 // r/|r| ~= (0.178394, 0.089197, 0.490583, 0.848262) - ensure("6. LLQuaternion nlerp(F32 t, const LLQuaternion &q) failed", - is_approx_equal_fraction(0.178394f, res_nlerp1.mQ[0], 16) && - is_approx_equal_fraction(0.089197f, res_nlerp1.mQ[1], 16) && - is_approx_equal_fraction(0.490583f, res_nlerp1.mQ[2], 16) && - is_approx_equal_fraction(0.848262f, res_nlerp1.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.178394f, res_nlerp1.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.089197f, res_nlerp1.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.490583f, res_nlerp1.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.848262f, res_nlerp1.mQ[3], 16)); //test case for lerp(F32 t, const LLQuaternion &q) fn. // value1 = 100, quat2 = (4, 3, 6.5, 9.7): @@ -285,25 +256,23 @@ namespace // |r| = sqrt(1431141) ~= 1196.303 // r/|r| ~= (0.334363, 0.250773, 0.543341, 0.728076) res_lerp = lerp(value1, quat2); - ensure("7. LLQuaternion lerp(F32 t, const LLQuaternion &q) failed", - is_approx_equal_fraction(0.334363f, res_lerp.mQ[0], 16) && - is_approx_equal_fraction(0.250773f, res_lerp.mQ[1], 16) && - is_approx_equal_fraction(0.543341f, res_lerp.mQ[2], 16) && - is_approx_equal_fraction(0.728076f, res_lerp.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.334363f, res_lerp.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.250773f, res_lerp.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.543341f, res_lerp.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.728076f, res_lerp.mQ[3], 16)); } - template<> template<> - void llquat_test_object_t::test<9>() + TEST_CASE("LLQuaternion: operator* and operator+ and operator- on two quaternions", "[LLQuaternion]") { //test case for LLQuaternion operator*(const LLQuaternion &a, const LLQuaternion &b) fn LLQuaternion quat1(1.0f, 2.5f, 3.5f, 5.5f); LLQuaternion quat2(4.0f, 3.0f, 5.0f, 1.0f); LLQuaternion result = quat1 * quat2; - ensure("1. LLQuaternion Operator* failed", (21.0f == result.mQ[0]) && - (10.0f == result.mQ[1]) && - (38.0f == result.mQ[2]) && - (-23.5f == result.mQ[3])); + CHECK(result.mQ[0] == 21.0f); + CHECK(result.mQ[1] == 10.0f); + CHECK(result.mQ[2] == 38.0f); + CHECK(result.mQ[3] == -23.5f); LLQuaternion quat3(2341.340f, 2352.345f, 233.25f, 7645.5f); LLQuaternion quat4(674.067f, 893.0897f, 578.0f, 231.0f); @@ -311,165 +280,133 @@ namespace // Products in the millions: the build contracts and reassociates // the arithmetic, so the last bit or two of each is the // machine's, not the one the constants were written on. - ensure("2. LLQuaternion Operator* failed", is_approx_equal_fraction(4543086.5f, result.mQ[0], 2) && - is_approx_equal_fraction(8567578.0f, result.mQ[1], 2) && - is_approx_equal_fraction(3967591.25f, result.mQ[2], 2) && - is_approx_equal_fraction(-2047783.25f, result.mQ[3], 2)); + CHECK(is_approx_equal_fraction(4543086.5f, result.mQ[0], 2)); + CHECK(is_approx_equal_fraction(8567578.0f, result.mQ[1], 2)); + CHECK(is_approx_equal_fraction(3967591.25f, result.mQ[2], 2)); + CHECK(is_approx_equal_fraction(-2047783.25f, result.mQ[3], 2)); //inline LLQuaternion operator+(const LLQuaternion &a, const LLQuaternion &b)fn. result = quat1 + quat2; - ensure("3. LLQuaternion operator+ failed", (5.0f == result.mQ[0]) && - (5.5f == result.mQ[1]) && - (8.5f == result.mQ[2]) && - (6.5f == result.mQ[3])); + CHECK(result.mQ[0] == 5.0f); + CHECK(result.mQ[1] == 5.5f); + CHECK(result.mQ[2] == 8.5f); + CHECK(result.mQ[3] == 6.5f); result = quat3 + quat4; - ensure( - "4. LLQuaternion operator+ failed", - is_approx_equal(3015.407227f, result.mQ[0]) && - is_approx_equal(3245.434570f, result.mQ[1]) && - (811.25f == result.mQ[2]) && - (7876.5f == result.mQ[3])); + CHECK(is_approx_equal(3015.407227f, result.mQ[0])); + CHECK(is_approx_equal(3245.434570f, result.mQ[1])); + CHECK(result.mQ[2] == 811.25f); + CHECK(result.mQ[3] == 7876.5f); //inline LLQuaternion operator-(const LLQuaternion &a, const LLQuaternion &b) fn result = quat1 - quat2; - ensure( - "5. LLQuaternion operator-(const LLQuaternion &a, const LLQuaternion &b) failed", - (-3.0f == result.mQ[0]) && - (-0.5f == result.mQ[1]) && - (-1.5f == result.mQ[2]) && - (4.5f == result.mQ[3])); + CHECK(result.mQ[0] == -3.0f); + CHECK(result.mQ[1] == -0.5f); + CHECK(result.mQ[2] == -1.5f); + CHECK(result.mQ[3] == 4.5f); result = quat3 - quat4; - ensure( - "6. LLQuaternion operator-(const LLQuaternion &a, const LLQuaternion &b) failed", - is_approx_equal(1667.273071f, result.mQ[0]) && - is_approx_equal(1459.255249f, result.mQ[1]) && - (-344.75f == result.mQ[2]) && - (7414.50f == result.mQ[3])); + CHECK(is_approx_equal(1667.273071f, result.mQ[0])); + CHECK(is_approx_equal(1459.255249f, result.mQ[1])); + CHECK(result.mQ[2] == -344.75f); + CHECK(result.mQ[3] == 7414.50f); } //test case for LLVector4 operator*(const LLVector4 &a, const LLQuaternion &rot) fn. - template<> template<> - void llquat_test_object_t::test<10>() + TEST_CASE("LLQuaternion: operator* rotates an LLVector4 by a quaternion", "[LLQuaternion]") { LLVector4 vect(12.0f, 5.0f, 60.0f, 75.1f); LLQuaternion quat(2323.034f, 23.5f, 673.23f, 57667.5f); LLVector4 result = vect * quat; - ensure( - "1. LLVector4 operator*(const LLVector4 &a, const LLQuaternion &rot) failed", - is_approx_equal(39928406016.0f, result.mV[0]) && - // gcc on x86 actually gives us more precision than we were expecting, verified with -ffloat-store - we forgive this - (1457802240.0f >= result.mV[1]) && // gcc+x86+linux - (1457800960.0f <= result.mV[1]) && // elsewhere - is_approx_equal(200580612096.0f, result.mV[2]) && - (75.099998f == result.mV[3])); + CHECK(is_approx_equal(39928406016.0f, result.mV[0])); + CHECK(1457802240.0f >= result.mV[1]); + CHECK(1457800960.0f <= result.mV[1]); + CHECK(is_approx_equal(200580612096.0f, result.mV[2])); + CHECK(result.mV[3] == 75.099998f); LLVector4 vect1(22.0f, 45.0f, 40.0f, 78.1f); LLQuaternion quat1(2.034f, 45.5f, 37.23f, 7.5f); result = vect1 * quat1; - ensure( - "2. LLVector4 operator*(const LLVector4 &a, const LLQuaternion &rot) failed", - is_approx_equal(-58153.5390f, result.mV[0]) && - is_approx_equal(183787.8125f, result.mV[1]) && - is_approx_equal(116864.164063f, result.mV[2]) && - is_approx_equal(78.099998f, result.mV[3])); + CHECK(is_approx_equal(-58153.5390f, result.mV[0])); + CHECK(is_approx_equal(183787.8125f, result.mV[1])); + CHECK(is_approx_equal(116864.164063f, result.mV[2])); + CHECK(is_approx_equal(78.099998f, result.mV[3])); } //test case for LLVector3 operator*(const LLVector3 &a, const LLQuaternion &rot) fn. - template<> template<> - void llquat_test_object_t::test<11>() + TEST_CASE("LLQuaternion: operator* rotates an LLVector3 by a quaternion", "[LLQuaternion]") { LLVector3 vect(12.0f, 5.0f, 60.0f); LLQuaternion quat(23.5f, 6.5f, 3.23f, 56.5f); LLVector3 result = vect * quat; - ensure( - "1. LLVector3 operator*(const LLVector3 &a, const LLQuaternion &rot) failed", - is_approx_equal(97182.953125f,result.mV[0]) && - is_approx_equal(-135405.640625f, result.mV[1]) && - is_approx_equal(162986.140f, result.mV[2])); + CHECK(is_approx_equal(97182.953125f,result.mV[0])); + CHECK(is_approx_equal(-135405.640625f, result.mV[1])); + CHECK(is_approx_equal(162986.140f, result.mV[2])); LLVector3 vect1(5.0f, 40.0f, 78.1f); LLQuaternion quat1(2.034f, 45.5f, 37.23f, 7.5f); result = vect1 * quat1; - ensure( - "2. LLVector3 operator*(const LLVector3 &a, const LLQuaternion &rot) failed", - is_approx_equal(33217.703f, result.mV[0]) && - is_approx_equal(295383.8125f, result.mV[1]) && - is_approx_equal(84718.140f, result.mV[2])); + CHECK(is_approx_equal(33217.703f, result.mV[0])); + CHECK(is_approx_equal(295383.8125f, result.mV[1])); + CHECK(is_approx_equal(84718.140f, result.mV[2])); } //test case for LLVector3d operator*(const LLVector3d &a, const LLQuaternion &rot) fn. - template<> template<> - void llquat_test_object_t::test<12>() + TEST_CASE("LLQuaternion: operator* rotates an LLVector3d by a quaternion", "[LLQuaternion]") { LLVector3d vect(-2.0f, 5.0f, -6.0f); LLQuaternion quat(-3.5f, 4.5f, 3.5f, 6.5f); LLVector3d result = vect * quat; - ensure( - "1. LLVector3d operator*(const LLVector3d &a, const LLQuaternion &rot) failed ", - (-633.0f == result.mdV[0]) && - (-300.0f == result.mdV[1]) && - (-36.0f == result.mdV[2])); + CHECK(result.mdV[0] == -633.0f); + CHECK(result.mdV[1] == -300.0f); + CHECK(result.mdV[2] == -36.0f); LLVector3d vect1(5.0f, -4.5f, 8.21f); LLQuaternion quat1(2.0f, 4.5f, -7.2f, 9.5f); result = vect1 * quat1; - ensure( - "2. LLVector3d operator*(const LLVector3d &a, const LLQuaternion &rot) failed", - is_approx_equal_fraction(-120.29f, (F32) result.mdV[0], 8) && - is_approx_equal_fraction(-1683.958f, (F32) result.mdV[1], 8) && - is_approx_equal_fraction(516.56f, (F32) result.mdV[2], 8)); + CHECK(is_approx_equal_fraction(-120.29f, (F32) result.mdV[0], 8)); + CHECK(is_approx_equal_fraction(-1683.958f, (F32) result.mdV[1], 8)); + CHECK(is_approx_equal_fraction(516.56f, (F32) result.mdV[2], 8)); LLVector3d vect2(2.0f, 3.5f, 1.1f); LLQuaternion quat2(1.0f, 4.0f, 2.0f, 5.0f); result = vect2 * quat2; - ensure( - "3. LLVector3d operator*(const LLVector3d &a, const LLQuaternion &rot) failed", - is_approx_equal_fraction(18.400001f, (F32) result.mdV[0], 8) && - is_approx_equal_fraction(188.6f, (F32) result.mdV[1], 8) && - is_approx_equal_fraction(32.20f, (F32) result.mdV[2], 8)); + CHECK(is_approx_equal_fraction(18.400001f, (F32) result.mdV[0], 8)); + CHECK(is_approx_equal_fraction(188.6f, (F32) result.mdV[1], 8)); + CHECK(is_approx_equal_fraction(32.20f, (F32) result.mdV[2], 8)); } //test case for inline LLQuaternion operator-(const LLQuaternion &a) fn. - template<> template<> - void llquat_test_object_t::test<13>() + TEST_CASE("LLQuaternion: unary operator- negates every component", "[LLQuaternion]") { LLQuaternion quat(23.5f, 34.5f, 16723.4f, 324.7f); LLQuaternion result = -quat; - ensure( - "1. LLQuaternion operator-(const LLQuaternion &a) failed", - (-23.5f == result.mQ[0]) && - (-34.5f == result.mQ[1]) && - (-16723.4f == result.mQ[2]) && - (-324.7f == result.mQ[3])); + CHECK(result.mQ[0] == -23.5f); + CHECK(result.mQ[1] == -34.5f); + CHECK(result.mQ[2] == -16723.4f); + CHECK(result.mQ[3] == -324.7f); LLQuaternion quat1(-3.5f, -34.5f, -16.4f, -154.7f); result = -quat1; - ensure( - "2. LLQuaternion operator-(const LLQuaternion &a) failed.", - (3.5f == result.mQ[0]) && - (34.5f == result.mQ[1]) && - (16.4f == result.mQ[2]) && - (154.7f == result.mQ[3])); + CHECK(result.mQ[0] == 3.5f); + CHECK(result.mQ[1] == 34.5f); + CHECK(result.mQ[2] == 16.4f); + CHECK(result.mQ[3] == 154.7f); } //test case for inline LLQuaternion operator*(F32 a, const LLQuaternion &q) and //inline LLQuaternion operator*(F32 a, const LLQuaternion &q) fns. - template<> template<> - void llquat_test_object_t::test<14>() + TEST_CASE("LLQuaternion: scalar operator* gives the same on either side", "[LLQuaternion]") { LLQuaternion quat_value(9.0f, 8.0f, 7.0f, 6.0f); F32 a =3.5f; LLQuaternion result = a * quat_value; LLQuaternion result1 = quat_value * a; - ensure( - "1. LLQuaternion operator* failed", - (result.mQ[0] == result1.mQ[0]) && - (result.mQ[1] == result1.mQ[1]) && - (result.mQ[2] == result1.mQ[2]) && - (result.mQ[3] == result1.mQ[3])); + CHECK(result.mQ[0] == result1.mQ[0]); + CHECK(result.mQ[1] == result1.mQ[1]); + CHECK(result.mQ[2] == result1.mQ[2]); + CHECK(result.mQ[3] == result1.mQ[3]); LLQuaternion quat_val(9454.0f, 43568.3450f, 456343247.0343f, 2346.03434f); @@ -477,46 +414,37 @@ namespace result = a * quat_val; result1 = quat_val * a; - ensure( - "2. LLQuaternion operator* failed", - (result.mQ[0] == result1.mQ[0]) && - (result.mQ[1] == result1.mQ[1]) && - (result.mQ[2] == result1.mQ[2]) && - (result.mQ[3] == result1.mQ[3])); + CHECK(result.mQ[0] == result1.mQ[0]); + CHECK(result.mQ[1] == result1.mQ[1]); + CHECK(result.mQ[2] == result1.mQ[2]); + CHECK(result.mQ[3] == result1.mQ[3]); } - template<> template<> - void llquat_test_object_t::test<15>() + TEST_CASE("LLQuaternion: operator~ conjugates and operator== matches equal quaternions", "[LLQuaternion]") { // test cases for inline LLQuaternion operator~(const LLQuaternion &a) LLQuaternion quat_val(2323.634f, -43535.4f, 3455.88f, -32232.45f); LLQuaternion result = ~quat_val; - ensure( - "1. LLQuaternion operator~(const LLQuaternion &a) failed ", - (-2323.634f == result.mQ[0]) && - (43535.4f == result.mQ[1]) && - (-3455.88f == result.mQ[2]) && - (-32232.45f == result.mQ[3])); + CHECK(result.mQ[0] == -2323.634f); + CHECK(result.mQ[1] == 43535.4f); + CHECK(result.mQ[2] == -3455.88f); + CHECK(result.mQ[3] == -32232.45f); //test case for inline bool LLQuaternion::operator==(const LLQuaternion &b) const LLQuaternion quat_val1(2323.634f, -43535.4f, 3455.88f, -32232.45f); LLQuaternion quat_val2(2323.634f, -43535.4f, 3455.88f, -32232.45f); - ensure( - "2. LLQuaternion::operator==(const LLQuaternion &b) failed", - quat_val1 == quat_val2); + CHECK(quat_val1 == quat_val2); } - template<> template<> - void llquat_test_object_t::test<16>() + TEST_CASE("LLQuaternion: operator!= tells apart quaternions differing in one component", "[LLQuaternion]") { //test case for inline bool LLQuaternion::operator!=(const LLQuaternion &b) const LLQuaternion quat_val1(2323.634f, -43535.4f, 3455.88f, -32232.45f); LLQuaternion quat_val2(0, -43535.4f, 3455.88f, -32232.45f); - ensure("LLQuaternion::operator!=(const LLQuaternion &b) failed", quat_val1 != quat_val2); + CHECK(quat_val1 != quat_val2); } - template<> template<> - void llquat_test_object_t::test<17>() + TEST_CASE("LLQuaternion: mayaQ builds the rotation for each axis order", "[LLQuaternion]") { //test case for LLQuaternion mayaQ(F32 xRot, F32 yRot, F32 zRot, LLQuaternion::Order order) F32 x = 2.0f; @@ -524,145 +452,124 @@ namespace F32 z = 3.0f; LLQuaternion result = mayaQ(x, y, z, LLQuaternion::XYZ); - ensure( - "1. LLQuaternion mayaQ(F32 xRot, F32 yRot, F32 zRot, LLQuaternion::Order order) failed for XYZ", - is_approx_equal_fraction(0.0172174f, result.mQ[0], 16) && - is_approx_equal_fraction(0.009179f, result.mQ[1], 16) && - is_approx_equal_fraction(0.026020f, result.mQ[2], 16) && - is_approx_equal_fraction(0.999471f, result.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.0172174f, result.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.009179f, result.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.026020f, result.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.999471f, result.mQ[3], 16)); LLQuaternion result1 = mayaQ(x, y, z, LLQuaternion::YZX); - ensure( - "2. LLQuaternion mayaQ(F32 xRot, F32 yRot, F32 zRot, LLQuaternion::Order order) failed for XYZ", - is_approx_equal_fraction(0.017217f, result1.mQ[0], 16) && - is_approx_equal_fraction(0.008265f, result1.mQ[1], 16) && - is_approx_equal_fraction(0.026324f, result1.mQ[2], 16) && - is_approx_equal_fraction(0.999471f, result1.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.017217f, result1.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.008265f, result1.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.026324f, result1.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.999471f, result1.mQ[3], 16)); LLQuaternion result2 = mayaQ(x, y, z, LLQuaternion::ZXY); - ensure( - "3. LLQuaternion mayaQ(F32 xRot, F32 yRot, F32 zRot, LLQuaternion::Order order) failed for ZXY", - is_approx_equal_fraction(0.017674f, result2.mQ[0], 16) && - is_approx_equal_fraction(0.008265f, result2.mQ[1], 16) && - is_approx_equal_fraction(0.026020f, result2.mQ[2], 16) && - is_approx_equal_fraction(0.999471f, result2.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.017674f, result2.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.008265f, result2.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.026020f, result2.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.999471f, result2.mQ[3], 16)); LLQuaternion result3 = mayaQ(x, y, z, LLQuaternion::XZY); - ensure( - "4. TLLQuaternion mayaQ(F32 xRot, F32 yRot, F32 zRot, LLQuaternion::Order order) failed for XZY", - is_approx_equal_fraction(0.017674f, result3.mQ[0], 16) && - is_approx_equal_fraction(0.009179f, result3.mQ[1], 16) && - is_approx_equal_fraction(0.026020f, result3.mQ[2], 16) && - is_approx_equal_fraction(0.999463f, result3.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.017674f, result3.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.009179f, result3.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.026020f, result3.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.999463f, result3.mQ[3], 16)); LLQuaternion result4 = mayaQ(x, y, z, LLQuaternion::YXZ); - ensure( - "5. LLQuaternion mayaQ(F32 xRot, F32 yRot, F32 zRot, LLQuaternion::Order order) failed for YXZ", - is_approx_equal_fraction(0.017217f, result4.mQ[0], 16) && - is_approx_equal_fraction(0.009179f, result4.mQ[1], 16) && - is_approx_equal_fraction(0.026324f, result4.mQ[2], 16) && - is_approx_equal_fraction(0.999463f, result4.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.017217f, result4.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.009179f, result4.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.026324f, result4.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.999463f, result4.mQ[3], 16)); LLQuaternion result5 = mayaQ(x, y, z, LLQuaternion::ZYX); - ensure( - "6. LLQuaternion mayaQ(F32 xRot, F32 yRot, F32 zRot, LLQuaternion::Order order) failed for ZYX", - is_approx_equal_fraction(0.017674f, result5.mQ[0], 16) && - is_approx_equal_fraction(0.008265f, result5.mQ[1], 16) && - is_approx_equal_fraction(0.026324f, result5.mQ[2], 16) && - is_approx_equal_fraction(0.999463f, result5.mQ[3], 16)); + CHECK(is_approx_equal_fraction(0.017674f, result5.mQ[0], 16)); + CHECK(is_approx_equal_fraction(0.008265f, result5.mQ[1], 16)); + CHECK(is_approx_equal_fraction(0.026324f, result5.mQ[2], 16)); + CHECK(is_approx_equal_fraction(0.999463f, result5.mQ[3], 16)); } - template<> template<> - void llquat_test_object_t::test<18>() + TEST_CASE("LLQuaternion: operator<< prints the components in braces", "[LLQuaternion]") { // test case for friend std::ostream& operator<<(std::ostream &s, const LLQuaternion &a) fn LLQuaternion a(1.0f, 1.0f, 1.0f, 1.0f); std::ostringstream result_value; result_value << a; - ensure_equals("1. Operator << failed", result_value.str(), "{ 1, 1, 1, 1 }"); + CHECK(result_value.str() == "{ 1, 1, 1, 1 }"); LLQuaternion b(-31.034f, 231.2340f, 3451.344320f, -341.0f); std::ostringstream result_value1; result_value1 << b; - ensure_equals("2. Operator << failed", result_value1.str(), "{ -31.034, 231.234, 3451.34, -341 }"); + CHECK(result_value1.str() == "{ -31.034, 231.234, 3451.34, -341 }"); LLQuaternion c(1.0f, 2.2f, 3.3f, 4.4f); result_value << c; - ensure_equals("3. Operator << failed", result_value.str(), "{ 1, 1, 1, 1 }{ 1, 2.2, 3.3, 4.4 }"); + CHECK(result_value.str() == "{ 1, 1, 1, 1 }{ 1, 2.2, 3.3, 4.4 }"); } - template<> template<> - void llquat_test_object_t::test<19>() + TEST_CASE("LLQuaternion: OrderToString names each axis order", "[LLQuaternion]") { //test case for const char *OrderToString( const LLQuaternion::Order order ) fn const char* result = OrderToString(LLQuaternion::XYZ); - ensure("1. OrderToString failed for XYZ", (0 == strcmp("XYZ", result))); + CHECK(std::string_view(result) == "XYZ"); result = OrderToString(LLQuaternion::YZX); - ensure("2. OrderToString failed for YZX", (0 == strcmp("YZX", result))); + CHECK(std::string_view(result) == "YZX"); result = OrderToString(LLQuaternion::ZXY); - ensure( - "3. OrderToString failed for ZXY", - (0 == strcmp("ZXY", result)) && - (0 != strcmp("XYZ", result)) && - (0 != strcmp("YXZ", result)) && - (0 != strcmp("ZYX", result)) && - (0 != strcmp("XYZ", result))); + CHECK(std::string_view(result) == "ZXY"); + CHECK(std::string_view(result) != "XYZ"); + CHECK(std::string_view(result) != "YXZ"); + CHECK(std::string_view(result) != "ZYX"); + CHECK(std::string_view(result) != "XYZ"); result = OrderToString(LLQuaternion::XZY); - ensure("4. OrderToString failed for XZY", (0 == strcmp("XZY", result))); + CHECK(std::string_view(result) == "XZY"); result = OrderToString(LLQuaternion::ZYX); - ensure("5. OrderToString failed for ZYX", (0 == strcmp("ZYX", result))); + CHECK(std::string_view(result) == "ZYX"); result = OrderToString(LLQuaternion::YXZ); - ensure("6.OrderToString failed for YXZ", (0 == strcmp("YXZ", result))); + CHECK(std::string_view(result) == "YXZ"); } - template<> template<> - void llquat_test_object_t::test<20>() + TEST_CASE("LLQuaternion: StringToOrder maps each name to its axis order", "[LLQuaternion]") { //test case for LLQuaternion::Order StringToOrder( const char *str ) fn int result = StringToOrder("XYZ"); - ensure("1. LLQuaternion::Order StringToOrder(const char *str ) failed for XYZ", 0 == result); + CHECK(result == 0); result = StringToOrder("YZX"); - ensure("2. LLQuaternion::Order StringToOrder(const char *str) failed for YZX", 1 == result); + CHECK(result == 1); result = StringToOrder("ZXY"); - ensure("3. LLQuaternion::Order StringToOrder(const char *str) failed for ZXY", 2 == result); + CHECK(result == 2); result = StringToOrder("XZY"); - ensure("4. LLQuaternion::Order StringToOrder(const char *str) failed for XZY", 3 == result); + CHECK(result == 3); result = StringToOrder("YXZ"); - ensure("5. LLQuaternion::Order StringToOrder(const char *str) failed for YXZ", 4 == result); + CHECK(result == 4); result = StringToOrder("ZYX"); - ensure("6. LLQuaternion::Order StringToOrder(const char *str) failed for ZYX", 5 == result); + CHECK(result == 5); } - template<> template<> - void llquat_test_object_t::test<21>() + TEST_CASE("LLQuaternion: getAngleAxis recovers the angle and the normalised axis", "[LLQuaternion]") { //void LLQuaternion::getAngleAxis(F32* angle, LLVector3 &vec) const fn F32 angle_value = 90.0f; LLVector3 vect(12.0f, 4.0f, 1.0f); LLQuaternion llquat(angle_value, vect); llquat.getAngleAxis(&angle_value, vect); - ensure( - "LLQuaternion::getAngleAxis(F32* angle, LLVector3 &vec) failed", - is_approx_equal_fraction(2.035406f, angle_value, 16) && - is_approx_equal_fraction(0.315244f, vect.mV[1], 16) && - is_approx_equal_fraction(0.078811f, vect.mV[2], 16) && - is_approx_equal_fraction(0.945733f, vect.mV[0], 16)); + CHECK(is_approx_equal_fraction(2.035406f, angle_value, 16)); + CHECK(is_approx_equal_fraction(0.315244f, vect.mV[1], 16)); + CHECK(is_approx_equal_fraction(0.078811f, vect.mV[2], 16)); + CHECK(is_approx_equal_fraction(0.945733f, vect.mV[0], 16)); } - template<> template<> - void llquat_test_object_t::test<22>() + TEST_CASE("LLQuaternion: getEulerAngles leaves an identity quaternion unchanged", "[LLQuaternion]") { //test case for void LLQuaternion::getEulerAngles(F32 *roll, F32 *pitch, F32 *yaw) const fn F32 roll = -12.0f; @@ -671,24 +578,21 @@ namespace LLQuaternion llquat; llquat.getEulerAngles(&roll, &pitch, &yaw); - ensure( - "LLQuaternion::getEulerAngles(F32 *roll, F32 *pitch, F32 *yaw) failed", - is_approx_equal(0.000f, llquat.mQ[0]) && - is_approx_equal(0.000f, llquat.mQ[1]) && - is_approx_equal(0.000f, llquat.mQ[2]) && - is_approx_equal(1.000f, llquat.mQ[3])); + CHECK(is_approx_equal(0.000f, llquat.mQ[0])); + CHECK(is_approx_equal(0.000f, llquat.mQ[1])); + CHECK(is_approx_equal(0.000f, llquat.mQ[2])); + CHECK(is_approx_equal(1.000f, llquat.mQ[3])); } - // The product and the dot evaluate at compile time. - template<> template<> - void llquat_test_object_t::test<23>() + TEST_CASE("LLQuaternion: product and dot evaluate at compile time", "[LLQuaternion]") { constexpr LLQuaternion half_turn(0.f, 0.f, 1.f, 0.f); constexpr LLQuaternion full_turn = half_turn * half_turn; static_assert(full_turn == LLQuaternion(0.f, 0.f, 0.f, -1.f)); static_assert(dot(half_turn, half_turn) == 1.f); static_assert(dot(half_turn, full_turn) == 0.f); - ensure("compile-time quaternion agrees at run time", full_turn.mQ[VW] == -1.f); + UNSCOPED_INFO("compile-time quaternion agrees at run time"); + CHECK(full_turn.mQ[VW] == -1.f); } } diff --git a/indra/llmath/tests/llrect_test.cpp b/indra/llmath/tests/llrect_test.cpp index a98907140c..72a8e22f5d 100644 --- a/indra/llmath/tests/llrect_test.cpp +++ b/indra/llmath/tests/llrect_test.cpp @@ -27,31 +27,17 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include +#include "altestmatchers.h" #include "../llrect.h" -namespace tut -{ namespace { - struct LLRectData - { - }; - - typedef test_group factory; - typedef factory::object object; -} } namespace { - tut::factory llrect_test_factory("LLRect"); -} - -namespace tut -{ - template<> template<> - void object::test<1>() + TEST_CASE("LLRect: default constructor gives an all-zero rect", "[LLRect]") { // // test the LLRect default constructor @@ -62,19 +48,18 @@ namespace tut // default constructor LLRect rect1; - ensure_equals("Empty rect", rect1.getValue(), zero); - ensure_equals("Empty rect left", rect1.mLeft, 0); - ensure_equals("Empty rect top", rect1.mTop, 0); - ensure_equals("Empty rect right", rect1.mRight, 0); - ensure_equals("Empty rect bottom", rect1.mBottom, 0); - ensure_equals("Empty rect width", rect1.getWidth(), 0); - ensure_equals("Empty rect height", rect1.getHeight(), 0); - ensure_equals("Empty rect centerx", rect1.getCenterX(), 0); - ensure_equals("Empty rect centery", rect1.getCenterY(), 0); + CHECK_THAT(rect1.getValue(), ll_test::LLSDEquals(zero)); + CHECK(rect1.mLeft == 0); + CHECK(rect1.mTop == 0); + CHECK(rect1.mRight == 0); + CHECK(rect1.mBottom == 0); + CHECK(rect1.getWidth() == 0); + CHECK(rect1.getHeight() == 0); + CHECK(rect1.getCenterX() == 0); + CHECK(rect1.getCenterY() == 0); } - template<> template<> - void object::test<2>() + TEST_CASE("LLRect: LLRectf default constructor gives an all-zero rect", "[LLRect]") { // // test the LLRectf default constructor @@ -84,55 +69,52 @@ namespace tut zerof.append(0.0f); zerof.append(0.0f); zerof.append(0.0f); zerof.append(0.0f); LLRectf rect2; - ensure_equals("Empty rectf", rect2.getValue(), zerof); - ensure_equals("Empty rectf left", rect2.mLeft, 0.0f); - ensure_equals("Empty rectf top", rect2.mTop, 0.0f); - ensure_equals("Empty rectf right", rect2.mRight, 0.0f); - ensure_equals("Empty rectf bottom", rect2.mBottom, 0.0f); - ensure_equals("Empty rectf width", rect2.getWidth(), 0.0f); - ensure_equals("Empty rectf height", rect2.getHeight(), 0.0f); - ensure_equals("Empty rectf centerx", rect2.getCenterX(), 0.0f); - ensure_equals("Empty rectf centery", rect2.getCenterY(), 0.0f); + CHECK_THAT(rect2.getValue(), ll_test::LLSDEquals(zerof)); + CHECK(rect2.mLeft == 0.0f); + CHECK(rect2.mTop == 0.0f); + CHECK(rect2.mRight == 0.0f); + CHECK(rect2.mBottom == 0.0f); + CHECK(rect2.getWidth() == 0.0f); + CHECK(rect2.getHeight() == 0.0f); + CHECK(rect2.getCenterX() == 0.0f); + CHECK(rect2.getCenterY() == 0.0f); } - template<> template<> - void object::test<3>() + TEST_CASE("LLRect: constructor from another LLRect copies its edges", "[LLRect]") { // // test the LLRect constructor from another LLRect // LLRect rect3(LLRect(1, 6, 7, 2)); - ensure_equals("Default rect left", rect3.mLeft, 1); - ensure_equals("Default rect top", rect3.mTop, 6); - ensure_equals("Default rect right", rect3.mRight, 7); - ensure_equals("Default rect bottom", rect3.mBottom, 2); - ensure_equals("Default rect width", rect3.getWidth(), 6); - ensure_equals("Default rect height", rect3.getHeight(), 4); - ensure_equals("Default rect centerx", rect3.getCenterX(), 4); - ensure_equals("Default rect centery", rect3.getCenterY(), 4); + CHECK(rect3.mLeft == 1); + CHECK(rect3.mTop == 6); + CHECK(rect3.mRight == 7); + CHECK(rect3.mBottom == 2); + CHECK(rect3.getWidth() == 6); + CHECK(rect3.getHeight() == 4); + CHECK(rect3.getCenterX() == 4); + CHECK(rect3.getCenterY() == 4); } - template<> template<> - void object::test<4>() + TEST_CASE("LLRect: LLRectf four-float constructor sets edges and size", "[LLRect]") { // // test the LLRectf four-float constructor // LLRectf rect4(1.0f, 5.0f, 6.0f, 2.0f); - ensure_equals("Default rectf left", rect4.mLeft, 1.0f); - ensure_equals("Default rectf top", rect4.mTop, 5.0f); - ensure_equals("Default rectf right", rect4.mRight, 6.0f); - ensure_equals("Default rectf bottom", rect4.mBottom, 2.0f); - ensure_equals("Default rectf width", rect4.getWidth(), 5.0f); - ensure_equals("Default rectf height", rect4.getHeight(), 3.0f); - ensure_equals("Default rectf centerx", rect4.getCenterX(), 3.5f); - ensure_equals("Default rectf centery", rect4.getCenterY(), 3.5f); + CHECK(rect4.mLeft == 1.0f); + CHECK(rect4.mTop == 5.0f); + CHECK(rect4.mRight == 6.0f); + CHECK(rect4.mBottom == 2.0f); + CHECK(rect4.getWidth() == 5.0f); + CHECK(rect4.getHeight() == 3.0f); + CHECK(rect4.getCenterX() == 3.5f); + CHECK(rect4.getCenterY() == 3.5f); } - template<> template<> - void object::test<5>() + TEST_CASE("LLRect: LLRectf LLSD constructor sets edges and size", "[LLRect]") { // // test the LLRectf LLSD constructor @@ -141,18 +123,17 @@ namespace tut LLSD array; array.append(-1.0f); array.append(0.0f); array.append(0.0f); array.append(-1.0f); LLRectf rect5(array); - ensure_equals("LLSD rectf left", rect5.mLeft, -1.0f); - ensure_equals("LLSD rectf top", rect5.mTop, 0.0f); - ensure_equals("LLSD rectf right", rect5.mRight, 0.0f); - ensure_equals("LLSD rectf bottom", rect5.mBottom, -1.0f); - ensure_equals("LLSD rectf width", rect5.getWidth(), 1.0f); - ensure_equals("LLSD rectf height", rect5.getHeight(), 1.0f); - ensure_equals("LLSD rectf centerx", rect5.getCenterX(), -0.5f); - ensure_equals("LLSD rectf centery", rect5.getCenterY(), -0.5f); + CHECK(rect5.mLeft == -1.0f); + CHECK(rect5.mTop == 0.0f); + CHECK(rect5.mRight == 0.0f); + CHECK(rect5.mBottom == -1.0f); + CHECK(rect5.getWidth() == 1.0f); + CHECK(rect5.getHeight() == 1.0f); + CHECK(rect5.getCenterX() == -0.5f); + CHECK(rect5.getCenterY() == -0.5f); } - template<> template<> - void object::test<6>() + TEST_CASE("LLRect: edges set directly give the matching size and center", "[LLRect]") { // // test directly setting the member variables for dimensions @@ -164,18 +145,17 @@ namespace tut rectf.mTop = 1.0f; rectf.mRight = 1.0f; rectf.mBottom = -1.0f; - ensure_equals("Member-set rectf left", rectf.mLeft, -1.0f); - ensure_equals("Member-set rectf top", rectf.mTop, 1.0f); - ensure_equals("Member-set rectf right", rectf.mRight, 1.0f); - ensure_equals("Member-set rectf bottom", rectf.mBottom, -1.0f); - ensure_equals("Member-set rectf width", rectf.getWidth(), 2.0f); - ensure_equals("Member-set rectf height", rectf.getHeight(), 2.0f); - ensure_equals("Member-set rectf centerx", rectf.getCenterX(), 0.0f); - ensure_equals("Member-set rectf centery", rectf.getCenterY(), 0.0f); + CHECK(rectf.mLeft == -1.0f); + CHECK(rectf.mTop == 1.0f); + CHECK(rectf.mRight == 1.0f); + CHECK(rectf.mBottom == -1.0f); + CHECK(rectf.getWidth() == 2.0f); + CHECK(rectf.getHeight() == 2.0f); + CHECK(rectf.getCenterX() == 0.0f); + CHECK(rectf.getCenterY() == 0.0f); } - template<> template<> - void object::test<7>() + TEST_CASE("LLRect: setValue() reads edges from LLSD", "[LLRect]") { // // test the setValue() method @@ -186,18 +166,17 @@ namespace tut LLSD array; array.append(-1.0f); array.append(0.0f); array.append(0.0f); array.append(-1.0f); rectf.setValue(array); - ensure_equals("setValue() rectf left", rectf.mLeft, -1.0f); - ensure_equals("setValue() rectf top", rectf.mTop, 0.0f); - ensure_equals("setValue() rectf right", rectf.mRight, 0.0f); - ensure_equals("setValue() rectf bottom", rectf.mBottom, -1.0f); - ensure_equals("setValue() rectf width", rectf.getWidth(), 1.0f); - ensure_equals("setValue() rectf height", rectf.getHeight(), 1.0f); - ensure_equals("setValue() rectf centerx", rectf.getCenterX(), -0.5f); - ensure_equals("setValue() rectf centery", rectf.getCenterY(), -0.5f); + CHECK(rectf.mLeft == -1.0f); + CHECK(rectf.mTop == 0.0f); + CHECK(rectf.mRight == 0.0f); + CHECK(rectf.mBottom == -1.0f); + CHECK(rectf.getWidth() == 1.0f); + CHECK(rectf.getHeight() == 1.0f); + CHECK(rectf.getCenterX() == -0.5f); + CHECK(rectf.getCenterY() == -0.5f); } - template<> template<> - void object::test<8>() + TEST_CASE("LLRect: set() sets edges and size", "[LLRect]") { // // test the set() method @@ -206,18 +185,17 @@ namespace tut LLRect rect; rect.set(10, 90, 70, 10); - ensure_equals("set() rectf left", rect.mLeft, 10); - ensure_equals("set() rectf top", rect.mTop, 90); - ensure_equals("set() rectf right", rect.mRight, 70); - ensure_equals("set() rectf bottom", rect.mBottom, 10); - ensure_equals("set() rectf width", rect.getWidth(), 60); - ensure_equals("set() rectf height", rect.getHeight(), 80); - ensure_equals("set() rectf centerx", rect.getCenterX(), 40); - ensure_equals("set() rectf centery", rect.getCenterY(), 50); + CHECK(rect.mLeft == 10); + CHECK(rect.mTop == 90); + CHECK(rect.mRight == 70); + CHECK(rect.mBottom == 10); + CHECK(rect.getWidth() == 60); + CHECK(rect.getHeight() == 80); + CHECK(rect.getCenterX() == 40); + CHECK(rect.getCenterY() == 50); } - template<> template<> - void object::test<9>() + TEST_CASE("LLRect: setOriginAndSize() grows up and right from the origin", "[LLRect]") { // // test the setOriginAndSize() method @@ -226,18 +204,17 @@ namespace tut LLRectf rectf; rectf.setOriginAndSize(0.0f, 0.0f, 2.0f, 1.0f); - ensure_equals("setOriginAndSize() rectf left", rectf.mLeft, 0.0f); - ensure_equals("setOriginAndSize() rectf top", rectf.mTop, 1.0f); - ensure_equals("setOriginAndSize() rectf right", rectf.mRight, 2.0f); - ensure_equals("setOriginAndSize() rectf bottom", rectf.mBottom, 0.0f); - ensure_equals("setOriginAndSize() rectf width", rectf.getWidth(), 2.0f); - ensure_equals("setOriginAndSize() rectf height", rectf.getHeight(), 1.0f); - ensure_equals("setOriginAndSize() rectf centerx", rectf.getCenterX(), 1.0f); - ensure_equals("setOriginAndSize() rectf centery", rectf.getCenterY(), 0.5f); + CHECK(rectf.mLeft == 0.0f); + CHECK(rectf.mTop == 1.0f); + CHECK(rectf.mRight == 2.0f); + CHECK(rectf.mBottom == 0.0f); + CHECK(rectf.getWidth() == 2.0f); + CHECK(rectf.getHeight() == 1.0f); + CHECK(rectf.getCenterX() == 1.0f); + CHECK(rectf.getCenterY() == 0.5f); } - template<> template<> - void object::test<10>() + TEST_CASE("LLRect: setLeftTopAndSize() grows down and right from the top-left", "[LLRect]") { // // test the setLeftTopAndSize() method @@ -246,18 +223,17 @@ namespace tut LLRectf rectf; rectf.setLeftTopAndSize(0.0f, 0.0f, 2.0f, 1.0f); - ensure_equals("setLeftTopAndSize() rectf left", rectf.mLeft, 0.0f); - ensure_equals("setLeftTopAndSize() rectf top", rectf.mTop, 0.0f); - ensure_equals("setLeftTopAndSize() rectf right", rectf.mRight, 2.0f); - ensure_equals("setLeftTopAndSize() rectf bottom", rectf.mBottom, -1.0f); - ensure_equals("setLeftTopAndSize() rectf width", rectf.getWidth(), 2.0f); - ensure_equals("setLeftTopAndSize() rectf height", rectf.getHeight(), 1.0f); - ensure_equals("setLeftTopAndSize() rectf centerx", rectf.getCenterX(), 1.0f); - ensure_equals("setLeftTopAndSize() rectf centery", rectf.getCenterY(), -0.5f); + CHECK(rectf.mLeft == 0.0f); + CHECK(rectf.mTop == 0.0f); + CHECK(rectf.mRight == 2.0f); + CHECK(rectf.mBottom == -1.0f); + CHECK(rectf.getWidth() == 2.0f); + CHECK(rectf.getHeight() == 1.0f); + CHECK(rectf.getCenterX() == 1.0f); + CHECK(rectf.getCenterY() == -0.5f); } - template<> template<> - void object::test<11>() + TEST_CASE("LLRect: setCenterAndSize() spreads the size about the center", "[LLRect]") { // // test the setCenterAndSize() method @@ -266,18 +242,17 @@ namespace tut LLRectf rectf; rectf.setCenterAndSize(0.0f, 0.0f, 2.0f, 1.0f); - ensure_equals("setCenterAndSize() rectf left", rectf.mLeft, -1.0f); - ensure_equals("setCenterAndSize() rectf top", rectf.mTop, 0.5f); - ensure_equals("setCenterAndSize() rectf right", rectf.mRight, 1.0f); - ensure_equals("setCenterAndSize() rectf bottom", rectf.mBottom, -0.5f); - ensure_equals("setCenterAndSize() rectf width", rectf.getWidth(), 2.0f); - ensure_equals("setCenterAndSize() rectf height", rectf.getHeight(), 1.0f); - ensure_equals("setCenterAndSize() rectf centerx", rectf.getCenterX(), 0.0f); - ensure_equals("setCenterAndSize() rectf centery", rectf.getCenterY(), 0.0f); + CHECK(rectf.mLeft == -1.0f); + CHECK(rectf.mTop == 0.5f); + CHECK(rectf.mRight == 1.0f); + CHECK(rectf.mBottom == -0.5f); + CHECK(rectf.getWidth() == 2.0f); + CHECK(rectf.getHeight() == 1.0f); + CHECK(rectf.getCenterX() == 0.0f); + CHECK(rectf.getCenterY() == 0.0f); } - template<> template<> - void object::test<12>() + TEST_CASE("LLRect: isValid() fails for an inverted rect until makeValid()", "[LLRect]") { // // test the validity checking method @@ -286,20 +261,21 @@ namespace tut LLRectf rectf; rectf.set(-1.0f, 1.0f, 1.0f, -1.0f); - ensure("BBox is valid", rectf.isValid()); + CHECK(rectf.isValid()); rectf.mLeft = 2.0f; - ensure("BBox is not valid", ! rectf.isValid()); + CHECK_FALSE(rectf.isValid()); rectf.makeValid(); - ensure("BBox forced valid", rectf.isValid()); + UNSCOPED_INFO("after makeValid()"); + CHECK(rectf.isValid()); rectf.set(-1.0f, -1.0f, -1.0f, -1.0f); - ensure("BBox(0,0,0,0) is valid", rectf.isValid()); + UNSCOPED_INFO("zero-size rect"); + CHECK(rectf.isValid()); } - template<> template<> - void object::test<13>() + TEST_CASE("LLRect: isEmpty() and notEmpty() on full and zero-size rects", "[LLRect]") { // // test the null checking methods @@ -308,19 +284,20 @@ namespace tut LLRectf rectf; rectf.set(-1.0f, 1.0f, 1.0f, -1.0f); - ensure("BBox is not Null", ! rectf.isEmpty()); - ensure("BBox notNull", rectf.notEmpty()); + CHECK_FALSE(rectf.isEmpty()); + CHECK(rectf.notEmpty()); rectf.mLeft = 2.0f; rectf.makeValid(); - ensure("BBox is now Null", rectf.isEmpty()); + UNSCOPED_INFO("inverted rect after makeValid()"); + CHECK(rectf.isEmpty()); rectf.set(-1.0f, -1.0f, -1.0f, -1.0f); - ensure("BBox(0,0,0,0) is Null", rectf.isEmpty()); + UNSCOPED_INFO("zero-size rect"); + CHECK(rectf.isEmpty()); } - template<> template<> - void object::test<14>() + TEST_CASE("LLRect: equality and inequality operators", "[LLRect]") { // // test the (in)equality operators @@ -331,15 +308,14 @@ namespace tut rect1.set(-1.0f, 1.0f, 1.0f, -1.0f); rect2.set(-1.0f, 0.9f, 1.0f, -1.0f); - ensure("rect1 == rect2 (false)", ! (rect1 == rect2)); - ensure("rect1 != rect2 (true)", rect1 != rect2); + CHECK_FALSE(rect1 == rect2); + CHECK(rect1 != rect2); - ensure("rect1 == rect1 (true)", rect1 == rect1); - ensure("rect1 != rect1 (false)", ! (rect1 != rect1)); + CHECK(rect1 == rect1); + CHECK_FALSE(rect1 != rect1); } - template<> template<> - void object::test<15>() + TEST_CASE("LLRect: copy constructor gives an equal rect", "[LLRect]") { // // test the copy constructor @@ -347,12 +323,11 @@ namespace tut LLRectf rect1, rect2(rect1); - ensure("rect1 == rect2 (true)", rect1 == rect2); - ensure("rect1 != rect2 (false)", ! (rect1 != rect2)); + CHECK(rect1 == rect2); + CHECK_FALSE(rect1 != rect2); } - template<> template<> - void object::test<16>() + TEST_CASE("LLRect: translate() moves all four edges", "[LLRect]") { // // test the translate() method @@ -363,28 +338,32 @@ namespace tut rect1.translate(0.0f, 0.0f); - ensure("translate(0, 0)", rect1 == rect2); + UNSCOPED_INFO("translate(0, 0)"); + CHECK(rect1 == rect2); rect1.translate(100.0f, 100.0f); rect1.translate(-100.0f, -100.0f); - ensure("translate(100, 100) + translate(-100, -100)", rect1 == rect2); + UNSCOPED_INFO("translate(100, 100) + translate(-100, -100)"); + CHECK(rect1 == rect2); rect1.translate(10.0f, 0.0f); rect2.set(9.0f, 1.0f, 11.0f, -1.0f); - ensure("translate(10, 0)", rect1 == rect2); + UNSCOPED_INFO("translate(10, 0)"); + CHECK(rect1 == rect2); rect1.translate(0.0f, 10.0f); rect2.set(9.0f, 11.0f, 11.0f, 9.0f); - ensure("translate(0, 10)", rect1 == rect2); + UNSCOPED_INFO("translate(0, 10)"); + CHECK(rect1 == rect2); rect1.translate(-10.0f, -10.0f); rect2.set(-1.0f, 1.0f, 1.0f, -1.0f); - ensure("translate(-10, -10)", rect1 == rect2); + UNSCOPED_INFO("translate(-10, -10)"); + CHECK(rect1 == rect2); } - template<> template<> - void object::test<17>() + TEST_CASE("LLRect: stretch() moves the edges outward", "[LLRect]") { // // test the stretch() method @@ -394,23 +373,26 @@ namespace tut LLRectf rect2(rect1); rect1.stretch(0.0f); - ensure("stretch(0)", rect1 == rect2); + UNSCOPED_INFO("stretch(0)"); + CHECK(rect1 == rect2); rect1.stretch(0.0f, 0.0f); - ensure("stretch(0, 0)", rect1 == rect2); + UNSCOPED_INFO("stretch(0, 0)"); + CHECK(rect1 == rect2); rect1.stretch(10.0f); rect1.stretch(-10.0f); - ensure("stretch(10) + stretch(-10)", rect1 == rect2); + UNSCOPED_INFO("stretch(10) + stretch(-10)"); + CHECK(rect1 == rect2); rect1.stretch(2.0f, 1.0f); rect2.set(-3.0f, 2.0f, 3.0f, -2.0f); - ensure("stretch(2, 1)", rect1 == rect2); + UNSCOPED_INFO("stretch(2, 1)"); + CHECK(rect1 == rect2); } - template<> template<> - void object::test<18>() + TEST_CASE("LLRect: unionWith() covers both rects", "[LLRect]") { // // test the unionWith() method @@ -422,23 +404,25 @@ namespace tut rect2.set(-1.0f, 1.0f, 1.0f, -1.0f); rect3 = rect1; rect3.unionWith(rect2); - ensure_equals("union with self", rect3, rect1); + UNSCOPED_INFO("union with self"); + CHECK(rect3 == rect1); rect1.set(-1.0f, 1.0f, 1.0f, -1.0f); rect2.set(-2.0f, 2.0f, 0.0f, 0.0f); rect3 = rect1; rect3.unionWith(rect2); - ensure_equals("union - overlap", rect3, LLRectf(-2.0f, 2.0f, 1.0f, -1.0f)); + UNSCOPED_INFO("overlapping rects"); + CHECK(rect3 == LLRectf(-2.0f, 2.0f, 1.0f, -1.0f)); rect1.set(-1.0f, 1.0f, 1.0f, -1.0f); rect2.set(5.0f, 10.0f, 10.0f, 5.0f); rect3 = rect1; rect3.unionWith(rect2); - ensure_equals("union - no overlap", rect3, LLRectf(-1.0f, 10.0f, 10.0f, -1.0f)); + UNSCOPED_INFO("disjoint rects"); + CHECK(rect3 == LLRectf(-1.0f, 10.0f, 10.0f, -1.0f)); } - template<> template<> - void object::test<19>() + TEST_CASE("LLRect: intersectWith() keeps only the overlap", "[LLRect]") { // // test the intersectWith() methods @@ -450,23 +434,25 @@ namespace tut rect2.set(-1.0f, 1.0f, 1.0f, -1.0f); rect3 = rect1; rect3.intersectWith(rect2); - ensure_equals("intersect with self", rect3, rect1); + UNSCOPED_INFO("intersect with self"); + CHECK(rect3 == rect1); rect1.set(-1.0f, 1.0f, 1.0f, -1.0f); rect2.set(-2.0f, 2.0f, 0.0f, 0.0f); rect3 = rect1; rect3.intersectWith(rect2); - ensure_equals("intersect - overlap", rect3, LLRectf(-1.0f, 1.0f, 0.0f, 0.0f)); + UNSCOPED_INFO("overlapping rects"); + CHECK(rect3 == LLRectf(-1.0f, 1.0f, 0.0f, 0.0f)); rect1.set(-1.0f, 1.0f, 1.0f, -1.0f); rect2.set(5.0f, 10.0f, 10.0f, 5.0f); rect3 = rect1; rect3.intersectWith(rect2); - ensure("intersect - no overlap", rect3.isEmpty()); + UNSCOPED_INFO("disjoint rects"); + CHECK(rect3.isEmpty()); } - template<> template<> - void object::test<20>() + TEST_CASE("LLRect: pointInRect() includes the left and bottom edges only", "[LLRect]") { // // test the pointInRect() method @@ -474,17 +460,16 @@ namespace tut LLRectf rect(1.0f, 3.0f, 3.0f, 1.0f); - ensure("(0,0) not in rect", rect.pointInRect(0.0f, 0.0f) == false); - ensure("(2,2) in rect", rect.pointInRect(2.0f, 2.0f) == true); - ensure("(1,1) in rect", rect.pointInRect(1.0f, 1.0f) == true); - ensure("(3,3) not in rect", rect.pointInRect(3.0f, 3.0f) == false); - ensure("(2.999,2.999) in rect", rect.pointInRect(2.999f, 2.999f) == true); - ensure("(2.999,3.0) not in rect", rect.pointInRect(2.999f, 3.0f) == false); - ensure("(3.0,2.999) not in rect", rect.pointInRect(3.0f, 2.999f) == false); + CHECK_FALSE(rect.pointInRect(0.0f, 0.0f)); + CHECK(rect.pointInRect(2.0f, 2.0f)); + CHECK(rect.pointInRect(1.0f, 1.0f)); + CHECK_FALSE(rect.pointInRect(3.0f, 3.0f)); + CHECK(rect.pointInRect(2.999f, 2.999f)); + CHECK_FALSE(rect.pointInRect(2.999f, 3.0f)); + CHECK_FALSE(rect.pointInRect(3.0f, 2.999f)); } - template<> template<> - void object::test<21>() + TEST_CASE("LLRect: localPointInRect() measures from the bottom-left corner", "[LLRect]") { // // test the localPointInRect() method @@ -492,17 +477,16 @@ namespace tut LLRectf rect(1.0f, 3.0f, 3.0f, 1.0f); - ensure("(0,0) in local rect", rect.localPointInRect(0.0f, 0.0f) == true); - ensure("(-0.0001,-0.0001) not in local rect", rect.localPointInRect(-0.0001f, -0.001f) == false); - ensure("(1,1) in local rect", rect.localPointInRect(1.0f, 1.0f) == true); - ensure("(2,2) not in local rect", rect.localPointInRect(2.0f, 2.0f) == false); - ensure("(1.999,1.999) in local rect", rect.localPointInRect(1.999f, 1.999f) == true); - ensure("(1.999,2.0) not in local rect", rect.localPointInRect(1.999f, 2.0f) == false); - ensure("(2.0,1.999) not in local rect", rect.localPointInRect(2.0f, 1.999f) == false); + CHECK(rect.localPointInRect(0.0f, 0.0f)); + CHECK_FALSE(rect.localPointInRect(-0.0001f, -0.001f)); + CHECK(rect.localPointInRect(1.0f, 1.0f)); + CHECK_FALSE(rect.localPointInRect(2.0f, 2.0f)); + CHECK(rect.localPointInRect(1.999f, 1.999f)); + CHECK_FALSE(rect.localPointInRect(1.999f, 2.0f)); + CHECK_FALSE(rect.localPointInRect(2.0f, 1.999f)); } - template<> template<> - void object::test<22>() + TEST_CASE("LLRect: clampPointToRect() clamps to the edges", "[LLRect]") { // // test the clampPointToRect() method @@ -513,17 +497,23 @@ namespace tut x = 2.0f; y = 2.0f; rect.clampPointToRect(x, y); - ensure_equals("clamp x-coord within rect", x, 2.0f); - ensure_equals("clamp y-coord within rect", y, 2.0f); + UNSCOPED_INFO("point inside rect"); + CHECK(x == 2.0f); + UNSCOPED_INFO("point inside rect"); + CHECK(y == 2.0f); x = -100.0f; y = 100.0f; rect.clampPointToRect(x, y); - ensure_equals("clamp x-coord outside rect", x, 1.0f); - ensure_equals("clamp y-coord outside rect", y, 3.0f); + UNSCOPED_INFO("point outside rect"); + CHECK(x == 1.0f); + UNSCOPED_INFO("point outside rect"); + CHECK(y == 3.0f); x = 3.0f; y = 1.0f; rect.clampPointToRect(x, y); - ensure_equals("clamp x-coord edge rect", x, 3.0f); - ensure_equals("clamp y-coord edge rect", y, 1.0f); + UNSCOPED_INFO("point on the bottom-right corner"); + CHECK(x == 3.0f); + UNSCOPED_INFO("point on the bottom-right corner"); + CHECK(y == 1.0f); } } diff --git a/indra/llmath/tests/llsdutil_math_test.cpp b/indra/llmath/tests/llsdutil_math_test.cpp index 8c79d002da..0314c32592 100644 --- a/indra/llmath/tests/llsdutil_math_test.cpp +++ b/indra/llmath/tests/llsdutil_math_test.cpp @@ -27,7 +27,8 @@ */ #include "linden_common.h" -#include "lltut.h" +#include +#include "altestmatchers.h" #include "m4math.h" #include "v2math.h" #include "v2math.h" @@ -43,8 +44,6 @@ #include #include -namespace tut -{ namespace { struct llsdutil_math_data @@ -64,7 +63,7 @@ namespace // This test is supposed to succeed. Comparing to the // empty string ensures that if the test fails, it will // display the string received so we can tell what failed. - ensure_equals("match", match, ""); + REQUIRE(match == ""); } else { @@ -72,85 +71,77 @@ namespace // the string 'match' will be empty, which doesn't tell us // much about which case went awry. So construct a more // detailed description string. - ensure(proto_key + " shouldn't match " + pi->first, ! match.empty()); + INFO(proto_key << " shouldn't match " << pi->first); + REQUIRE_FALSE(match.empty()); } } } }; - typedef test_group llsdutil_math_test; - typedef llsdutil_math_test::object llsdutil_math_object; - tut::llsdutil_math_test llsdutil_math_testcase("llsdutil_math"); -} - template<> template<> - void llsdutil_math_object::test<1>() + TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLVector3 round-trips through LLSD", "[llsdutil_math]") { LLSD sd; LLVector3 vec1(-1.0, 2.0, -3.0); sd = ll_sd_from_vector3(vec1); LLVector3 vec2 = ll_vector3_from_sd(sd); - ensure_equals("vector3 -> sd -> vector3: 1", vec1, vec2); + CHECK(vec1 == vec2); LLVector3 vec3(sd); - ensure_equals("vector3 -> sd -> vector3: 2", vec1, vec3); + CHECK(vec1 == vec3); sd.clear(); vec1.setVec(0., 0., 0.); sd = ll_sd_from_vector3(vec1); vec2 = ll_vector3_from_sd(sd); - ensure_equals("vector3 -> sd -> vector3: 3", vec1, vec2); + CHECK(vec1 == vec2); sd.clear(); } - template<> template<> - void llsdutil_math_object::test<2>() + TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLVector3d round-trips through LLSD", "[llsdutil_math]") { LLSD sd; LLVector3d vec1((F64)(U64L(0xFEDCBA9876543210) << 2), -1., 0); sd = ll_sd_from_vector3d(vec1); LLVector3d vec2 = ll_vector3d_from_sd(sd); - ensure_equals("vector3d -> sd -> vector3d: 1", vec1, vec2); + CHECK(vec1 == vec2); LLVector3d vec3(sd); - ensure_equals("vector3d -> sd -> vector3d : 2", vec1, vec3); + CHECK(vec1 == vec3); } - template<> template<> - void llsdutil_math_object::test<3>() + TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLVector2 and its LLSD round-trip both ways", "[llsdutil_math]") { LLSD sd; LLVector2 vec((F32) -3., (F32) 4.2); sd = ll_sd_from_vector2(vec); LLVector2 vec1 = ll_vector2_from_sd(sd); - ensure_equals("vector2 -> sd -> vector2", vec, vec1); + CHECK(vec == vec1); LLSD sd2 = ll_sd_from_vector2(vec1); - ensure_equals("sd -> vector2 -> sd: 2", sd, sd2); + CHECK_THAT(sd, ll_test::LLSDEquals(sd2)); } - template<> template<> - void llsdutil_math_object::test<4>() + TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLQuaternion and its LLSD round-trip both ways", "[llsdutil_math]") { LLSD sd; LLQuaternion quat((F32) 1., (F32) -0.98, (F32) 2.3, (F32) 0xffff); sd = ll_sd_from_quaternion(quat); LLQuaternion quat1 = ll_quaternion_from_sd(sd); - ensure_equals("LLQuaternion -> sd -> LLQuaternion", quat, quat1); + CHECK(quat == quat1); LLSD sd2 = ll_sd_from_quaternion(quat1); - ensure_equals("sd -> LLQuaternion -> sd ", sd, sd2); + CHECK_THAT(sd, ll_test::LLSDEquals(sd2)); } - template<> template<> - void llsdutil_math_object::test<5>() + TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLColor4 and its LLSD round-trip both ways", "[llsdutil_math]") { LLSD sd; LLColor4 c(1.0f, 2.2f, 4.0f, 7.f); sd = ll_sd_from_color4(c); LLColor4 c1 =ll_color4_from_sd(sd); - ensure_equals("LLColor4 -> sd -> LLColor4", c, c1); + CHECK(c == c1); LLSD sd1 = ll_sd_from_color4(c1); - ensure_equals("sd -> LLColor4 -> sd", sd, sd1); + CHECK_THAT(sd, ll_test::LLSDEquals(sd1)); } } diff --git a/indra/llmath/tests/llvector4a_test.cpp b/indra/llmath/tests/llvector4a_test.cpp index 18b34bb974..3479dc3f92 100644 --- a/indra/llmath/tests/llvector4a_test.cpp +++ b/indra/llmath/tests/llvector4a_test.cpp @@ -33,7 +33,7 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../llmath.h" #include "../llsimdmath.h" #include "../llvector4a.h" @@ -49,8 +49,6 @@ #include #include -namespace tut -{ namespace { U32 bits_of(F32 f) @@ -82,53 +80,60 @@ namespace return (U32)(d < 0 ? -d : d); } - void ensure_bits(const std::string& what, const LLVector4a& got, U32 x, U32 y, U32 z, U32 w) + void require_bits(const std::string& what, const LLVector4a& got, U32 x, U32 y, U32 z, U32 w) { + INFO(what); const U32 expected[4] = {x, y, z, w}; - for (int i = 0; i < 4; ++i) + for (int lane = 0; lane < 4; ++lane) { - ensure_equals(what + " lane " + std::to_string(i), bits_of(got[i]), expected[i]); + CAPTURE(lane); + REQUIRE(bits_of(got[lane]) == expected[lane]); } } - void ensure_exact(const std::string& what, const LLVector4a& got, F32 x, F32 y, F32 z, F32 w) + void require_exact(const std::string& what, const LLVector4a& got, F32 x, F32 y, F32 z, F32 w) { - ensure_bits(what, got, bits_of(x), bits_of(y), bits_of(z), bits_of(w)); + require_bits(what, got, bits_of(x), bits_of(y), bits_of(z), bits_of(w)); } - void ensure_ulps(const std::string& what, F32 got, F32 expected, U32 ulps) + void require_ulps(const std::string& what, F32 got, F32 expected, U32 ulps) { const U32 d = ulps_between(got, expected); - ensure(what + ": " + precise(got) + " is " + std::to_string(d) + " ulps from " + precise(expected), d <= ulps); + INFO(what << ": " << precise(got) << " is " << d << " ulps from " << precise(expected)); + REQUIRE(d <= ulps); } - void ensure_ulps(const std::string& what, const LLVector4a& got, const F32* expected, U32 ulps, int lanes = 4) + void require_ulps(const std::string& what, const LLVector4a& got, const F32* expected, U32 ulps, int lanes = 4) { - for (int i = 0; i < lanes; ++i) + for (int lane = 0; lane < lanes; ++lane) { - ensure_ulps(what + " lane " + std::to_string(i), got[i], expected[i], ulps); + CAPTURE(lane); + require_ulps(what, got[lane], expected[lane], ulps); } } - void ensure_relative(const std::string& what, F32 got, double expected, double relative) + void require_relative(const std::string& what, F32 got, double expected, double relative) { const double err = std::fabs((double)got - expected) / std::fabs(expected); - ensure(what + ": " + precise(got) + " is off " + precise(err) + " relative from " + precise(expected), err <= relative); + INFO(what << ": " << precise(got) << " is off " << precise(err) << " relative from " << precise(expected)); + REQUIRE(err <= relative); } - void ensure_relative(const std::string& what, const LLVector4a& got, const double* expected, double relative, int lanes = 4) + void require_relative(const std::string& what, const LLVector4a& got, const double* expected, double relative, int lanes = 4) { - for (int i = 0; i < lanes; ++i) + for (int lane = 0; lane < lanes; ++lane) { - ensure_relative(what + " lane " + std::to_string(i), got[i], expected[i], relative); + CAPTURE(lane); + require_relative(what, got[lane], expected[lane], relative); } } - void ensure_same_lanes(const std::string& what, const LLVector4a& v) + void require_same_lanes(const std::string& what, const LLVector4a& v) { - ensure_equals(what + " lane 1", bits_of(v[1]), bits_of(v[0])); - ensure_equals(what + " lane 2", bits_of(v[2]), bits_of(v[0])); - ensure_equals(what + " lane 3", bits_of(v[3]), bits_of(v[0])); + INFO(what); + REQUIRE(bits_of(v[1]) == bits_of(v[0])); + REQUIRE(bits_of(v[2]) == bits_of(v[0])); + REQUIRE(bits_of(v[3]) == bits_of(v[0])); } // Scalar references, every intermediate rounded to float in a volatile @@ -155,11 +160,8 @@ namespace const F32 INF = std::numeric_limits::infinity(); const F32 NAN_QUIET = std::numeric_limits::quiet_NaN(); const F32 DENORMAL = std::numeric_limits::denorm_min(); -} -} // namespace tut +} // anonymous namespace -namespace tut -{ namespace { struct llvector4a_data @@ -180,95 +182,88 @@ namespace LLVector4a n_a() const { return LLVector4a(na[0], na[1], na[2], na[3]); } LLVector4a n_b() const { return LLVector4a(nb[0], nb[1], nb[2], nb[3]); } }; - typedef test_group llvector4a_test; - typedef llvector4a_test::object llvector4a_object; - tut::llvector4a_test llvector4a_testcase("LLVector4a"); -} // Plain data of one register, and the two constants. - template<> template<> - void llvector4a_object::test<1>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: plain data of one register and the two constants", "[LLVector4a]") { - ensure_equals("sizeof LLVector4a", sizeof(LLVector4a), (size_t)16); - ensure_equals("alignof LLVector4a", alignof(LLVector4a), (size_t)16); - ensure_equals("sizeof LLSimdScalar", sizeof(LLSimdScalar), (size_t)16); - ensure_equals("sizeof LLVector4Logical", sizeof(LLVector4Logical), (size_t)16); - ensure("trivially copyable", std::is_trivially_copyable::value); - ensure("standard layout", std::is_standard_layout::value); - - ensure_exact("zero", LLVector4a::getZero(), 0.f, 0.f, 0.f, 0.f); - ensure_exact("epsilon", LLVector4a::getEpsilon(), F_APPROXIMATELY_ZERO, F_APPROXIMATELY_ZERO, F_APPROXIMATELY_ZERO, F_APPROXIMATELY_ZERO); + CHECK(sizeof(LLVector4a) == (size_t)16); + CHECK(alignof(LLVector4a) == (size_t)16); + CHECK(sizeof(LLSimdScalar) == (size_t)16); + CHECK(sizeof(LLVector4Logical) == (size_t)16); + CHECK(std::is_trivially_copyable::value); + CHECK(std::is_standard_layout::value); + + require_exact("zero", LLVector4a::getZero(), 0.f, 0.f, 0.f, 0.f); + require_exact("epsilon", LLVector4a::getEpsilon(), F_APPROXIMATELY_ZERO, F_APPROXIMATELY_ZERO, F_APPROXIMATELY_ZERO, F_APPROXIMATELY_ZERO); } // Loads, stores, set, splat and the lane accessors move bits. - template<> template<> - void llvector4a_object::test<2>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: loads and stores and splats and the lane accessors move bits", "[LLVector4a]") { alignas(16) F32 src[5] = {float_of(pa[0]), float_of(pa[1]), float_of(pa[2]), float_of(pa[3]), 99.f}; LLVector4a v; v.load4a(src); - ensure_bits("load4a", v, pa[0], pa[1], pa[2], pa[3]); + require_bits("load4a", v, pa[0], pa[1], pa[2], pa[3]); v.loadua(src + 1); - ensure_bits("loadua", v, pa[1], pa[2], pa[3], bits_of(99.f)); + require_bits("loadua", v, pa[1], pa[2], pa[3], bits_of(99.f)); v.load3(src + 1); - ensure_bits("load3", v, pa[1], pa[2], pa[3], 0u); + require_bits("load3", v, pa[1], pa[2], pa[3], 0u); alignas(16) F32 dst[4] = {1.f, 1.f, 1.f, 1.f}; a().store4a(dst); - ensure_equals("store4a x", bits_of(dst[0]), pa[0]); - ensure_equals("store4a y", bits_of(dst[1]), pa[1]); - ensure_equals("store4a z", bits_of(dst[2]), pa[2]); - ensure_equals("store4a w", bits_of(dst[3]), pa[3]); + CHECK(bits_of(dst[0]) == pa[0]); + CHECK(bits_of(dst[1]) == pa[1]); + CHECK(bits_of(dst[2]) == pa[2]); + CHECK(bits_of(dst[3]) == pa[3]); alignas(16) F32 copied[4]; LLVector4a::copy4a(copied, src); - ensure_equals("copy4a", bits_of(copied[2]), pa[2]); + CHECK(bits_of(copied[2]) == pa[2]); v.set(1.f, 2.f, 3.f); - ensure_exact("set defaults w to zero", v, 1.f, 2.f, 3.f, 0.f); + require_exact("set defaults w to zero", v, 1.f, 2.f, 3.f, 0.f); v.clear(); - ensure_exact("clear", v, 0.f, 0.f, 0.f, 0.f); + require_exact("clear", v, 0.f, 0.f, 0.f, 0.f); v.splat(float_of(pa[2])); - ensure_bits("splat(F32)", v, pa[2], pa[2], pa[2], pa[2]); + require_bits("splat(F32)", v, pa[2], pa[2], pa[2], pa[2]); v.splat<3>(a()); - ensure_bits("splat<3>", v, pa[3], pa[3], pa[3], pa[3]); + require_bits("splat<3>", v, pa[3], pa[3], pa[3], pa[3]); v.splat(a(), 1); - ensure_bits("splat(v, 1)", v, pa[1], pa[1], pa[1], pa[1]); + require_bits("splat(v, 1)", v, pa[1], pa[1], pa[1], pa[1]); const LLVector4a splatted(a().getScalarAt<2>()); - ensure_bits("LLVector4a(LLSimdScalar)", splatted, pa[2], pa[2], pa[2], pa[2]); + require_bits("LLVector4a(LLSimdScalar)", splatted, pa[2], pa[2], pa[2], pa[2]); v.splat(a().getScalarAt(3)); - ensure_bits("splat(LLSimdScalar) from getScalarAt(3)", v, pa[3], pa[3], pa[3], pa[3]); + require_bits("splat(LLSimdScalar) from getScalarAt(3)", v, pa[3], pa[3], pa[3], pa[3]); const LLVector4a one(1.f); - ensure_exact("explicit LLVector4a(F32)", one, 1.f, 1.f, 1.f, 1.f); + require_exact("explicit LLVector4a(F32)", one, 1.f, 1.f, 1.f, 1.f); const LLVector4a from_a = a(); - ensure_equals("operator[] 2", bits_of(from_a[2]), pa[2]); - ensure_equals("getScalarAt<1>", bits_of(from_a.getScalarAt<1>().getF32()), pa[1]); - ensure_equals("getScalarAt(0)", bits_of(from_a.getScalarAt(0).getF32()), pa[0]); + CHECK(bits_of(from_a[2]) == pa[2]); + CHECK(bits_of(from_a.getScalarAt<1>().getF32()) == pa[1]); + CHECK(bits_of(from_a.getScalarAt(0).getF32()) == pa[0]); LLVector4a written = a(); written.getF32ptr()[1] = 7.f; - ensure_bits("getF32ptr writes through", written, pa[0], bits_of(7.f), pa[2], pa[3]); - ensure_equals("getF32ptr reads", bits_of(written.getF32ptr()[3]), pa[3]); + require_bits("getF32ptr writes through", written, pa[0], bits_of(7.f), pa[2], pa[3]); + CHECK(bits_of(written.getF32ptr()[3]) == pa[3]); LLVector4a copy; copy = (LLQuad)a(); - ensure_bits("operator=(LLQuad)", copy, pa[0], pa[1], pa[2], pa[3]); + require_bits("operator=(LLQuad)", copy, pa[0], pa[1], pa[2], pa[3]); } // The mask type: compares, gathered bits, any/all, invert, the elements. - template<> template<> - void llvector4a_object::test<3>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: compare masks and their queries and setSelectWithMask", "[LLVector4a]") { // the NaN comes through a volatile so the fast floating-point // contract cannot fold a compare of a value with itself @@ -277,37 +272,38 @@ namespace const LLVector4a y(2.f, 2.f, 2.f, nan_lane); const LLVector4a y_again(2.f, 2.f, 2.f, nan_lane); - ensure_equals("greaterThan", x.greaterThan(y).getGatheredBits(), (U32)(LLVector4Logical::MASK_Z)); - ensure_equals("lessThan", x.lessThan(y).getGatheredBits(), (U32)(LLVector4Logical::MASK_X)); - ensure_equals("greaterEqual", x.greaterEqual(y).getGatheredBits(), (U32)(LLVector4Logical::MASK_Y | LLVector4Logical::MASK_Z)); - ensure_equals("lessEqual", x.lessEqual(y).getGatheredBits(), (U32)(LLVector4Logical::MASK_X | LLVector4Logical::MASK_Y)); - ensure_equals("equal", x.equal(y).getGatheredBits(), (U32)(LLVector4Logical::MASK_Y)); - ensure_equals("NaN compares equal to nothing", y.equal(y_again).getGatheredBits(), (U32)(LLVector4Logical::MASK_XYZ)); + CHECK(x.greaterThan(y).getGatheredBits() == (U32)(LLVector4Logical::MASK_Z)); + CHECK(x.lessThan(y).getGatheredBits() == (U32)(LLVector4Logical::MASK_X)); + CHECK(x.greaterEqual(y).getGatheredBits() == (U32)(LLVector4Logical::MASK_Y | LLVector4Logical::MASK_Z)); + CHECK(x.lessEqual(y).getGatheredBits() == (U32)(LLVector4Logical::MASK_X | LLVector4Logical::MASK_Y)); + CHECK(x.equal(y).getGatheredBits() == (U32)(LLVector4Logical::MASK_Y)); + UNSCOPED_INFO("NaN compares equal to nothing"); + CHECK(y.equal(y_again).getGatheredBits() == (U32)(LLVector4Logical::MASK_XYZ)); LLVector4Logical m = x.greaterEqual(y); - ensure("areAnySet", m.areAnySet()); - ensure("areAnySet(MASK_XYZ)", m.areAnySet(LLVector4Logical::MASK_XYZ)); - ensure("areAnySet(MASK_X) false", !m.areAnySet(LLVector4Logical::MASK_X)); - ensure("areAnySet(MASK_W) false", !m.areAnySet(LLVector4Logical::MASK_W)); - ensure("areAllSet false", !m.areAllSet()); - ensure("areAllSet(MASK_XYZ) false", !m.areAllSet(LLVector4Logical::MASK_XYZ)); - ensure("areAllSet(Y|Z)", m.areAllSet(LLVector4Logical::MASK_Y | LLVector4Logical::MASK_Z)); + CHECK(m.areAnySet()); + CHECK(m.areAnySet(LLVector4Logical::MASK_XYZ)); + CHECK_FALSE(m.areAnySet(LLVector4Logical::MASK_X)); + CHECK_FALSE(m.areAnySet(LLVector4Logical::MASK_W)); + CHECK_FALSE(m.areAllSet()); + CHECK_FALSE(m.areAllSet(LLVector4Logical::MASK_XYZ)); + CHECK(m.areAllSet(LLVector4Logical::MASK_Y | LLVector4Logical::MASK_Z)); m.invert(); - ensure_equals("invert", m.getGatheredBits(), (U32)(LLVector4Logical::MASK_X | LLVector4Logical::MASK_W)); + CHECK(m.getGatheredBits() == (U32)(LLVector4Logical::MASK_X | LLVector4Logical::MASK_W)); LLVector4Logical built; built.clear(); - ensure_equals("clear", built.getGatheredBits(), 0u); - ensure("clear areAnySet", !built.areAnySet()); + CHECK(built.getGatheredBits() == 0u); + CHECK_FALSE(built.areAnySet()); built.setElement<0>(); built.setElement<2>(); - ensure_equals("setElement", built.getGatheredBits(), (U32)(LLVector4Logical::MASK_X | LLVector4Logical::MASK_Z)); + CHECK(built.getGatheredBits() == (U32)(LLVector4Logical::MASK_X | LLVector4Logical::MASK_Z)); built.setElement<1>(); - ensure("areAllSet(MASK_XYZ)", built.areAllSet(LLVector4Logical::MASK_XYZ)); - ensure("areAllSet still false", !built.areAllSet()); + CHECK(built.areAllSet(LLVector4Logical::MASK_XYZ)); + CHECK_FALSE(built.areAllSet()); built.setElement<3>(); - ensure("areAllSet", built.areAllSet()); + CHECK(built.areAllSet()); // select takes whole lanes, bits and all LLVector4Logical pick; @@ -316,13 +312,12 @@ namespace pick.setElement<3>(); LLVector4a chosen; chosen.setSelectWithMask(pick, a(), b()); - ensure_bits("setSelectWithMask", chosen, pb[0], pa[1], pb[2], pa[3]); + require_bits("setSelectWithMask", chosen, pb[0], pa[1], pb[2], pa[3]); } // Lane arithmetic is one rounding per lane, so it matches scalar float // bit for bit; the sign ops touch only the sign bit. - template<> template<> - void llvector4a_object::test<4>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: lane arithmetic matches scalar float bit for bit", "[LLVector4a]") { const LLVector4a p = n_a(); const LLVector4a q = n_b(); @@ -332,100 +327,100 @@ namespace r.setAdd(p, q); for (int i = 0; i < 4; ++i) { volatile F32 e = na[i] + nb[i]; expected[i] = e; } - ensure_exact("setAdd", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("setAdd", r, expected[0], expected[1], expected[2], expected[3]); r = p; r.add(q); - ensure_exact("add", r, expected[0], expected[1], expected[2], expected[3]); - ensure_exact("operator+", p + q, expected[0], expected[1], expected[2], expected[3]); + require_exact("add", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("operator+", p + q, expected[0], expected[1], expected[2], expected[3]); r = p; r += q; - ensure_exact("operator+=", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("operator+=", r, expected[0], expected[1], expected[2], expected[3]); r.setSub(p, q); for (int i = 0; i < 4; ++i) { volatile F32 e = na[i] - nb[i]; expected[i] = e; } - ensure_exact("setSub", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("setSub", r, expected[0], expected[1], expected[2], expected[3]); r = p; r.sub(q); - ensure_exact("sub", r, expected[0], expected[1], expected[2], expected[3]); - ensure_exact("operator-", p - q, expected[0], expected[1], expected[2], expected[3]); + require_exact("sub", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("operator-", p - q, expected[0], expected[1], expected[2], expected[3]); r = p; r -= q; - ensure_exact("operator-=", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("operator-=", r, expected[0], expected[1], expected[2], expected[3]); r.setMul(p, q); for (int i = 0; i < 4; ++i) { volatile F32 e = na[i] * nb[i]; expected[i] = e; } - ensure_exact("setMul", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("setMul", r, expected[0], expected[1], expected[2], expected[3]); r = p; r.mul(q); - ensure_exact("mul", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("mul", r, expected[0], expected[1], expected[2], expected[3]); r = p; r.mul(-3.f); for (int i = 0; i < 4; ++i) { volatile F32 e = na[i] * -3.f; expected[i] = e; } - ensure_exact("mul(F32)", r, expected[0], expected[1], expected[2], expected[3]); + require_exact("mul(F32)", r, expected[0], expected[1], expected[2], expected[3]); r.setDiv(p, q); for (int i = 0; i < 4; ++i) { volatile F32 e = na[i] / nb[i]; expected[i] = e; } - ensure_ulps("setDiv", r, expected, 1); + require_ulps("setDiv", r, expected, 1); r = p; r.div(q); - ensure_ulps("div", r, expected, 1); + require_ulps("div", r, expected, 1); // The negative zero as its bits, in and out: /fp:fast lets the // compiler take -0.f for 0.f, and MSVC for arm64 does. const LLVector4a signs(alsimd::or_(alsimd::set(-1.5f, 0.f, 0.f, -INF), alsimd::and_(alsimd::set1_bits(0x80000000u), alsimd::mask_lane<2>()))); r.setAbs(signs); - ensure_bits("setAbs", r, bits_of(1.5f), 0u, 0u, bits_of(INF)); + require_bits("setAbs", r, bits_of(1.5f), 0u, 0u, bits_of(INF)); r.setNeg(signs); - ensure_bits("setNeg", r, bits_of(1.5f), 0x80000000u, 0u, bits_of(INF)); + require_bits("setNeg", r, bits_of(1.5f), 0x80000000u, 0u, bits_of(INF)); r = signs; r.negate(); - ensure_bits("negate", r, bits_of(1.5f), 0x80000000u, 0u, bits_of(INF)); + require_bits("negate", r, bits_of(1.5f), 0x80000000u, 0u, bits_of(INF)); // a NaN payload survives the sign ops unchanged r.setAbs(a()); - ensure_equals("setAbs keeps a NaN payload", bits_of(r[2]), pa[2]); + UNSCOPED_INFO("setAbs keeps a NaN payload"); + CHECK(bits_of(r[2]) == pa[2]); r.setNeg(a()); - ensure_equals("setNeg keeps a NaN payload", bits_of(r[2]), pa[2] | 0x80000000u); + UNSCOPED_INFO("setNeg keeps a NaN payload"); + CHECK(bits_of(r[2]) == (pa[2] | 0x80000000u)); // denormals pass through the arithmetic here, whatever the viewer // sets its own threads to const LLVector4a tiny(DENORMAL, DENORMAL, 0.f, 0.f); r.setAdd(tiny, tiny); - ensure_equals("denormal add", bits_of(r[0]), bits_of(DENORMAL) * 2); + CHECK(bits_of(r[0]) == bits_of(DENORMAL) * 2); } // The dot products against double, the whole-register forms in every // lane, and the scalar forms in the lane that is read. - template<> template<> - void llvector4a_object::test<5>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: dot products and lengths against double", "[LLVector4a]") { const LLVector4a p = n_a(); const LLVector4a q = n_b(); LLVector4a r; r.setAllDot3(p, q); - ensure_same_lanes("setAllDot3", r); - ensure_relative("setAllDot3", r[0], dot3_exact(na, nb), 4e-7); - ensure_ulps("setAllDot3 vs scalar", r[0], dot3_ref(na, nb), 2); + require_same_lanes("setAllDot3", r); + require_relative("setAllDot3", r[0], dot3_exact(na, nb), 4e-7); + require_ulps("setAllDot3 vs scalar", r[0], dot3_ref(na, nb), 2); r.setAllDot4(p, q); - ensure_same_lanes("setAllDot4", r); - ensure_relative("setAllDot4", r[0], dot4_exact(na, nb), 4e-7); + require_same_lanes("setAllDot4", r); + require_relative("setAllDot4", r[0], dot4_exact(na, nb), 4e-7); - ensure_relative("dot3", p.dot3(q).getF32(), dot3_exact(na, nb), 4e-7); - ensure_relative("dot4", p.dot4(q).getF32(), dot4_exact(na, nb), 4e-7); + require_relative("dot3", p.dot3(q).getF32(), dot3_exact(na, nb), 4e-7); + require_relative("dot4", p.dot4(q).getF32(), dot4_exact(na, nb), 4e-7); // w does not reach the three-lane forms const LLVector4a p_w(na[0], na[1], na[2], 1e30f); - ensure_relative("dot3 ignores w", p_w.dot3(q).getF32(), dot3_exact(na, nb), 4e-7); + require_relative("dot3 ignores w", p_w.dot3(q).getF32(), dot3_exact(na, nb), 4e-7); // a length, both ways const LLVector4a v(3.f, 4.f, 12.f, 100.f); - ensure_ulps("getLength3", v.getLength3().getF32(), 13.f, 1); + require_ulps("getLength3", v.getLength3().getF32(), 13.f, 1); r.setAllLength3(v); - ensure_same_lanes("setAllLength3", r); - ensure_ulps("setAllLength3", r[0], 13.f, 1); + require_same_lanes("setAllLength3", r); + require_ulps("setAllLength3", r[0], 13.f, 1); } // The cross product against double, within the rounding the fused form // is allowed, and the identities that name it. - template<> template<> - void llvector4a_object::test<6>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: the cross product against double and its identities", "[LLVector4a]") { const LLVector4a p = n_a(); const LLVector4a q = n_b(); @@ -439,28 +434,30 @@ namespace LLVector4a r; r.setCross3(p, q); - ensure_relative("setCross3", r, expected, 4e-7, 3); - ensure_equals("setCross3 w is zero", r[3], 0.f); - ensure_relative("cross3", p.cross3(q), expected, 4e-7, 3); + require_relative("setCross3", r, expected, 4e-7, 3); + CHECK(r[3] == 0.f); + require_relative("cross3", p.cross3(q), expected, 4e-7, 3); // perpendicular to both, and antisymmetric - ensure("cross is perpendicular to a", std::fabs(r.dot3(p).getF32()) < 1e-5f); - ensure("cross is perpendicular to b", std::fabs(r.dot3(q).getF32()) < 1e-5f); + UNSCOPED_INFO("cross is perpendicular to a"); + CHECK(std::fabs(r.dot3(p).getF32()) < 1e-5f); + UNSCOPED_INFO("cross is perpendicular to b"); + CHECK(std::fabs(r.dot3(q).getF32()) < 1e-5f); LLVector4a s; s.setCross3(q, p); s.negate(); - ensure("b x a is -(a x b)", s.equals3(r, 1e-6f)); + UNSCOPED_INFO("b x a is -(a x b)"); + CHECK(s.equals3(r, 1e-6f)); // the basis r.setCross3(LLVector4a(1.f, 0.f, 0.f), LLVector4a(0.f, 1.f, 0.f)); - ensure_exact("x cross y is z", r, 0.f, 0.f, 1.f, 0.f); + require_exact("x cross y is z", r, 0.f, 0.f, 1.f, 0.f); } // The normalize family: the full-precision forms within an ulp or two of // the double answer, the fast form within its promised 11 bits, and the // checked form's two escapes. - template<> template<> - void llvector4a_object::test<7>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: the normalize family against double and the checked form's escapes", "[LLVector4a]") { const F32 raw[4] = {3.f, -4.f, 12.f, 2.f}; const double len3 = 13.0; @@ -470,141 +467,143 @@ namespace LLVector4a v(raw[0], raw[1], raw[2], raw[3]); v.normalize3(); - ensure_relative("normalize3", v, n3, 4e-7, 3); - ensure("normalize3 result isNormalized3", v.isNormalized3()); - ensure("normalize3 result isNormalized3 tight", v.isNormalized3(1e-6f)); + require_relative("normalize3", v, n3, 4e-7, 3); + CHECK(v.isNormalized3()); + CHECK(v.isNormalized3(1e-6f)); v.set(raw[0], raw[1], raw[2], raw[3]); v.normalize4(); - ensure_relative("normalize4", v, n4, 4e-7); - ensure("normalize4 result isNormalized4", v.isNormalized4(1e-6f)); + require_relative("normalize4", v, n4, 4e-7); + CHECK(v.isNormalized4(1e-6f)); v.set(raw[0], raw[1], raw[2], raw[3]); const F32 length = v.normalize3withLength().getF32(); - ensure_relative("normalize3withLength direction", v, n3, 4e-7, 3); - ensure_ulps("normalize3withLength length", length, 13.f, 1); + require_relative("normalize3withLength direction", v, n3, 4e-7, 3); + require_ulps("normalize3withLength length", length, 13.f, 1); v.set(raw[0], raw[1], raw[2], raw[3]); v.normalize3fast(); - ensure_relative("normalize3fast", v, n3, 1.0 / 2048.0, 3); + require_relative("normalize3fast", v, n3, 1.0 / 2048.0, 3); // large and small lengths, where an estimate's range matters const double third = 1.0 / std::sqrt(3.0); const double unit3[3] = {third, third, third}; v.set(1e18f, 1e18f, 1e18f); v.normalize3(); - ensure_relative("normalize3 of a large vector", v, unit3, 4e-7, 3); + require_relative("normalize3 of a large vector", v, unit3, 4e-7, 3); v.set(1e-18f, 1e-18f, 1e-18f); v.normalize3(); - ensure_relative("normalize3 of a small vector", v, unit3, 4e-7, 3); + require_relative("normalize3 of a small vector", v, unit3, 4e-7, 3); // isNormalized measures length, not squared length const LLVector4a off(1.0005f, 0.f, 0.f, 0.f); - ensure("isNormalized3 at the default tolerance", off.isNormalized3()); - ensure("isNormalized3 at a tight tolerance", !off.isNormalized3(1e-4f)); + CHECK(off.isNormalized3()); + CHECK_FALSE(off.isNormalized3(1e-4f)); const LLVector4a off4(0.f, 0.f, 0.f, 1.0005f); - ensure("isNormalized4 sees w", off4.isNormalized4()); - ensure("isNormalized3 does not see w", !off4.isNormalized3()); + UNSCOPED_INFO("isNormalized4 sees w"); + CHECK(off4.isNormalized4()); + UNSCOPED_INFO("isNormalized3 does not see w"); + CHECK_FALSE(off4.isNormalized3()); // the checked form LLVector4a checked(3.f, -4.f, 12.f, 0.f); checked.normalize3fast_checked(); - ensure_relative("normalize3fast_checked", checked, n3, 1.0 / 2048.0, 3); + require_relative("normalize3fast_checked", checked, n3, 1.0 / 2048.0, 3); checked.set(NAN_QUIET, 1.f, 1.f); checked.normalize3fast_checked(); - ensure_exact("normalize3fast_checked of a NaN is the default", checked, 0.f, 1.f, 0.f, 1.f); + require_exact("normalize3fast_checked of a NaN is the default", checked, 0.f, 1.f, 0.f, 1.f); checked.set(0.f, 0.f, 0.f); LLVector4a fallback(0.f, 0.f, 1.f, 0.f); checked.normalize3fast_checked(&fallback); - ensure_exact("normalize3fast_checked of zero is the fallback", checked, 0.f, 0.f, 1.f, 0.f); + require_exact("normalize3fast_checked of zero is the fallback", checked, 0.f, 0.f, 1.f, 0.f); } // min, max, clamp and lerp: the first two on numbers alone, clamp with // the NaN it promises to leave alone. - template<> template<> - void llvector4a_object::test<8>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: setMin and setMax and update_min_max and clamp and setLerp", "[LLVector4a]") { const LLVector4a p = n_a(); const LLVector4a q = n_b(); LLVector4a r; r.setMin(p, q); - ensure_exact("setMin", r, -0.75f, -2.25f, 1.0625f, -8.f); + require_exact("setMin", r, -0.75f, -2.25f, 1.0625f, -8.f); r.setMax(p, q); - ensure_exact("setMax", r, 1.5f, 4.f, 3.125f, 0.5f); + require_exact("setMax", r, 1.5f, 4.f, 3.125f, 0.5f); LLVector4a lo(-1.f, -1.f, -1.f, -1.f), hi(1.f, 1.f, 1.f, 1.f); update_min_max(lo, hi, p); - ensure_exact("update_min_max min", lo, -1.f, -2.25f, -1.f, -1.f); - ensure_exact("update_min_max max", hi, 1.5f, 1.f, 3.125f, 1.f); + require_exact("update_min_max min", lo, -1.f, -2.25f, -1.f, -1.f); + require_exact("update_min_max max", hi, 1.5f, 1.f, 3.125f, 1.f); const LLVector4a low(0.f, -1.f, -1.f, 0.f); const LLVector4a high(1.f, 1.f, 2.f, 0.f); r.set(-0.5f, 0.5f, 3.f, NAN_QUIET); r.clamp(low, high); - ensure_exact("clamp x y z", LLVector4a(r[0], r[1], r[2], 0.f), 0.f, 0.5f, 2.f, 0.f); - ensure("clamp leaves a NaN lane NaN", std::isnan(r[3])); + require_exact("clamp x y z", LLVector4a(r[0], r[1], r[2], 0.f), 0.f, 0.5f, 2.f, 0.f); + UNSCOPED_INFO("clamp leaves a NaN lane NaN"); + CHECK(std::isnan(r[3])); r.set(0.f, 1.f, -1.f, 0.f); r.clamp(low, high); - ensure_exact("clamp at the bounds", r, 0.f, 1.f, -1.f, 0.f); + require_exact("clamp at the bounds", r, 0.f, 1.f, -1.f, 0.f); r.setLerp(p, q, 0.f); - ensure_exact("setLerp at 0 is lhs", r, na[0], na[1], na[2], na[3]); + require_exact("setLerp at 0 is lhs", r, na[0], na[1], na[2], na[3]); r.setLerp(p, q, 1.f); - ensure_ulps("setLerp at 1 is rhs", r, nb, 1); + require_ulps("setLerp at 1 is rhs", r, nb, 1); r.setLerp(p, q, 0.25f); F32 expected[4]; for (int i = 0; i < 4; ++i) { expected[i] = (F32)((double)na[i] + ((double)nb[i] - na[i]) * 0.25); } - ensure_ulps("setLerp at a quarter", r, expected, 2); + require_ulps("setLerp at a quarter", r, expected, 2); } // Finite means every exponent short of all ones, in the lanes asked. - template<> template<> - void llvector4a_object::test<9>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: isFinite3 and isFinite4 reject infinity and NaN in their lanes", "[LLVector4a]") { - ensure("numbers are finite", n_a().isFinite4()); - ensure("a denormal is finite", LLVector4a(DENORMAL, 0.f, 0.f, 0.f).isFinite4()); - ensure("the largest float is finite", LLVector4a(std::numeric_limits::max(), 0.f, 0.f, 0.f).isFinite4()); - ensure("an infinity in x", !LLVector4a(INF, 0.f, 0.f, 0.f).isFinite3()); - ensure("a negative infinity in z", !LLVector4a(0.f, 0.f, -INF, 0.f).isFinite3()); - ensure("a NaN in y", !LLVector4a(0.f, NAN_QUIET, 0.f, 0.f).isFinite3()); - ensure("a NaN in w passes isFinite3", LLVector4a(0.f, 0.f, 0.f, NAN_QUIET).isFinite3()); - ensure("a NaN in w fails isFinite4", !LLVector4a(0.f, 0.f, 0.f, NAN_QUIET).isFinite4()); - ensure("a NaN payload fails", !a().isFinite3()); + CHECK(n_a().isFinite4()); + CHECK(LLVector4a(DENORMAL, 0.f, 0.f, 0.f).isFinite4()); + CHECK(LLVector4a(std::numeric_limits::max(), 0.f, 0.f, 0.f).isFinite4()); + CHECK_FALSE(LLVector4a(INF, 0.f, 0.f, 0.f).isFinite3()); + CHECK_FALSE(LLVector4a(0.f, 0.f, -INF, 0.f).isFinite3()); + CHECK_FALSE(LLVector4a(0.f, NAN_QUIET, 0.f, 0.f).isFinite3()); + CHECK(LLVector4a(0.f, 0.f, 0.f, NAN_QUIET).isFinite3()); + CHECK_FALSE(LLVector4a(0.f, 0.f, 0.f, NAN_QUIET).isFinite4()); + UNSCOPED_INFO("a NaN payload fails"); + CHECK_FALSE(a().isFinite3()); } // The tolerance compares. - template<> template<> - void llvector4a_object::test<10>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: equals3 and equals4 compare within a tolerance", "[LLVector4a]") { const LLVector4a p = n_a(); LLVector4a nearby = p; nearby.add(LLVector4a(1e-7f, -1e-7f, 1e-7f, 1e-7f)); - ensure("equals4 within the default tolerance", p.equals4(nearby)); - ensure("operator== within the default tolerance", p == nearby); - ensure("equals4 outside a tight tolerance", !p.equals4(nearby, 1e-8f)); + CHECK(p.equals4(nearby)); + CHECK(p == nearby); + CHECK_FALSE(p.equals4(nearby, 1e-8f)); LLVector4a w_off = p; w_off.add(LLVector4a(0.f, 0.f, 0.f, 1.f)); - ensure("equals3 ignores w", p.equals3(w_off)); - ensure("equals4 sees w", !p.equals4(w_off)); - ensure("operator!=", p != w_off); + UNSCOPED_INFO("equals3 ignores w"); + CHECK(p.equals3(w_off)); + UNSCOPED_INFO("equals4 sees w"); + CHECK_FALSE(p.equals4(w_off)); + CHECK(p != w_off); // Held against another value rather than itself: /fp:fast lets the // compiler fold x - x to 0, so a vector equals itself whatever it holds. const LLVector4a nan(NAN_QUIET, 0.f, 0.f, 0.f); - ensure("a NaN equals nothing", !nan.equals4(LLVector4a::getZero())); - ensure("and nothing equals a NaN", !LLVector4a::getZero().equals4(nan)); + CHECK_FALSE(nan.equals4(LLVector4a::getZero())); + CHECK_FALSE(LLVector4a::getZero().equals4(nan)); } // Quantizing maps onto the stored integers and back: the result is // within half a step of the input, stable under a second pass, collapses // a flat channel onto its bound, and rounds anything within a step of // zero to zero. - template<> template<> - void llvector4a_object::test<11>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: quantize8 and quantize16 map onto the stored integers and back", "[LLVector4a]") { const LLVector4a low(-1.f, -2.f, 0.f, 5.f); const LLVector4a high(1.f, 2.f, 10.f, 5.f); @@ -627,38 +626,43 @@ namespace for (int i = 0; i < 3; ++i) { const F32 clamped = llclamp(in[i], low[i], high[i]); - const std::string lane = " lane " + std::to_string(i) + " of " + precise(in[i]); - ensure("quantize8 within half a step" + lane, std::fabs(v8[i] - clamped) <= delta[i] / 255.f * 0.5f + 1e-6f); - ensure("quantize16 within half a step" + lane, std::fabs(v16[i] - clamped) <= delta[i] / 65535.f * 0.5f + 1e-6f); + INFO("lane " << i << " of " << precise(in[i])); + REQUIRE(std::fabs(v8[i] - clamped) <= delta[i] / 255.f * 0.5f + 1e-6f); + REQUIRE(std::fabs(v16[i] - clamped) <= delta[i] / 65535.f * 0.5f + 1e-6f); // the way back multiplies by the step and by the range, so a // bound can come back an ulp past itself const F32 slack = 1e-6f * llmax(1.f, std::fabs(high[i])); - ensure("quantize8 within the range" + lane, v8[i] >= low[i] - slack && v8[i] <= high[i] + slack); - ensure("quantize16 within the range" + lane, v16[i] >= low[i] - slack && v16[i] <= high[i] + slack); + REQUIRE(v8[i] >= low[i] - slack); + REQUIRE(v8[i] <= high[i] + slack); + REQUIRE(v16[i] >= low[i] - slack); + REQUIRE(v16[i] <= high[i] + slack); } - ensure_equals("quantize8 flat channel is low", v8[3], 5.f); - ensure_equals("quantize16 flat channel is low", v16[3], 5.f); + UNSCOPED_INFO("quantize8 flat channel is low"); + REQUIRE(v8[3] == 5.f); + UNSCOPED_INFO("quantize16 flat channel is low"); + REQUIRE(v16[3] == 5.f); LLVector4a again8 = v8; again8.quantize8(low, high); - ensure("quantize8 is stable", again8.equals4(v8, 1e-6f)); + UNSCOPED_INFO("quantize8 is stable"); + REQUIRE(again8.equals4(v8, 1e-6f)); LLVector4a again16 = v16; again16.quantize16(low, high); - ensure("quantize16 is stable", again16.equals4(v16, 1e-7f)); + UNSCOPED_INFO("quantize16 is stable"); + REQUIRE(again16.equals4(v16, 1e-7f)); } // a value within a step of zero becomes zero LLVector4a tiny(0.001f, -0.001f, 0.01f, 5.f); tiny.quantize8(low, high); - ensure_equals("quantize8 snaps x to zero", bits_of(tiny[0]), 0u); - ensure_equals("quantize8 snaps y to zero", bits_of(tiny[1]), 0u); - ensure_equals("quantize8 snaps z to zero", bits_of(tiny[2]), 0u); + CHECK(bits_of(tiny[0]) == 0u); + CHECK(bits_of(tiny[1]) == 0u); + CHECK(bits_of(tiny[2]) == 0u); } // Rotation by a matrix and by a quaternion against the scalar types, // and the inverse forms undoing them. - template<> template<> - void llvector4a_object::test<12>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: rotation by a matrix and by a quaternion against the scalar types", "[LLVector4a]") { const LLQuaternion rotations[] = { LLQuaternion(), @@ -680,7 +684,8 @@ namespace const LLMatrix3 m3 = q.getMatrix3(); LLRotation rot; rot.loadu(m3); - ensure("the matrix is a rotation", rot.isOkRotation()); + UNSCOPED_INFO("the matrix is a rotation"); + REQUIRE(rot.isOkRotation()); LLQuaternion2 q2(q); @@ -695,21 +700,23 @@ namespace LLVector4a by_matrix; by_matrix.setRotated(rot, v); - ensure("setRotated(LLRotation) x", std::fabs(by_matrix[0] - expected.mV[0]) <= tolerance); - ensure("setRotated(LLRotation) y", std::fabs(by_matrix[1] - expected.mV[1]) <= tolerance); - ensure("setRotated(LLRotation) z", std::fabs(by_matrix[2] - expected.mV[2]) <= tolerance); + REQUIRE(std::fabs(by_matrix[0] - expected.mV[0]) <= tolerance); + REQUIRE(std::fabs(by_matrix[1] - expected.mV[1]) <= tolerance); + REQUIRE(std::fabs(by_matrix[2] - expected.mV[2]) <= tolerance); LLVector4a by_quat; by_quat.setRotated(q2, v); - ensure("setRotated(LLQuaternion2) x", std::fabs(by_quat[0] - expected_q.mV[0]) <= tolerance); - ensure("setRotated(LLQuaternion2) y", std::fabs(by_quat[1] - expected_q.mV[1]) <= tolerance); - ensure("setRotated(LLQuaternion2) z", std::fabs(by_quat[2] - expected_q.mV[2]) <= tolerance); + REQUIRE(std::fabs(by_quat[0] - expected_q.mV[0]) <= tolerance); + REQUIRE(std::fabs(by_quat[1] - expected_q.mV[1]) <= tolerance); + REQUIRE(std::fabs(by_quat[2] - expected_q.mV[2]) <= tolerance); LLVector4a back; back.setRotatedInv(rot, by_matrix); - ensure("setRotatedInv(LLRotation) undoes it", back.equals3(v, tolerance)); + UNSCOPED_INFO("setRotatedInv(LLRotation) undoes it"); + REQUIRE(back.equals3(v, tolerance)); back.setRotatedInv(q2, by_quat); - ensure("setRotatedInv(LLQuaternion2) undoes it", back.equals3(v, tolerance)); + UNSCOPED_INFO("setRotatedInv(LLQuaternion2) undoes it"); + REQUIRE(back.equals3(v, tolerance)); } } } @@ -717,41 +724,40 @@ namespace // LLSimdScalar: the arithmetic on lane 0, and the compares. A NaN is // not compared: the fast floating-point contract this tree builds under // leaves a scalar compare of one unordered. - template<> template<> - void llvector4a_object::test<13>() + TEST_CASE_METHOD(llvector4a_data, "LLVector4a: LLSimdScalar arithmetic and compares", "[LLVector4a]") { const LLSimdScalar three(3.f); const LLSimdScalar minus_two(-2.f); - ensure_equals("getF32", three.getF32(), 3.f); - ensure_equals("operator+", (three + minus_two).getF32(), 1.f); - ensure_equals("operator-", (three - minus_two).getF32(), 5.f); - ensure_equals("operator*", (three * minus_two).getF32(), -6.f); - ensure_equals("operator/", (three / minus_two).getF32(), -1.5f); - ensure_equals("unary minus", (-three).getF32(), -3.f); - ensure_equals("getAbs", minus_two.getAbs().getF32(), 2.f); - ensure_equals("getZero", LLSimdScalar::getZero().getF32(), 0.f); + CHECK(three.getF32() == 3.f); + CHECK((three + minus_two).getF32() == 1.f); + CHECK((three - minus_two).getF32() == 5.f); + CHECK((three * minus_two).getF32() == -6.f); + CHECK((three / minus_two).getF32() == -1.5f); + CHECK((-three).getF32() == -3.f); + CHECK(minus_two.getAbs().getF32() == 2.f); + CHECK(LLSimdScalar::getZero().getF32() == 0.f); LLSimdScalar s = three; - s += minus_two; ensure_equals("+=", s.getF32(), 1.f); - s -= minus_two; ensure_equals("-=", s.getF32(), 3.f); - s *= minus_two; ensure_equals("*=", s.getF32(), -6.f); - s /= three; ensure_equals("/=", s.getF32(), -2.f); - s = 7.f; ensure_equals("=", s.getF32(), 7.f); + s += minus_two; CHECK(s.getF32() == 1.f); + s -= minus_two; CHECK(s.getF32() == 3.f); + s *= minus_two; CHECK(s.getF32() == -6.f); + s /= three; CHECK(s.getF32() == -2.f); + s = 7.f; CHECK(s.getF32() == 7.f); LLSimdScalar mm; - mm.setMax(three, minus_two); ensure_equals("setMax", mm.getF32(), 3.f); - mm.setMin(three, minus_two); ensure_equals("setMin", mm.getF32(), -2.f); - - ensure("<", minus_two < three); - ensure("<=", three <= three); - ensure(">", three > minus_two); - ensure(">=", minus_two >= minus_two); - ensure("==", three == LLSimdScalar(3.f)); - ensure("!=", three != minus_two); - ensure("isApproximatelyEqual", three.isApproximatelyEqual(LLSimdScalar(3.f + 1e-6f))); - ensure("isApproximatelyEqual outside", !three.isApproximatelyEqual(minus_two)); + mm.setMax(three, minus_two); CHECK(mm.getF32() == 3.f); + mm.setMin(three, minus_two); CHECK(mm.getF32() == -2.f); + + CHECK(minus_two < three); + CHECK(three <= three); + CHECK(three > minus_two); + CHECK(minus_two >= minus_two); + CHECK(three == LLSimdScalar(3.f)); + CHECK(three != minus_two); + CHECK(three.isApproximatelyEqual(LLSimdScalar(3.f + 1e-6f))); + CHECK_FALSE(three.isApproximatelyEqual(minus_two)); // a dot product comes back in the lane that is read - ensure_equals("a dot as scalar", n_a().dot3(LLVector4a(1.f, 0.f, 0.f)).getF32(), na[0]); + CHECK(n_a().dot3(LLVector4a(1.f, 0.f, 0.f)).getF32() == na[0]); } } diff --git a/indra/llmath/tests/m3math_test.cpp b/indra/llmath/tests/m3math_test.cpp index 6a9a841436..35b6ba611c 100644 --- a/indra/llmath/tests/m3math_test.cpp +++ b/indra/llmath/tests/m3math_test.cpp @@ -35,7 +35,7 @@ #include "../llquaternion.h" #include "../v3dmath.h" -#include "../test/lltut.h" +#include #if LL_WINDOWS // disable unreachable code warnings caused by usage of skip. @@ -47,76 +47,62 @@ #pragma warning(disable: 4702) #endif -namespace tut -{ namespace { - struct m3math_test - { - }; - typedef test_group m3math_test_t; - typedef m3math_test_t::object m3math_test_object_t; - tut::m3math_test_t tut_m3math_test("m3math_h"); -} - //test case for setIdentity() fn. - template<> template<> - void m3math_test_object_t::test<1>() + TEST_CASE("m3math_h: setIdentity makes the identity matrix", "[m3math_h]") { LLMatrix3 llmat3_obj; llmat3_obj.setIdentity(); - ensure("LLMatrix3::setIdentity failed", 1.f == llmat3_obj.mMatrix[0][0] && - 0.f == llmat3_obj.mMatrix[0][1] && - 0.f == llmat3_obj.mMatrix[0][2] && - 0.f == llmat3_obj.mMatrix[1][0] && - 1.f == llmat3_obj.mMatrix[1][1] && - 0.f == llmat3_obj.mMatrix[1][2] && - 0.f == llmat3_obj.mMatrix[2][0] && - 0.f == llmat3_obj.mMatrix[2][1] && - 1.f == llmat3_obj.mMatrix[2][2]); + CHECK(llmat3_obj.mMatrix[0][0] == 1.f); + CHECK(llmat3_obj.mMatrix[0][1] == 0.f); + CHECK(llmat3_obj.mMatrix[0][2] == 0.f); + CHECK(llmat3_obj.mMatrix[1][0] == 0.f); + CHECK(llmat3_obj.mMatrix[1][1] == 1.f); + CHECK(llmat3_obj.mMatrix[1][2] == 0.f); + CHECK(llmat3_obj.mMatrix[2][0] == 0.f); + CHECK(llmat3_obj.mMatrix[2][1] == 0.f); + CHECK(llmat3_obj.mMatrix[2][2] == 1.f); } //test case for LLMatrix3& setZero() fn. - template<> template<> - void m3math_test_object_t::test<2>() + TEST_CASE("m3math_h: setZero clears every element", "[m3math_h]") { LLMatrix3 llmat3_obj; llmat3_obj.setZero(); - ensure("LLMatrix3::setZero failed", 0.f == llmat3_obj.setZero().mMatrix[0][0] && - 0.f == llmat3_obj.setZero().mMatrix[0][1] && - 0.f == llmat3_obj.setZero().mMatrix[0][2] && - 0.f == llmat3_obj.setZero().mMatrix[1][0] && - 0.f == llmat3_obj.setZero().mMatrix[1][1] && - 0.f == llmat3_obj.setZero().mMatrix[1][2] && - 0.f == llmat3_obj.setZero().mMatrix[2][0] && - 0.f == llmat3_obj.setZero().mMatrix[2][1] && - 0.f == llmat3_obj.setZero().mMatrix[2][2]); + CHECK(llmat3_obj.setZero().mMatrix[0][0] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[0][1] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[0][2] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[1][0] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[1][1] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[1][2] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[2][0] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[2][1] == 0.f); + CHECK(llmat3_obj.setZero().mMatrix[2][2] == 0.f); } //test case for setRows(const LLVector3 &x_axis, const LLVector3 &y_axis, const LLVector3 &z_axis) fns. - template<> template<> - void m3math_test_object_t::test<3>() + TEST_CASE("m3math_h: setRows fills the matrix row by row", "[m3math_h]") { LLMatrix3 llmat3_obj; LLVector3 vect1(2, 1, 4); LLVector3 vect2(3, 5, 7); LLVector3 vect3(6, 9, 7); llmat3_obj.setRows(vect1, vect2, vect3); - ensure("LLVector3::setRows failed ", 2 == llmat3_obj.mMatrix[0][0] && - 1 == llmat3_obj.mMatrix[0][1] && - 4 == llmat3_obj.mMatrix[0][2] && - 3 == llmat3_obj.mMatrix[1][0] && - 5 == llmat3_obj.mMatrix[1][1] && - 7 == llmat3_obj.mMatrix[1][2] && - 6 == llmat3_obj.mMatrix[2][0] && - 9 == llmat3_obj.mMatrix[2][1] && - 7 == llmat3_obj.mMatrix[2][2]); + CHECK(llmat3_obj.mMatrix[0][0] == 2); + CHECK(llmat3_obj.mMatrix[0][1] == 1); + CHECK(llmat3_obj.mMatrix[0][2] == 4); + CHECK(llmat3_obj.mMatrix[1][0] == 3); + CHECK(llmat3_obj.mMatrix[1][1] == 5); + CHECK(llmat3_obj.mMatrix[1][2] == 7); + CHECK(llmat3_obj.mMatrix[2][0] == 6); + CHECK(llmat3_obj.mMatrix[2][1] == 9); + CHECK(llmat3_obj.mMatrix[2][2] == 7); } //test case for getFwdRow(), getLeftRow(), getUpRow() fns. - template<> template<> - void m3math_test_object_t::test<4>() + TEST_CASE("m3math_h: getFwdRow/getLeftRow/getUpRow return the rows set", "[m3math_h]") { LLMatrix3 llmat3_obj; LLVector3 vect1(2, 1, 4); @@ -124,14 +110,13 @@ namespace LLVector3 vect3(6, 9, 7); llmat3_obj.setRows(vect1, vect2, vect3); - ensure("LLVector3::getFwdRow failed ", vect1 == llmat3_obj.getFwdRow()); - ensure("LLVector3::getLeftRow failed ", vect2 == llmat3_obj.getLeftRow()); - ensure("LLVector3::getUpRow failed ", vect3 == llmat3_obj.getUpRow()); + CHECK(vect1 == llmat3_obj.getFwdRow()); + CHECK(vect2 == llmat3_obj.getLeftRow()); + CHECK(vect3 == llmat3_obj.getUpRow()); } //test case for operator*(const LLMatrix3 &a, const LLMatrix3 &b) - template<> template<> - void m3math_test_object_t::test<5>() + TEST_CASE("m3math_h: operator* multiplies two matrices", "[m3math_h]") { LLMatrix3 llmat_obj1; LLMatrix3 llmat_obj2; @@ -153,22 +138,20 @@ namespace llmat_obj2.setRows(llvec4, llvec5, llvec6); llmat_obj3.setRows(llvec7, llvec8, llvec9); llmat_obj3 = llmat_obj1 * llmat_obj2; - ensure("LLMatrix3::operator*(const LLMatrix3 &a, const LLMatrix3 &b) failed", - 50 == llmat_obj3.mMatrix[0][0] && - 49 == llmat_obj3.mMatrix[0][1] && - 39 == llmat_obj3.mMatrix[0][2] && - 29 == llmat_obj3.mMatrix[1][0] && - 45 == llmat_obj3.mMatrix[1][1] && - 65 == llmat_obj3.mMatrix[1][2] && - 94 == llmat_obj3.mMatrix[2][0] && - 94 == llmat_obj3.mMatrix[2][1] && - 86 == llmat_obj3.mMatrix[2][2]); + CHECK(llmat_obj3.mMatrix[0][0] == 50); + CHECK(llmat_obj3.mMatrix[0][1] == 49); + CHECK(llmat_obj3.mMatrix[0][2] == 39); + CHECK(llmat_obj3.mMatrix[1][0] == 29); + CHECK(llmat_obj3.mMatrix[1][1] == 45); + CHECK(llmat_obj3.mMatrix[1][2] == 65); + CHECK(llmat_obj3.mMatrix[2][0] == 94); + CHECK(llmat_obj3.mMatrix[2][1] == 94); + CHECK(llmat_obj3.mMatrix[2][2] == 86); } //test case for operator*(const LLVector3 &a, const LLMatrix3 &b) - template<> template<> - void m3math_test_object_t::test<6>() + TEST_CASE("m3math_h: operator* transforms an LLVector3 by a matrix", "[m3math_h]") { LLMatrix3 llmat_obj1; @@ -184,12 +167,11 @@ namespace LLVector3 expected_result(30, 51, 53); - ensure("LLMatrix3::operator*(const LLVector3 &a, const LLMatrix3 &b) failed", res_vec == expected_result); + CHECK(res_vec == expected_result); } //test case for operator*(const LLVector3d &a, const LLMatrix3 &b) - template<> template<> - void m3math_test_object_t::test<7>() + TEST_CASE("m3math_h: operator* transforms an LLVector3d by a matrix", "[m3math_h]") { LLMatrix3 llmat_obj1; LLVector3d llvec3d1; @@ -204,12 +186,11 @@ namespace LLVector3d expected_result(25, 30, 3); - ensure("LLMatrix3::operator*(const LLVector3 &a, const LLMatrix3 &b) failed", llvec3d1 == expected_result); + CHECK(llvec3d1 == expected_result); } // test case for operator==(const LLMatrix3 &a, const LLMatrix3 &b) - template<> template<> - void m3math_test_object_t::test<8>() + TEST_CASE("m3math_h: operator== and operator!= compare matrices", "[m3math_h]") { LLMatrix3 llmat_obj1; LLMatrix3 llmat_obj2; @@ -220,15 +201,14 @@ namespace llmat_obj1.setRows(llvec1, llvec2, llvec3); llmat_obj2.setRows(llvec1, llvec2, llvec3); - ensure("LLMatrix3::operator==(const LLMatrix3 &a, const LLMatrix3 &b) failed", llmat_obj1 == llmat_obj2); + CHECK(llmat_obj1 == llmat_obj2); llmat_obj2.setRows(llvec2, llvec2, llvec3); - ensure("LLMatrix3::operator!=(const LLMatrix3 &a, const LLMatrix3 &b) failed", llmat_obj1 != llmat_obj2); + CHECK(llmat_obj1 != llmat_obj2); } //test case for quaternion() fn. - template<> template<> - void m3math_test_object_t::test<9>() + TEST_CASE("m3math_h: quaternion converts the matrix to a quaternion", "[m3math_h]") { LLMatrix3 llmat_obj1; LLQuaternion llmat_quat; @@ -239,15 +219,14 @@ namespace llmat_obj1.setRows(llmat1, llmat2, llmat3); llmat_quat = llmat_obj1.quaternion(); - ensure("LLMatrix3::quaternion failed ", is_approx_equal(-0.66666669f, llmat_quat.mQ[0]) && - is_approx_equal(-0.83333337f, llmat_quat.mQ[1]) && - is_approx_equal(0.0f, llmat_quat.mQ[2]) && - is_approx_equal(1.5f, llmat_quat.mQ[3])); + CHECK(is_approx_equal(-0.66666669f, llmat_quat.mQ[0])); + CHECK(is_approx_equal(-0.83333337f, llmat_quat.mQ[1])); + CHECK(is_approx_equal(0.0f, llmat_quat.mQ[2])); + CHECK(is_approx_equal(1.5f, llmat_quat.mQ[3])); } //test case for transpose() fn. - template<> template<> - void m3math_test_object_t::test<10>() + TEST_CASE("m3math_h: transpose swaps rows and columns", "[m3math_h]") { LLMatrix3 llmat_obj; @@ -264,12 +243,11 @@ namespace LLMatrix3 expectedllmat_obj; expectedllmat_obj.setRows(resllvec1, resllvec2, resllvec3); - ensure("LLMatrix3::transpose failed ", llmat_obj == expectedllmat_obj); + CHECK(llmat_obj == expectedllmat_obj); } //test case for determinant() fn. - template<> template<> - void m3math_test_object_t::test<11>() + TEST_CASE("m3math_h: determinant of a singular matrix is zero", "[m3math_h]") { LLMatrix3 llmat_obj1; @@ -277,12 +255,11 @@ namespace LLVector3 llvec2(3, 2, 1); LLVector3 llvec3(2, 2, 2); llmat_obj1.setRows(llvec1, llvec2, llvec3); - ensure("LLMatrix3::determinant failed ", 0.0f == llmat_obj1.determinant()); + CHECK(llmat_obj1.determinant() == 0.0f); } //test case for orthogonalize() fn. - template<> template<> - void m3math_test_object_t::test<12>() + TEST_CASE("m3math_h: orthogonalize makes the rows orthonormal", "[m3math_h]") { LLMatrix3 llmat_obj; @@ -290,26 +267,24 @@ namespace LLVector3 llvec2(1, 2, 0); LLVector3 llvec3(2, 4, 2); - skip("This test fails depending on architecture. Need to fix comparison operation, is_approx_equal, to work on more than one platform."); + SKIP("This test fails depending on architecture. Need to fix comparison operation, is_approx_equal, to work on more than one platform."); llmat_obj.setRows(llvec1, llvec2, llvec3); llmat_obj.orthogonalize(); - ensure("LLMatrix3::orthogonalize failed ", - is_approx_equal(0.19611614f, llmat_obj.mMatrix[0][0]) && - is_approx_equal(0.78446454f, llmat_obj.mMatrix[0][1]) && - is_approx_equal(0.58834841f, llmat_obj.mMatrix[0][2]) && - is_approx_equal(0.47628204f, llmat_obj.mMatrix[1][0]) && - is_approx_equal(0.44826545f, llmat_obj.mMatrix[1][1]) && - is_approx_equal(-0.75644795f, llmat_obj.mMatrix[1][2]) && - is_approx_equal(-0.85714286f, llmat_obj.mMatrix[2][0]) && - is_approx_equal(0.42857143f, llmat_obj.mMatrix[2][1]) && - is_approx_equal(-0.28571429f, llmat_obj.mMatrix[2][2])); + CHECK(is_approx_equal(0.19611614f, llmat_obj.mMatrix[0][0])); + CHECK(is_approx_equal(0.78446454f, llmat_obj.mMatrix[0][1])); + CHECK(is_approx_equal(0.58834841f, llmat_obj.mMatrix[0][2])); + CHECK(is_approx_equal(0.47628204f, llmat_obj.mMatrix[1][0])); + CHECK(is_approx_equal(0.44826545f, llmat_obj.mMatrix[1][1])); + CHECK(is_approx_equal(-0.75644795f, llmat_obj.mMatrix[1][2])); + CHECK(is_approx_equal(-0.85714286f, llmat_obj.mMatrix[2][0])); + CHECK(is_approx_equal(0.42857143f, llmat_obj.mMatrix[2][1])); + CHECK(is_approx_equal(-0.28571429f, llmat_obj.mMatrix[2][2])); } //test case for adjointTranspose() fn. - template<> template<> - void m3math_test_object_t::test<13>() + TEST_CASE("m3math_h: adjointTranspose gives the expected cofactors", "[m3math_h]") { LLMatrix3 llmat_obj; @@ -320,22 +295,20 @@ namespace llmat_obj.setRows(llvec1, llvec2, llvec3); llmat_obj.adjointTranspose(); - ensure("LLMatrix3::adjointTranspose failed ", 10 == llmat_obj.mMatrix[0][0] && - -45 == llmat_obj.mMatrix[1][0] && - 30 == llmat_obj.mMatrix[2][0] && - -10 == llmat_obj.mMatrix[0][1] && - 21 == llmat_obj.mMatrix[1][1] && - -12 == llmat_obj.mMatrix[2][1] && - 0 == llmat_obj.mMatrix[0][2] && - 3 == llmat_obj.mMatrix[1][2] && - -6 == llmat_obj.mMatrix[2][2]); + CHECK(llmat_obj.mMatrix[0][0] == 10); + CHECK(llmat_obj.mMatrix[1][0] == -45); + CHECK(llmat_obj.mMatrix[2][0] == 30); + CHECK(llmat_obj.mMatrix[0][1] == -10); + CHECK(llmat_obj.mMatrix[1][1] == 21); + CHECK(llmat_obj.mMatrix[2][1] == -12); + CHECK(llmat_obj.mMatrix[0][2] == 0); + CHECK(llmat_obj.mMatrix[1][2] == 3); + CHECK(llmat_obj.mMatrix[2][2] == -6); } /* TBD: Need to add test cases for getEulerAngles() and setRot() functions */ - // The field-only operations of both matrices evaluate at compile time. - template<> template<> - void m3math_test_object_t::test<14>() + TEST_CASE("m3math_h: field-only operations of both matrices evaluate at compile time", "[m3math_h]") { constexpr LLMatrix3 product = []() constexpr { @@ -401,6 +374,10 @@ namespace }(); static_assert(sum.mMatrix[0][0] == 3.f && sum.mMatrix[0][1] == 0.f && sum.mMatrix[3][3] == 3.f); - ensure("compile-time matrices agree at run time", product.mMatrix[2][2] == 47.f && inverse.getTranslation().mV[2] == -3.f); + { + INFO("compile-time matrices agree at run time"); + CHECK(product.mMatrix[2][2] == 47.f); + CHECK(inverse.getTranslation().mV[2] == -3.f); + } } } diff --git a/indra/llmath/tests/mathmisc_test.cpp b/indra/llmath/tests/mathmisc_test.cpp index c28d353ace..70dd912dee 100644 --- a/indra/llmath/tests/mathmisc_test.cpp +++ b/indra/llmath/tests/mathmisc_test.cpp @@ -27,7 +27,7 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../test/alrandomreseed.h" #include "llcrc.h" @@ -40,214 +40,153 @@ #include -namespace tut -{ namespace { - struct math_data - { - }; - typedef test_group math_test; - typedef math_test::object math_object; - tut::math_test tm("BasicLindenMath"); -} - - template<> template<> - void math_object::test<1>() + TEST_CASE("BasicLindenMath: llabs of an S32", "[BasicLindenMath]") { S32 val = 89543; val = llabs(val); - ensure("integer absolute value 1", (89543 == val)); + CHECK(val == 89543); val = -500; val = llabs(val); - ensure("integer absolute value 2", (500 == val)); + CHECK(val == 500); } - template<> template<> - void math_object::test<2>() + TEST_CASE("BasicLindenMath: llabs of an F32", "[BasicLindenMath]") { F32 val = -2583.4f; val = llabs(val); - ensure("float absolute value 1", (2583.4f == val)); + CHECK(val == 2583.4f); val = 430903.f; val = llabs(val); - ensure("float absolute value 2", (430903.f == val)); + CHECK(val == 430903.f); } - template<> template<> - void math_object::test<3>() + TEST_CASE("BasicLindenMath: llabs of an F64", "[BasicLindenMath]") { F64 val = 387439393.987329839; val = llabs(val); - ensure("double absolute value 1", (387439393.987329839 == val)); + CHECK(val == 387439393.987329839); val = -8937843.9394878; val = llabs(val); - ensure("double absolute value 2", (8937843.9394878 == val)); + CHECK(val == 8937843.9394878); } - template<> template<> - void math_object::test<4>() + TEST_CASE("BasicLindenMath: lltrunc of an F32", "[BasicLindenMath]") { F32 val = 430903.9f; S32 val1 = lltrunc(val); - ensure("float truncate value 1", (430903 == val1)); + CHECK(val1 == 430903); val = -2303.9f; val1 = lltrunc(val); - ensure("float truncate value 2", (-2303 == val1)); + CHECK(val1 == -2303); } - template<> template<> - void math_object::test<5>() + TEST_CASE("BasicLindenMath: lltrunc of an F64", "[BasicLindenMath]") { F64 val = 387439393.987329839 ; S32 val1 = lltrunc(val); - ensure("float truncate value 1", (387439393 == val1)); + CHECK(val1 == 387439393); val = -387439393.987329839; val1 = lltrunc(val); - ensure("float truncate value 2", (-387439393 == val1)); + CHECK(val1 == -387439393); } - template<> template<> - void math_object::test<6>() + TEST_CASE("BasicLindenMath: llfloor of an F32", "[BasicLindenMath]") { F32 val = 430903.2f; S32 val1 = llfloor(val); - ensure("float llfloor value 1", (430903 == val1)); + CHECK(val1 == 430903); val = -430903.9f; val1 = llfloor(val); - ensure("float llfloor value 2", (-430904 == val1)); + CHECK(val1 == -430904); } - template<> template<> - void math_object::test<7>() + TEST_CASE("BasicLindenMath: llceil of an F32", "[BasicLindenMath]") { F32 val = 430903.2f; S32 val1 = llceil(val); - ensure("float llceil value 1", (430904 == val1)); + CHECK(val1 == 430904); val = -430903.9f; val1 = llceil(val); - ensure("float llceil value 2", (-430903 == val1)); + CHECK(val1 == -430903); } - template<> template<> - void math_object::test<8>() + TEST_CASE("BasicLindenMath: ll_round of an F32 to an integer", "[BasicLindenMath]") { F32 val = 430903.2f; S32 val1 = ll_round(val); - ensure("float ll_round value 1", (430903 == val1)); + CHECK(val1 == 430903); val = -430903.9f; val1 = ll_round(val); - ensure("float ll_round value 2", (-430904 == val1)); + CHECK(val1 == -430904); } - template<> template<> - void math_object::test<9>() + TEST_CASE("BasicLindenMath: ll_round of an F32 to the nearest multiple", "[BasicLindenMath]") { F32 val = 430905.2654f, nearest = 100.f; val = ll_round(val, nearest); - ensure("float ll_round value 1", (430900 == val)); + CHECK(val == 430900); val = -430905.2654f, nearest = 10.f; val = ll_round(val, nearest); - ensure("float ll_round value 1", (-430910 == val)); + CHECK(val == -430910); } - template<> template<> - void math_object::test<10>() + TEST_CASE("BasicLindenMath: ll_round of an F64 to the nearest multiple", "[BasicLindenMath]") { F64 val = 430905.2654, nearest = 100.0; val = ll_round(val, nearest); - ensure("double ll_round value 1", (430900 == val)); + CHECK(val == 430900); val = -430905.2654, nearest = 10.0; val = ll_round(val, nearest); - ensure("double ll_round value 1", (-430910.00000 == val)); + CHECK(val == -430910.00000); } - template<> template<> - void math_object::test<11>() + TEST_CASE("BasicLindenMath: llsimple_angle wraps an angle into -pi to pi", "[BasicLindenMath]") { const F32 F_PI = 3.1415926535897932384626433832795f; F32 angle = 3506.f; angle = llsimple_angle(angle); - ensure("llsimple_angle value 1", (angle <=F_PI && angle >= -F_PI)); + CHECK(angle <=F_PI); + CHECK(angle >= -F_PI); angle = -431.f; angle = llsimple_angle(angle); - ensure("llsimple_angle value 1", (angle <=F_PI && angle >= -F_PI)); - } -} - -namespace tut -{ -namespace -{ - struct uuid_data - { - LLUUID id; - }; - typedef test_group uuid_test; - typedef uuid_test::object uuid_object; - tut::uuid_test tu("mathmisc LLUUID"); -} - - template<> template<> - void uuid_object::test<1>() - { - ensure("uuid null", id.isNull()); - id.generate(); - ensure("generate not null", id.notNull()); - id.setNull(); - ensure("set null", id.isNull()); - } - - template<> template<> - void uuid_object::test<2>() - { - id.generate(); - LLUUID a(id); - ensure_equals("copy equal", id, a); - a.generate(); - ensure_not_equals("generate not equal", id, a); - a = id; - ensure_equals("assignment equal", id, a); + CHECK(angle <=F_PI); + CHECK(angle >= -F_PI); } - template<> template<> - void uuid_object::test<3>() + // The floating-point environment this executable was linked with. The + // build sets flush-to-zero and denormals-are-zero per thread on purpose, + // in code; nothing may impose them on the whole process from the link + // line. A denormal that survives a multiply here means no crtfastmath.o + // was linked. + TEST_CASE("BasicLindenMath: the process default keeps denormals", "[BasicLindenMath]") { - id.generate(); - LLUUID copy(id); - LLUUID mask; - mask.generate(); - copy ^= mask; - ensure_not_equals("mask not equal", id, copy); - copy ^= mask; - ensure_equals("mask back", id, copy); + volatile float denormal = 1.0e-40f; + volatile float one = 1.0f; + float product = denormal * one; + UNSCOPED_INFO("denormals are not flushed by the process default"); + CHECK(product != 0.0f); + UNSCOPED_INFO("a denormal is still a denormal"); + CHECK(product == 1.0e-40f); } - template<> template<> - void uuid_object::test<4>() + // Optimised builds relax floating-point semantics to MSVC's /fp:fast + // contract, which keeps NaN and infinity. -ffinite-math-only would fold + // both of these guards to a constant. + TEST_CASE("BasicLindenMath: optimised builds keep NaN and infinity", "[BasicLindenMath]") { - id.generate(); - std::string id_str = id.asString(); - LLUUID copy(id_str.c_str()); - ensure_equals("string serialization", id, copy); + volatile float zero = 0.0f; + float nan = zero / zero; + float inf = 1.0f / zero; + CHECK(std::isnan(nan)); + CHECK(std::isinf(inf)); } - } -namespace tut -{ namespace { - struct crc_data - { - }; - typedef test_group crc_test; - typedef crc_test::object crc_object; - tut::crc_test tc("LLCrc"); -} - - template<> template<> - void crc_object::test<1>() + TEST_CASE("LLCrc: a buffer update equals updating one char at a time", "[LLCrc]") { /* Test buffer update and individual char update */ const char TEST_BUFFER[] = "hello &#$)$&Nd0"; @@ -259,11 +198,10 @@ namespace c2.update(*rh); ++rh; } - ensure_equals("crc update 1", c1.getCRC(), c2.getCRC()); + CHECK(c1.getCRC() == c2.getCRC()); } - template<> template<> - void crc_object::test<2>() + TEST_CASE("LLCrc: buffer and char updates mixed in either order agree", "[LLCrc]") { /* Test mixing of buffer and individual char update */ const char TEST_BUFFER1[] = "Split Buffer one $^%$%#@$"; @@ -285,12 +223,10 @@ namespace } c2.update((U8*)TEST_BUFFER2, sizeof(TEST_BUFFER2) - 1); - ensure_equals("crc update 2", c1.getCRC(), c2.getCRC()); + CHECK(c1.getCRC() == c2.getCRC()); } } -namespace tut -{ namespace { F32 SMALL_RADIUS = 1.0f; @@ -299,16 +235,11 @@ namespace struct line_data { - // Tests 1 and 3 seed the main thread's generator. + // The intersects and two-planes cases seed the main thread's generator. ~line_data() { ll_test::reseedRandomFromEntropy(); } }; - typedef test_group line_test; - typedef line_test::object line_object; - tut::line_test tline("LLLine"); -} - template<> template<> - void line_object::test<1>() + TEST_CASE_METHOD(line_data, "LLLine: intersects finds points within a tolerance of the line", "[LLLine]") { // this is a test for LLLine::intersects(point) which returns true // if the line passes within some tolerance of point @@ -363,15 +294,16 @@ namespace // test that the intersection point is within MEDIUM_RADIUS + SMALL_RADIUS F32 test_radius = MEDIUM_RADIUS + SMALL_RADIUS; test_radius += (LARGE_RADIUS * allowable_relative_error); - ensure("line should pass near intersection point", line.intersects(point, test_radius)); + UNSCOPED_INFO("line should pass near intersection point"); + REQUIRE(line.intersects(point, test_radius)); test_radius = allowable_relative_error * (point - point_on_line).length(); - ensure("line should intersect point used to define it", line.intersects(point_on_line, test_radius)); + UNSCOPED_INFO("line should intersect point used to define it"); + REQUIRE(line.intersects(point_on_line, test_radius)); } } - template<> template<> - void line_object::test<2>() + TEST_CASE_METHOD(line_data, "LLLine: nearestApproach is disabled for DEV-16600", "[LLLine]") { /* These tests fail intermittently on all platforms - see DEV-16600 @@ -481,13 +413,9 @@ namespace */ } -namespace -{ F32 ALMOST_PARALLEL = 0.99f; -} - template<> template<> - void line_object::test<3>() + TEST_CASE_METHOD(line_data, "LLLine: getIntersectionBetweenTwoPlanes on axis and random planes", "[LLLine]") { // this is a test for LLLine::getIntersectionBetweenTwoPlanes() method @@ -504,9 +432,12 @@ namespace bool y_success = LLLine::getIntersectionBetweenTwoPlanes(y_line, yz_plane, xy_plane); bool z_success = LLLine::getIntersectionBetweenTwoPlanes(z_line, zx_plane, yz_plane); - ensure("xy and zx planes should intersect", x_success); - ensure("yz and xy planes should intersect", y_success); - ensure("zx and yz planes should intersect", z_success); + UNSCOPED_INFO("xy and zx planes should intersect"); + CHECK(x_success); + UNSCOPED_INFO("yz and xy planes should intersect"); + CHECK(y_success); + UNSCOPED_INFO("zx and yz planes should intersect"); + CHECK(z_success); // These directions come from normalize() of a cross product of unit // axes -- analytically exact (1, 0, 0) etc., but under -ffast-math the @@ -514,20 +445,26 @@ namespace // component drifts to ~0.99997. Use a tolerance instead of ==. constexpr F32 AXIS_EPS = 1e-4f; LLVector3 direction = x_line.getDirection(); - ensure("x_line should be parallel to x_axis", - fabs(fabs(direction.mV[VX]) - 1.f) < AXIS_EPS - && fabs(direction.mV[VY]) < AXIS_EPS - && fabs(direction.mV[VZ]) < AXIS_EPS); + { + INFO("x_line should be parallel to x_axis"); + CHECK(fabs(fabs(direction.mV[VX]) - 1.f) < AXIS_EPS); + CHECK(fabs(direction.mV[VY]) < AXIS_EPS); + CHECK(fabs(direction.mV[VZ]) < AXIS_EPS); + } direction = y_line.getDirection(); - ensure("y_line should be parallel to y_axis", - fabs(direction.mV[VX]) < AXIS_EPS - && fabs(fabs(direction.mV[VY]) - 1.f) < AXIS_EPS - && fabs(direction.mV[VZ]) < AXIS_EPS); + { + INFO("y_line should be parallel to y_axis"); + CHECK(fabs(direction.mV[VX]) < AXIS_EPS); + CHECK(fabs(fabs(direction.mV[VY]) - 1.f) < AXIS_EPS); + CHECK(fabs(direction.mV[VZ]) < AXIS_EPS); + } direction = z_line.getDirection(); - ensure("z_line should be parallel to z_axis", - fabs(direction.mV[VX]) < AXIS_EPS - && fabs(direction.mV[VY]) < AXIS_EPS - && fabs(fabs(direction.mV[VZ]) - 1.f) < AXIS_EPS); + { + INFO("z_line should be parallel to z_axis"); + CHECK(fabs(direction.mV[VX]) < AXIS_EPS); + CHECK(fabs(direction.mV[VY]) < AXIS_EPS); + CHECK(fabs(fabs(direction.mV[VZ]) - 1.f) < AXIS_EPS); + } // next some random tests F32 allowable_relative_error = 0.0001f; @@ -581,43 +518,63 @@ namespace first_plane, second_plane); - ensure("plane intersection should succeed", success); + UNSCOPED_INFO("plane intersection should succeed"); + REQUIRE(success); F32 dot = fabs(known_intersection.getDirection() * measured_intersection.getDirection()); - ensure("measured intersection should be parallel to known intersection", - dot > ALMOST_PARALLEL); + UNSCOPED_INFO("measured intersection should be parallel to known intersection"); + REQUIRE(dot > ALMOST_PARALLEL); - ensure("measured intersection should pass near known point", - measured_intersection.intersects(some_point, LARGE_RADIUS * allowable_relative_error)); + UNSCOPED_INFO("measured intersection should pass near known point"); + REQUIRE(measured_intersection.intersects(some_point, LARGE_RADIUS * allowable_relative_error)); } } +} - // The floating-point environment this executable was linked with. The - // build sets flush-to-zero and denormals-are-zero per thread on purpose, - // in code; nothing may impose them on the whole process from the link - // line. A denormal that survives a multiply here means no crtfastmath.o - // was linked. - template<> template<> - void math_object::test<12>() +namespace +{ + struct uuid_data { - volatile float denormal = 1.0e-40f; - volatile float one = 1.0f; - float product = denormal * one; - ensure("denormals are not flushed by the process default", product != 0.0f); - ensure("a denormal is still a denormal", product == 1.0e-40f); + LLUUID id; + }; + + TEST_CASE_METHOD(uuid_data, "mathmisc LLUUID: isNull by default and after setNull but not after generate", "[mathmisc_LLUUID]") + { + CHECK(id.isNull()); + id.generate(); + CHECK(id.notNull()); + id.setNull(); + CHECK(id.isNull()); } - // Optimised builds relax floating-point semantics to MSVC's /fp:fast - // contract, which keeps NaN and infinity. -ffinite-math-only would fold - // both of these guards to a constant. - template<> template<> - void math_object::test<13>() + TEST_CASE_METHOD(uuid_data, "mathmisc LLUUID: copies and assignments compare equal", "[mathmisc_LLUUID]") { - volatile float zero = 0.0f; - float nan = zero / zero; - float inf = 1.0f / zero; - ensure("isnan sees a NaN", std::isnan(nan)); - ensure("isinf sees an infinity", std::isinf(inf)); + id.generate(); + LLUUID a(id); + CHECK(id == a); + a.generate(); + CHECK(id != a); + a = id; + CHECK(id == a); } -} + TEST_CASE_METHOD(uuid_data, "mathmisc LLUUID: xor with a mask twice restores the id", "[mathmisc_LLUUID]") + { + id.generate(); + LLUUID copy(id); + LLUUID mask; + mask.generate(); + copy ^= mask; + CHECK(id != copy); + copy ^= mask; + CHECK(id == copy); + } + + TEST_CASE_METHOD(uuid_data, "mathmisc LLUUID: asString round-trips through the string constructor", "[mathmisc_LLUUID]") + { + id.generate(); + std::string id_str = id.asString(); + LLUUID copy(id_str.c_str()); + CHECK(id == copy); + } +} diff --git a/indra/llmath/tests/v2math_test.cpp b/indra/llmath/tests/v2math_test.cpp index 7bce7d5fe7..7f71583579 100644 --- a/indra/llmath/tests/v2math_test.cpp +++ b/indra/llmath/tests/v2math_test.cpp @@ -27,103 +27,98 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../v2math.h" -namespace tut -{ namespace { - struct v2math_data - { - }; - typedef test_group v2math_test; - typedef v2math_test::object v2math_object; - tut::v2math_test v2math_testcase("v2math_h"); -} - - template<> template<> - void v2math_object::test<1>() + TEST_CASE("v2math_h: constructors and clearVec/zeroVec", "[v2math_h]") { LLVector2 vec2; - ensure("LLVector2:Fail to initialize ", (0.f == vec2.mV[VX] && 0.f == vec2.mV[VY])); + CHECK(vec2.mV[VX] == 0.f); + CHECK(vec2.mV[VY] == 0.f); F32 x =2.0f, y = 3.2f ; LLVector2 vec3(x,y); - ensure("LLVector2(F32 x, F32 y):Fail to initialize ", (x == vec3.mV[VX]) && (y == vec3.mV[VY])); + CHECK(x == vec3.mV[VX]); + CHECK(y == vec3.mV[VY]); const F32 vec[2] = {3.2f, 4.5f}; LLVector2 vec4(vec); - ensure("LLVector2(const F32 *vec):Fail to initialize ", (vec[0] == vec4.mV[VX]) && (vec[1] == vec4.mV[VY])); + CHECK(vec[0] == vec4.mV[VX]); + CHECK(vec[1] == vec4.mV[VY]); vec4.clearVec(); - ensure("clearVec():Fail to clean the values ", (0.f == vec4.mV[VX] && 0.f == vec4.mV[VY])); + CHECK(vec4.mV[VX] == 0.f); + CHECK(vec4.mV[VY] == 0.f); vec3.zeroVec(); - ensure("zeroVec():Fail to fill the zero ", (0.f == vec3.mV[VX] && 0.f == vec3.mV[VY])); + CHECK(vec3.mV[VX] == 0.f); + CHECK(vec3.mV[VY] == 0.f); } - template<> template<> - void v2math_object::test<2>() + TEST_CASE("v2math_h: the setVec overloads", "[v2math_h]") { F32 x = 123.356f, y = 2387.453f; LLVector2 vec2,vec3; vec2.setVec(x, y); - ensure("1:setVec: Fail ", (x == vec2.mV[VX]) && (y == vec2.mV[VY])); + CHECK(x == vec2.mV[VX]); + CHECK(y == vec2.mV[VY]); vec3.setVec(vec2); - ensure("2:setVec: Fail " ,(vec2 == vec3)); + CHECK(vec2 == vec3); vec3.zeroVec(); const F32 vec[2] = {3.24653f, 457653.4f}; vec3.setVec(vec); - ensure("3:setVec: Fail ", (vec[0] == vec3.mV[VX]) && (vec[1] == vec3.mV[VY])); + CHECK(vec[0] == vec3.mV[VX]); + CHECK(vec[1] == vec3.mV[VY]); } - template<> template<> - void v2math_object::test<3>() + TEST_CASE("v2math_h: magVecSquared and magVec", "[v2math_h]") { F32 x = 2.2345f, y = 3.5678f ; LLVector2 vec2(x,y); - ensure("magVecSquared:Fail ", is_approx_equal(vec2.magVecSquared(), (x*x + y*y))); - ensure("magVec:Fail ", is_approx_equal(vec2.magVec(), (F32) sqrt(x*x + y*y))); + CHECK(is_approx_equal(vec2.magVecSquared(), (x*x + y*y))); + CHECK(is_approx_equal(vec2.magVec(), (F32) sqrt(x*x + y*y))); } - template<> template<> - void v2math_object::test<4>() + TEST_CASE("v2math_h: abs then isNull on long and very short vectors", "[v2math_h]") { F32 x =-2.0f, y = -3.0f ; LLVector2 vec2(x,y); - ensure_equals("abs():Fail", vec2.abs(), true); - ensure("abs() x", is_approx_equal(vec2.mV[VX], 2.f)); - ensure("abs() y", is_approx_equal(vec2.mV[VY], 3.f)); + CHECK(vec2.abs()); + CHECK(is_approx_equal(vec2.mV[VX], 2.f)); + CHECK(is_approx_equal(vec2.mV[VY], 3.f)); - ensure("isNull():Fail ", false == vec2.isNull()); //Returns true if vector has a _very_small_ length + CHECK_FALSE(vec2.isNull()); //Returns true if vector has a _very_small_ length x =.00000001f, y = .000001001f; vec2.setVec(x, y); - ensure("isNull(): Fail ", true == vec2.isNull()); + CHECK(vec2.isNull()); } - template<> template<> - void v2math_object::test<5>() + TEST_CASE("v2math_h: scaleVec and isExactlyZero", "[v2math_h]") { F32 x =1.f, y = 2.f; LLVector2 vec2(x, y), vec3; vec3 = vec3.scaleVec(vec2); - ensure("scaleVec: Fail ", vec3.mV[VX] == 0. && vec3.mV[VY] == 0.); - ensure("isExactlyZero(): Fail", true == vec3.isExactlyZero()); + CHECK(vec3.mV[VX] == 0.); + CHECK(vec3.mV[VY] == 0.); + + CHECK(vec3.isExactlyZero()); vec3.setVec(2.f, 1.f); vec3 = vec3.scaleVec(vec2); - ensure("scaleVec: Fail ", (2.f == vec3.mV[VX]) && (2.f == vec3.mV[VY])); - ensure("isExactlyZero():Fail", false == vec3.isExactlyZero()); + CHECK(vec3.mV[VX] == 2.f); + CHECK(vec3.mV[VY] == 2.f); + + CHECK_FALSE(vec3.isExactlyZero()); } - template<> template<> - void v2math_object::test<6>() + TEST_CASE("v2math_h: operator+ adds componentwise", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, x2 = -2.3f, y2 = 1.11f; F32 val1, val2; @@ -131,7 +126,8 @@ namespace vec4 = vec2 + vec3 ; val1 = x1+x2; val2 = y1+y2; - ensure("1:operator+ failed",(val1 == vec4.mV[VX]) && ((val2 == vec4.mV[VY]))); + CHECK(val1 == vec4.mV[VX]); + CHECK(val2 == vec4.mV[VY]); vec2.clearVec(); vec3.clearVec(); @@ -141,11 +137,11 @@ namespace vec4 = vec2 + vec3; val1 = x1+x2; val2 = y1+y2; - ensure("2:operator+ failed",(val1 == vec4.mV[VX]) && ((val2 == vec4.mV[VY]))); + CHECK(val1 == vec4.mV[VX]); + CHECK(val2 == vec4.mV[VY]); } - template<> template<> - void v2math_object::test<7>() + TEST_CASE("v2math_h: operator- subtracts componentwise", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, x2 = -2.3f, y2 = 1.11f; F32 val1, val2; @@ -153,7 +149,8 @@ namespace vec4 = vec2 - vec3 ; val1 = x1-x2; val2 = y1-y2; - ensure("1:operator- failed",(val1 == vec4.mV[VX]) && ((val2 == vec4.mV[VY]))); + CHECK(val1 == vec4.mV[VX]); + CHECK(val2 == vec4.mV[VY]); vec2.clearVec(); vec3.clearVec(); @@ -164,33 +161,34 @@ namespace vec4 = vec2 - vec3; val1 = x1-x2; val2 = y1-y2; - ensure("2:operator- failed",(val1 == vec4.mV[VX]) && ((val2 == vec4.mV[VY]))); + CHECK(val1 == vec4.mV[VX]); + CHECK(val2 == vec4.mV[VY]); } - template<> template<> - void v2math_object::test<8>() + TEST_CASE("v2math_h: operator* as dot product and scalar product", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, x2 = -2.3f, y2 = 1.11f; F32 val1, val2; LLVector2 vec2(x1, y1), vec3(x2, y2); val1 = vec2 * vec3; val2 = x1*x2 + y1*y2; - ensure("1:operator* failed",(val1 == val2)); + CHECK(val1 == val2); vec3.clearVec(); F32 mulVal = 4.332f; vec3 = vec2 * mulVal; val1 = x1*mulVal; val2 = y1*mulVal; - ensure("2:operator* failed",(val1 == vec3.mV[VX]) && (val2 == vec3.mV[VY])); + CHECK(val1 == vec3.mV[VX]); + CHECK(val2 == vec3.mV[VY]); vec3.clearVec(); vec3 = mulVal * vec2; - ensure("3:operator* failed",(val1 == vec3.mV[VX]) && (val2 == vec3.mV[VY])); + CHECK(val1 == vec3.mV[VX]); + CHECK(val2 == vec3.mV[VY]); } - template<> template<> - void v2math_object::test<9>() + TEST_CASE("v2math_h: operator/ divides by a scalar", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, div = 3.2f; F32 val1, val2; @@ -198,7 +196,8 @@ namespace vec3 = vec2 / div; val1 = x1 / div; val2 = y1 / div; - ensure("1:operator/ failed", is_approx_equal(val1, vec3.mV[VX]) && is_approx_equal(val2, vec3.mV[VY])); + CHECK(is_approx_equal(val1, vec3.mV[VX])); + CHECK(is_approx_equal(val2, vec3.mV[VY])); vec3.clearVec(); x1 = -.235f, y1 = -24.32f, div = -2.2f; @@ -206,11 +205,11 @@ namespace vec3 = vec2 / div; val1 = x1 / div; val2 = y1 / div; - ensure("2:operator/ failed", is_approx_equal(val1, vec3.mV[VX]) && is_approx_equal(val2, vec3.mV[VY])); + CHECK(is_approx_equal(val1, vec3.mV[VX])); + CHECK(is_approx_equal(val2, vec3.mV[VY])); } - template<> template<> - void v2math_object::test<10>() + TEST_CASE("v2math_h: operator% is the cross product", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, x2 = -2.3f, y2 = 1.11f; F32 val1, val2; @@ -218,7 +217,8 @@ namespace vec4 = vec2 % vec3; val1 = x1*y2 - x2*y1; val2 = y1*x2 - y2*x1; - ensure("1:operator% failed",(val1 == vec4.mV[VX]) && (val2 == vec4.mV[VY])); + CHECK(val1 == vec4.mV[VX]); + CHECK(val2 == vec4.mV[VY]); vec2.clearVec(); vec3.clearVec(); @@ -229,38 +229,38 @@ namespace vec4 = vec2 % vec3; val1 = x1*y2 - x2*y1; val2 = y1*x2 - y2*x1; - ensure("2:operator% failed",(val1 == vec4.mV[VX]) && (val2 == vec4.mV[VY])); + CHECK(val1 == vec4.mV[VX]); + CHECK(val2 == vec4.mV[VY]); } - template<> template<> - void v2math_object::test<11>() + + TEST_CASE("v2math_h: operator== for equal vectors", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f; LLVector2 vec2(x1, y1), vec3(x1, y1); - ensure("1:operator== failed",(vec2 == vec3)); + CHECK(vec2 == vec3); vec2.clearVec(); vec3.clearVec(); x1 = -.235f, y1 = -24.32f; vec2.setVec(x1, y1); vec3.setVec(vec2); - ensure("2:operator== failed",(vec2 == vec3)); + CHECK(vec2 == vec3); } - template<> template<> - void v2math_object::test<12>() + TEST_CASE("v2math_h: operator!= for different and equal vectors", "[v2math_h]") { F32 x1 = 1.f, y1 = 2.f,x2 = 2.332f, y2 = -1.23f; LLVector2 vec2(x1, y1), vec3(x2, y2); - ensure("1:operator!= failed",(vec2 != vec3)); + CHECK(vec2 != vec3); vec2.clearVec(); vec3.clearVec(); vec2.setVec(x1, y1); vec3.setVec(vec2); - ensure("2:operator!= failed", (false == (vec2 != vec3))); + CHECK_FALSE(vec2 != vec3); } - template<> template<> - void v2math_object::test<13>() + + TEST_CASE("v2math_h: operator+= and operator-=", "[v2math_h]") { F32 x1 = 1.f, y1 = 2.f,x2 = 2.332f, y2 = -1.23f; F32 val1, val2; @@ -268,13 +268,15 @@ namespace vec2 +=vec3; val1 = x1+x2; val2 = y1+y2; - ensure("1:operator+= failed",(val1 == vec2.mV[VX]) && (val2 == vec2.mV[VY])); + CHECK(val1 == vec2.mV[VX]); + CHECK(val2 == vec2.mV[VY]); vec2.setVec(x1, y1); vec2 -=vec3; val1 = x1-x2; val2 = y1-y2; - ensure("2:operator-= failed",(val1 == vec2.mV[VX]) && (val2 == vec2.mV[VY])); + CHECK(val1 == vec2.mV[VX]); + CHECK(val2 == vec2.mV[VY]); vec2.clearVec(); vec3.clearVec(); @@ -284,17 +286,18 @@ namespace vec2 +=vec3; val1 = x1+x2; val2 = y1+y2; - ensure("3:operator+= failed",(val1 == vec2.mV[VX]) && (val2 == vec2.mV[VY])); + CHECK(val1 == vec2.mV[VX]); + CHECK(val2 == vec2.mV[VY]); vec2.setVec(x1, y1); vec2 -=vec3; val1 = x1-x2; val2 = y1-y2; - ensure("4:operator-= failed", is_approx_equal(val1, vec2.mV[VX]) && is_approx_equal(val2, vec2.mV[VY])); + CHECK(is_approx_equal(val1, vec2.mV[VX])); + CHECK(is_approx_equal(val2, vec2.mV[VY])); } - template<> template<> - void v2math_object::test<14>() + TEST_CASE("v2math_h: operator/= divides by a scalar", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f; F32 val1, val2, mulVal = 4.332f; @@ -302,7 +305,8 @@ namespace vec2 /=mulVal; val1 = x1 / mulVal; val2 = y1 / mulVal; - ensure("1:operator/= failed", is_approx_equal(val1, vec2.mV[VX]) && is_approx_equal(val2, vec2.mV[VY])); + CHECK(is_approx_equal(val1, vec2.mV[VX])); + CHECK(is_approx_equal(val2, vec2.mV[VY])); vec2.clearVec(); x1 = .213f, y1 = -2.34f, mulVal = -.23f; @@ -310,11 +314,11 @@ namespace vec2 /=mulVal; val1 = x1 / mulVal; val2 = y1 / mulVal; - ensure("2:operator/= failed", is_approx_equal(val1, vec2.mV[VX]) && is_approx_equal(val2, vec2.mV[VY])); + CHECK(is_approx_equal(val1, vec2.mV[VX])); + CHECK(is_approx_equal(val2, vec2.mV[VY])); } - template<> template<> - void v2math_object::test<15>() + TEST_CASE("v2math_h: operator*= multiplies by a scalar", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f; F32 val1, val2, mulVal = 4.332f; @@ -322,7 +326,8 @@ namespace vec2 *=mulVal; val1 = x1*mulVal; val2 = y1*mulVal; - ensure("1:operator*= failed",(val1 == vec2.mV[VX]) && (val2 == vec2.mV[VY])); + CHECK(val1 == vec2.mV[VX]); + CHECK(val2 == vec2.mV[VY]); vec2.clearVec(); x1 = .213f, y1 = -2.34f, mulVal = -.23f; @@ -330,11 +335,11 @@ namespace vec2 *=mulVal; val1 = x1*mulVal; val2 = y1*mulVal; - ensure("2:operator*= failed",(val1 == vec2.mV[VX]) && (val2 == vec2.mV[VY])); + CHECK(val1 == vec2.mV[VX]); + CHECK(val2 == vec2.mV[VY]); } - template<> template<> - void v2math_object::test<16>() + TEST_CASE("v2math_h: operator%= matches the cross product", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, x2 = -2.3f, y2 = 1.11f; F32 val1, val2; @@ -342,20 +347,19 @@ namespace vec2 %= vec3; val1 = x1*y2 - x2*y1; val2 = y1*x2 - y2*x1; - ensure("1:operator%= failed",(val1 == vec2.mV[VX]) && (val2 == vec2.mV[VY])); + CHECK(val1 == vec2.mV[VX]); + CHECK(val2 == vec2.mV[VY]); } - template<> template<> - void v2math_object::test<17>() + TEST_CASE("v2math_h: unary operator- negates", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f; LLVector2 vec2(x1, y1),vec3; vec3 = -vec2; - ensure("1:operator- failed",(-vec3 == vec2)); + CHECK(-vec3 == vec2); } - template<> template<> - void v2math_object::test<18>() + TEST_CASE("v2math_h: operator<< prints equal vectors alike", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f; std::ostringstream stream1, stream2; @@ -363,59 +367,55 @@ namespace stream1 << vec2; vec3.setVec(x1, y1); stream2 << vec3; - ensure("1:operator << failed",(stream1.str() == stream2.str())); + CHECK(stream1.str() == stream2.str()); } - template<> template<> - void v2math_object::test<19>() + TEST_CASE("v2math_h: operator< orders by x then y", "[v2math_h]") { F32 x1 =1.0f, y1 = 2.0f, x2 = -.32f, y2 = .2234f; LLVector2 vec2(x1, y1),vec3(x2, y2); - ensure("1:operator < failed",(vec3 < vec2)); + CHECK(vec3 < vec2); x1 = 1.0f, y1 = 2.0f, x2 = 1.0f, y2 = 3.2234f; vec2.setVec(x1, y1); vec3.setVec(x2, y2); - ensure("2:operator < failed", (false == (vec3 < vec2))); + CHECK_FALSE(vec3 < vec2); } - template<> template<> - void v2math_object::test<20>() + TEST_CASE("v2math_h: subscript operator reads each component", "[v2math_h]") { F32 x1 =1.0f, y1 = 2.0f; LLVector2 vec2(x1, y1); - ensure("1:operator [] failed",( x1 == vec2[0])); - ensure("2:operator [] failed",( y1 == vec2[1])); + CHECK(x1 == vec2[0]); + CHECK(y1 == vec2[1]); vec2.clearVec(); x1 = 23.0f, y1 = -.2361f; vec2.setVec(x1, y1); F32 ref1 = vec2[0]; - ensure("3:operator [] failed", ( ref1 == x1)); + CHECK(ref1 == x1); F32 ref2 = vec2[1]; - ensure("4:operator [] failed", ( ref2 == y1)); + CHECK(ref2 == y1); } - template<> template<> - void v2math_object::test<21>() + TEST_CASE("v2math_h: dist_vec and its squared and 2D forms", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, x2 = -.32f, y2 = .2234f; F32 val1, val2; LLVector2 vec2(x1, y1),vec3(x2, y2); val1 = dist_vec_squared2D(vec2, vec3); val2 = (x1 - x2)*(x1 - x2) + (y1 - y2)* (y1 - y2); - ensure_equals("dist_vec_squared2D values are not equal",val2, val1); + CHECK(val2 == val1); val1 = dist_vec_squared(vec2, vec3); - ensure_equals("dist_vec_squared values are not equal",val2, val1); + CHECK(val2 == val1); val1 = dist_vec(vec2, vec3); val2 = (F32) sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)* (y1 - y2)); - ensure_equals("dist_vec values are not equal",val2, val1); + CHECK(val2 == val1); } - template<> template<> - void v2math_object::test<22>() + TEST_CASE("v2math_h: lerp between two vectors", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f, x2 = -.32f, y2 = .2234f,fVal = .0121f; F32 val1, val2; @@ -423,11 +423,11 @@ namespace LLVector2 vec4 = lerp(vec2, vec3, fVal); val1 = x1 + (x2 - x1) * fVal; val2 = y1 + (y2 - y1) * fVal; - ensure("lerp values are not equal", ((val1 == vec4.mV[VX]) && (val2 == vec4.mV[VY]))); + CHECK(val1 == vec4.mV[VX]); + CHECK(val2 == vec4.mV[VY]); } - template<> template<> - void v2math_object::test<23>() + TEST_CASE("v2math_h: normVec normalises and zeroes a tiny vector", "[v2math_h]") { F32 x1 =1.f, y1 = 2.f; F32 val1, val2; @@ -440,12 +440,16 @@ namespace val1 = x1 * oomag; val2 = y1 * oomag; - ensure("normVec failed", is_approx_equal(val1, vec2.mV[VX]) && is_approx_equal(val2, vec2.mV[VY]) && is_approx_equal(vecMag, mag)); + CHECK(is_approx_equal(val1, vec2.mV[VX])); + CHECK(is_approx_equal(val2, vec2.mV[VY])); + CHECK(is_approx_equal(vecMag, mag)); x1 =.00000001f, y1 = 0.f; vec2.setVec(x1, y1); vecMag = vec2.normVec(); - ensure("normVec failed should be 0.", 0. == vec2.mV[VX] && 0. == vec2.mV[VY] && vecMag == 0.); + CHECK(vec2.mV[VX] == 0.); + CHECK(vec2.mV[VY] == 0.); + CHECK(vecMag == 0.); } } diff --git a/indra/llmath/tests/v3color_test.cpp b/indra/llmath/tests/v3color_test.cpp index 0003c3cd60..7ffa6cb49a 100644 --- a/indra/llmath/tests/v3color_test.cpp +++ b/indra/llmath/tests/v3color_test.cpp @@ -27,80 +27,81 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../v3color.h" -namespace tut -{ namespace { - struct v3color_data - { - }; - typedef test_group v3color_test; - typedef v3color_test::object v3color_object; - tut::v3color_test v3color_testcase("v3color_h"); -} - - template<> template<> - void v3color_object::test<1>() + TEST_CASE("v3color_h: constructors take nothing or components or an array or a hex string", "[v3color_h]") { LLColor3 llcolor3; - ensure("1:LLColor3:Fail to default-initialize ", (0.0f == llcolor3.mV[0]) && (0.0f == llcolor3.mV[1]) && (0.0f == llcolor3.mV[2])); + CHECK(llcolor3.mV[0] == 0.0f); + CHECK(llcolor3.mV[1] == 0.0f); + CHECK(llcolor3.mV[2] == 0.0f); F32 r = 2.0f, g = 3.2f, b = 1.f; F32 v1,v2,v3; LLColor3 llcolor3a(r,g,b); - ensure("2:LLColor3:Fail to initialize " ,(2.0f == llcolor3a.mV[0]) && (3.2f == llcolor3a.mV[1]) && (1.f == llcolor3a.mV[2])); + CHECK(llcolor3a.mV[0] == 2.0f); + CHECK(llcolor3a.mV[1] == 3.2f); + CHECK(llcolor3a.mV[2] == 1.f); const F32 vec[3] = {2.0f, 3.2f,1.f}; LLColor3 llcolor3b(vec); - ensure("3:LLColor3:Fail to initialize " ,(2.0f == llcolor3b.mV[0]) && (3.2f == llcolor3b.mV[1]) && (1.f == llcolor3b.mV[2])); + CHECK(llcolor3b.mV[0] == 2.0f); + CHECK(llcolor3b.mV[1] == 3.2f); + CHECK(llcolor3b.mV[2] == 1.f); const char* str = "561122"; LLColor3 llcolor3c(str); v1 = (F32)86.0f/255.0f; // 0x56 = 86 v2 = (F32)17.0f/255.0f; // 0x11 = 17 v3 = (F32)34.0f/255.f; // 0x22 = 34 - ensure("4:LLColor3:Fail to initialize " , is_approx_equal(v1, llcolor3c.mV[0]) && is_approx_equal(v2, llcolor3c.mV[1]) && is_approx_equal(v3, llcolor3c.mV[2])); + CHECK(is_approx_equal(v1, llcolor3c.mV[0])); + CHECK(is_approx_equal(v2, llcolor3c.mV[1])); + CHECK(is_approx_equal(v3, llcolor3c.mV[2])); } - template<> template<> - void v3color_object::test<2>() + TEST_CASE("v3color_h: setToBlack and setToWhite", "[v3color_h]") { LLColor3 llcolor3; llcolor3.setToBlack(); - ensure("setToBlack:Fail to set black ", ((llcolor3.mV[0] == 0.f) && (llcolor3.mV[1] == 0.f) && (llcolor3.mV[2] == 0.f))); + CHECK(llcolor3.mV[0] == 0.f); + CHECK(llcolor3.mV[1] == 0.f); + CHECK(llcolor3.mV[2] == 0.f); llcolor3.setToWhite(); - ensure("setToWhite:Fail to set white ", ((llcolor3.mV[0] == 1.f) && (llcolor3.mV[1] == 1.f) && (llcolor3.mV[2] == 1.f))); + CHECK(llcolor3.mV[0] == 1.f); + CHECK(llcolor3.mV[1] == 1.f); + CHECK(llcolor3.mV[2] == 1.f); } - template<> template<> - void v3color_object::test<3>() + TEST_CASE("v3color_h: setVec from components or another color or an array", "[v3color_h]") { F32 r = 2.3436212f, g = 1231.f, b = 4.7849321232f; LLColor3 llcolor3, llcolor3a; llcolor3.setVec(r,g,b); - ensure("1:setVec(r,g,b) Fail ",((r == llcolor3.mV[0]) && (g == llcolor3.mV[1]) && (b == llcolor3.mV[2]))); + CHECK(r == llcolor3.mV[0]); + CHECK(g == llcolor3.mV[1]); + CHECK(b == llcolor3.mV[2]); llcolor3a.setVec(llcolor3); - ensure_equals("2:setVec(LLColor3) Fail ", llcolor3,llcolor3a); + CHECK(llcolor3 == llcolor3a); F32 vec[3] = {1.2324f, 2.45634f, .234563f}; llcolor3.setToBlack(); llcolor3.setVec(vec); - ensure("3:setVec(F32*) Fail ",((vec[0] == llcolor3.mV[0]) && (vec[1] == llcolor3.mV[1]) && (vec[2] == llcolor3.mV[2]))); + CHECK(vec[0] == llcolor3.mV[0]); + CHECK(vec[1] == llcolor3.mV[1]); + CHECK(vec[2] == llcolor3.mV[2]); } - template<> template<> - void v3color_object::test<4>() + TEST_CASE("v3color_h: magVecSquared and magVec", "[v3color_h]") { F32 r = 2.3436212f, g = 1231.f, b = 4.7849321232f; LLColor3 llcolor3(r,g,b); - ensure("magVecSquared:Fail ", is_approx_equal(llcolor3.magVecSquared(), (r*r + g*g + b*b))); - ensure("magVec:Fail ", is_approx_equal(llcolor3.magVec(), (F32) sqrt(r*r + g*g + b*b))); + CHECK(is_approx_equal(llcolor3.magVecSquared(), (r*r + g*g + b*b))); + CHECK(is_approx_equal(llcolor3.magVec(), (F32) sqrt(r*r + g*g + b*b))); } - template<> template<> - void v3color_object::test<5>() + TEST_CASE("v3color_h: normVec normalizes and leaves black at zero", "[v3color_h]") { F32 r = 2.3436212f, g = 1231.f, b = 4.7849321232f; F32 val1, val2,val3; @@ -111,15 +112,20 @@ namespace val1 = r * oomag; val2 = g * oomag; val3 = b * oomag; - ensure("1:normVec failed ", (is_approx_equal(val1, llcolor3.mV[0]) && is_approx_equal(val2, llcolor3.mV[1]) && is_approx_equal(val3, llcolor3.mV[2]) && is_approx_equal(vecMag, mag))); + CHECK(is_approx_equal(val1, llcolor3.mV[0])); + CHECK(is_approx_equal(val2, llcolor3.mV[1])); + CHECK(is_approx_equal(val3, llcolor3.mV[2])); + CHECK(is_approx_equal(vecMag, mag)); r = .000000000f, g = 0.f, b = 0.0f; llcolor3.setVec(r,g,b); vecMag = llcolor3.normVec(); - ensure("2:normVec failed should be 0. ", (0. == llcolor3.mV[0] && 0. == llcolor3.mV[1] && 0. == llcolor3.mV[2] && vecMag == 0.)); + CHECK(llcolor3.mV[0] == 0.); + CHECK(llcolor3.mV[1] == 0.); + CHECK(llcolor3.mV[2] == 0.); + CHECK(vecMag == 0.); } - template<> template<> - void v3color_object::test<6>() + TEST_CASE("v3color_h: operator<< prints equal colors the same", "[v3color_h]") { F32 r = 2.3436212f, g = -1231.f, b = .7849321232f; std::ostringstream stream1, stream2; @@ -127,151 +133,165 @@ namespace stream1 << llcolor3; llcolor3a.setVec(r,g,b); stream2 << llcolor3a; - ensure("operator << failed ", (stream1.str() == stream2.str())); + CHECK(stream1.str() == stream2.str()); } - template<> template<> - void v3color_object::test<7>() + TEST_CASE("v3color_h: assignment gives an equal color", "[v3color_h]") { F32 r = 2.3436212f, g = -1231.f, b = .7849321232f; LLColor3 llcolor3(r,g,b),llcolor3a; llcolor3a = llcolor3; - ensure("operator == failed ", (llcolor3a == llcolor3)); + CHECK(llcolor3a == llcolor3); } - template<> template<> - void v3color_object::test<8>() + TEST_CASE("v3color_h: operator+ adds component-wise", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2),llcolor3b; llcolor3b = llcolor3 + llcolor3a ; - ensure("1:operator+ failed",is_approx_equal(r1+r2 ,llcolor3b.mV[0]) && is_approx_equal(g1+g2,llcolor3b.mV[1])&& is_approx_equal(b1+b2,llcolor3b.mV[2])); + CHECK(is_approx_equal(r1+r2 ,llcolor3b.mV[0])); + CHECK(is_approx_equal(g1+g2,llcolor3b.mV[1])); + CHECK(is_approx_equal(b1+b2,llcolor3b.mV[2])); r1 = -.235f, g1 = -24.32f, b1 = 2.13f, r2 = -2.3f, g2 = 1.f, b2 = 34.21f; llcolor3.setVec(r1,g1,b1); llcolor3a.setVec(r2,g2,b2); llcolor3b = llcolor3 + llcolor3a; - ensure("2:operator+ failed",is_approx_equal(r1+r2 ,llcolor3b.mV[0]) && is_approx_equal(g1+g2,llcolor3b.mV[1])&& is_approx_equal(b1+b2,llcolor3b.mV[2])); + CHECK(is_approx_equal(r1+r2 ,llcolor3b.mV[0])); + CHECK(is_approx_equal(g1+g2,llcolor3b.mV[1])); + CHECK(is_approx_equal(b1+b2,llcolor3b.mV[2])); } - template<> template<> - void v3color_object::test<9>() + TEST_CASE("v3color_h: operator- subtracts component-wise", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2),llcolor3b; llcolor3b = llcolor3 - llcolor3a ; - ensure("1:operator- failed",is_approx_equal(r1-r2 ,llcolor3b.mV[0]) && is_approx_equal(g1-g2,llcolor3b.mV[1])&& is_approx_equal(b1-b2,llcolor3b.mV[2])); + CHECK(is_approx_equal(r1-r2 ,llcolor3b.mV[0])); + CHECK(is_approx_equal(g1-g2,llcolor3b.mV[1])); + CHECK(is_approx_equal(b1-b2,llcolor3b.mV[2])); r1 = -.235f, g1 = -24.32f, b1 = 2.13f, r2 = -2.3f, g2 = 1.f, b2 = 34.21f; llcolor3.setVec(r1,g1,b1); llcolor3a.setVec(r2,g2,b2); llcolor3b = llcolor3 - llcolor3a; - ensure("2:operator- failed",is_approx_equal(r1-r2 ,llcolor3b.mV[0]) && is_approx_equal(g1-g2,llcolor3b.mV[1])&& is_approx_equal(b1-b2,llcolor3b.mV[2])); + CHECK(is_approx_equal(r1-r2 ,llcolor3b.mV[0])); + CHECK(is_approx_equal(g1-g2,llcolor3b.mV[1])); + CHECK(is_approx_equal(b1-b2,llcolor3b.mV[2])); } - template<> template<> - void v3color_object::test<10>() + TEST_CASE("v3color_h: operator* by a color and by a scalar on either side", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2),llcolor3b; llcolor3b = llcolor3 * llcolor3a; - ensure("1:operator* failed",is_approx_equal(r1*r2 ,llcolor3b.mV[0]) && is_approx_equal(g1*g2,llcolor3b.mV[1])&& is_approx_equal(b1*b2,llcolor3b.mV[2])); + CHECK(is_approx_equal(r1*r2 ,llcolor3b.mV[0])); + CHECK(is_approx_equal(g1*g2,llcolor3b.mV[1])); + CHECK(is_approx_equal(b1*b2,llcolor3b.mV[2])); llcolor3a.setToBlack(); F32 mulVal = 4.332f; llcolor3a = llcolor3 * mulVal; - ensure("2:operator* failed",is_approx_equal(r1*mulVal ,llcolor3a.mV[0]) && is_approx_equal(g1*mulVal,llcolor3a.mV[1])&& is_approx_equal(b1*mulVal,llcolor3a.mV[2])); + CHECK(is_approx_equal(r1*mulVal ,llcolor3a.mV[0])); + CHECK(is_approx_equal(g1*mulVal,llcolor3a.mV[1])); + CHECK(is_approx_equal(b1*mulVal,llcolor3a.mV[2])); llcolor3a.setToBlack(); llcolor3a = mulVal * llcolor3; - ensure("3:operator* failed",is_approx_equal(r1*mulVal ,llcolor3a.mV[0]) && is_approx_equal(g1*mulVal,llcolor3a.mV[1])&& is_approx_equal(b1*mulVal,llcolor3a.mV[2])); + CHECK(is_approx_equal(r1*mulVal ,llcolor3a.mV[0])); + CHECK(is_approx_equal(g1*mulVal,llcolor3a.mV[1])); + CHECK(is_approx_equal(b1*mulVal,llcolor3a.mV[2])); } - template<> template<> - void v3color_object::test<11>() + TEST_CASE("v3color_h: unary minus negates", "[v3color_h]") { F32 r = 2.3436212f, g = 1231.f, b = 4.7849321232f; LLColor3 llcolor3(r,g,b),llcolor3a; llcolor3a = -llcolor3; - ensure("operator- failed ", (-llcolor3a == llcolor3)); + CHECK(-llcolor3a == llcolor3); } - template<> template<> - void v3color_object::test<12>() + TEST_CASE("v3color_h: operator== on equal colors", "[v3color_h]") { F32 r = 2.3436212f, g = 1231.f, b = 4.7849321232f; LLColor3 llcolor3(r,g,b),llcolor3a(r,g,b); - ensure_equals("1:operator== failed",llcolor3a,llcolor3); + CHECK(llcolor3a == llcolor3); r = 13.3436212f, g = -11.f, b = .7849321232f; llcolor3.setVec(r,g,b); llcolor3a.setVec(r,g,b); - ensure_equals("2:operator== failed",llcolor3a,llcolor3); + CHECK(llcolor3a == llcolor3); } - template<> template<> - void v3color_object::test<13>() + TEST_CASE("v3color_h: operator!= on different and equal colors", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2); - ensure("1:operator!= failed",(llcolor3 != llcolor3a)); + CHECK(llcolor3 != llcolor3a); llcolor3.setToBlack(); llcolor3a.setVec(llcolor3); - ensure("2:operator!= failed", ( false == (llcolor3a != llcolor3))); + CHECK_FALSE(llcolor3a != llcolor3); } - template<> template<> - void v3color_object::test<14>() + TEST_CASE("v3color_h: operator+= adds component-wise", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2); llcolor3a += llcolor3; - ensure("1:operator+= failed",is_approx_equal(r1+r2 ,llcolor3a.mV[0]) && is_approx_equal(g1+g2,llcolor3a.mV[1])&& is_approx_equal(b1+b2,llcolor3a.mV[2])); + CHECK(is_approx_equal(r1+r2 ,llcolor3a.mV[0])); + CHECK(is_approx_equal(g1+g2,llcolor3a.mV[1])); + CHECK(is_approx_equal(b1+b2,llcolor3a.mV[2])); llcolor3.setVec(r1,g1,b1); llcolor3a.setVec(r2,g2,b2); llcolor3a += llcolor3; - ensure("2:operator+= failed",is_approx_equal(r1+r2 ,llcolor3a.mV[0]) && is_approx_equal(g1+g2,llcolor3a.mV[1])&& is_approx_equal(b1+b2,llcolor3a.mV[2])); + CHECK(is_approx_equal(r1+r2 ,llcolor3a.mV[0])); + CHECK(is_approx_equal(g1+g2,llcolor3a.mV[1])); + CHECK(is_approx_equal(b1+b2,llcolor3a.mV[2])); } - template<> template<> - void v3color_object::test<15>() + TEST_CASE("v3color_h: operator-= subtracts component-wise", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2); llcolor3a -= llcolor3; - ensure("1:operator-= failed", is_approx_equal(r2-r1, llcolor3a.mV[0])); - ensure("2:operator-= failed", is_approx_equal(g2-g1, llcolor3a.mV[1])); - ensure("3:operator-= failed", is_approx_equal(b2-b1, llcolor3a.mV[2])); + CHECK(is_approx_equal(r2-r1, llcolor3a.mV[0])); + CHECK(is_approx_equal(g2-g1, llcolor3a.mV[1])); + CHECK(is_approx_equal(b2-b1, llcolor3a.mV[2])); llcolor3.setVec(r1,g1,b1); llcolor3a.setVec(r2,g2,b2); llcolor3a -= llcolor3; - ensure("4:operator-= failed", is_approx_equal(r2-r1, llcolor3a.mV[0])); - ensure("5:operator-= failed", is_approx_equal(g2-g1, llcolor3a.mV[1])); - ensure("6:operator-= failed", is_approx_equal(b2-b1, llcolor3a.mV[2])); + CHECK(is_approx_equal(r2-r1, llcolor3a.mV[0])); + CHECK(is_approx_equal(g2-g1, llcolor3a.mV[1])); + CHECK(is_approx_equal(b2-b1, llcolor3a.mV[2])); } - template<> template<> - void v3color_object::test<16>() + TEST_CASE("v3color_h: operator*= by a color and by a scalar", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2); llcolor3a *= llcolor3; - ensure("1:operator*= failed",is_approx_equal(r1*r2 ,llcolor3a.mV[0]) && is_approx_equal(g1*g2,llcolor3a.mV[1])&& is_approx_equal(b1*b2,llcolor3a.mV[2])); + CHECK(is_approx_equal(r1*r2 ,llcolor3a.mV[0])); + CHECK(is_approx_equal(g1*g2,llcolor3a.mV[1])); + CHECK(is_approx_equal(b1*b2,llcolor3a.mV[2])); F32 mulVal = 4.332f; llcolor3 *=mulVal; - ensure("2:operator*= failed",is_approx_equal(r1*mulVal ,llcolor3.mV[0]) && is_approx_equal(g1*mulVal,llcolor3.mV[1])&& is_approx_equal(b1*mulVal,llcolor3.mV[2])); + CHECK(is_approx_equal(r1*mulVal ,llcolor3.mV[0])); + CHECK(is_approx_equal(g1*mulVal,llcolor3.mV[1])); + CHECK(is_approx_equal(b1*mulVal,llcolor3.mV[2])); } - template<> template<> - void v3color_object::test<17>() + TEST_CASE("v3color_h: clamp limits components to the 0 to 1 range", "[v3color_h]") { F32 r = 2.3436212f, g = -1231.f, b = .7849321232f; LLColor3 llcolor3(r,g,b); llcolor3.clamp(); - ensure("1:clamp:Fail to clamp " ,(1.0f == llcolor3.mV[0]) && (0.f == llcolor3.mV[1]) && (b == llcolor3.mV[2])); + CHECK(llcolor3.mV[0] == 1.0f); + CHECK(llcolor3.mV[1] == 0.f); + CHECK(b == llcolor3.mV[2]); r = -2.3436212f, g = -1231.f, b = 67.7849321232f; llcolor3.setVec(r,g,b); llcolor3.clamp(); - ensure("2:clamp:Fail to clamp " ,(0.f == llcolor3.mV[0]) && (0.f == llcolor3.mV[1]) && (1.f == llcolor3.mV[2])); + CHECK(llcolor3.mV[0] == 0.f); + CHECK(llcolor3.mV[1] == 0.f); + CHECK(llcolor3.mV[2] == 1.f); } - template<> template<> - void v3color_object::test<18>() + TEST_CASE("v3color_h: lerp interpolates every component", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; F32 val = 2.3f,val1,val2,val3; @@ -280,23 +300,23 @@ namespace val2 = g1 + (g2 - g1)* val; val3 = b1 + (b2 - b1)* val; LLColor3 llcolor3b = lerp(llcolor3,llcolor3a,val); - ensure("lerp failed ", ((val1 ==llcolor3b.mV[0])&& (val2 ==llcolor3b.mV[1]) && (val3 ==llcolor3b.mV[2]))); + CHECK(val1 ==llcolor3b.mV[0]); + CHECK(val2 ==llcolor3b.mV[1]); + CHECK(val3 ==llcolor3b.mV[2]); } - template<> template<> - void v3color_object::test<19>() + TEST_CASE("v3color_h: distVec and distVec_squared", "[v3color_h]") { F32 r1 =1.f, g1 = 2.f,b1 = 1.2f, r2 = -2.3f, g2 = 1.11f, b2 = 1234.234f; LLColor3 llcolor3(r1,g1,b1),llcolor3a(r2,g2,b2); F32 val = distVec(llcolor3,llcolor3a); - ensure("distVec failed ", is_approx_equal((F32) sqrt((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2)) ,val)); + CHECK(is_approx_equal((F32) sqrt((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2)) ,val)); F32 val1 = distVec_squared(llcolor3,llcolor3a); - ensure("distVec_squared failed ", is_approx_equal(((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2)) ,val1)); + CHECK(is_approx_equal(((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2)) ,val1)); } - template<> template<> - void v3color_object::test<20>() + TEST_CASE("v3color_h: getValue round-trips through setValue and the LLSD constructor", "[v3color_h]") { F32 r1 = 1.02223f, g1 = 22222.212f, b1 = 122222.00002f; LLColor3 llcolor31(r1,g1,b1); @@ -304,9 +324,9 @@ namespace LLSD sd = llcolor31.getValue(); LLColor3 llcolor32; llcolor32.setValue(sd); - ensure_equals("LLColor3::setValue/getValue failed", llcolor31, llcolor32); + CHECK(llcolor31 == llcolor32); LLColor3 llcolor33(sd); - ensure_equals("LLColor3(LLSD) failed", llcolor31, llcolor33); + CHECK(llcolor31 == llcolor33); } } diff --git a/indra/llmath/tests/v3dmath_test.cpp b/indra/llmath/tests/v3dmath_test.cpp index 10ee4c3523..3f660402ec 100644 --- a/indra/llmath/tests/v3dmath_test.cpp +++ b/indra/llmath/tests/v3dmath_test.cpp @@ -28,7 +28,8 @@ #include "linden_common.h" #include "llsd.h" -#include "../test/lltut.h" +#include +#include "altestmatchers.h" #include "../m3math.h" #include "../v4math.h" @@ -36,47 +37,43 @@ #include "../v3dmath.h" #include "../llquaternion.h" -namespace tut -{ namespace { - struct v3dmath_data - { - }; - typedef test_group v3dmath_test; - typedef v3dmath_test::object v3dmath_object; - tut::v3dmath_test v3dmath_testcase("v3dmath_h"); -} - - template<> template<> - void v3dmath_object::test<1>() + TEST_CASE("v3dmath_h: constructors set every component", "[v3dmath_h]") { LLVector3d vec3D; - ensure("1:LLVector3d:Fail to initialize ", ((0 == vec3D.mdV[VX]) && (0 == vec3D.mdV[VY]) && (0 == vec3D.mdV[VZ]))); + CHECK(vec3D.mdV[VX] == 0); + CHECK(vec3D.mdV[VY] == 0); + CHECK(vec3D.mdV[VZ] == 0); + F64 x = 2.32f, y = 1.212f, z = -.12f; LLVector3d vec3Da(x,y,z); - ensure("2:LLVector3d:Fail to initialize ", ((2.32f == vec3Da.mdV[VX]) && (1.212f == vec3Da.mdV[VY]) && (-.12f == vec3Da.mdV[VZ]))); + CHECK(vec3Da.mdV[VX] == 2.32f); + CHECK(vec3Da.mdV[VY] == 1.212f); + CHECK(vec3Da.mdV[VZ] == -.12f); + const F64 vec[3] = {1.2f ,3.2f, -4.2f}; LLVector3d vec3Db(vec); - ensure("3:LLVector3d:Fail to initialize ", ((1.2f == vec3Db.mdV[VX]) && (3.2f == vec3Db.mdV[VY]) && (-4.2f == vec3Db.mdV[VZ]))); + CHECK(vec3Db.mdV[VX] == 1.2f); + CHECK(vec3Db.mdV[VY] == 3.2f); + CHECK(vec3Db.mdV[VZ] == -4.2f); + LLVector3 vec3((F32)x,(F32)y,(F32)z); LLVector3d vec3Dc(vec3); - ensure_equals("4:LLVector3d Fail to initialize",vec3Da,vec3Dc); + CHECK(vec3Da == vec3Dc); } - template<> template<> - void v3dmath_object::test<2>() + TEST_CASE("v3dmath_h: LLSD constructor and getValue round trip", "[v3dmath_h]") { S32 a = -235; LLSD llsd(a); LLVector3d vec3d(llsd); LLSD sd = vec3d.getValue(); LLVector3d vec3da(sd); - ensure("1:getValue:Fail ", (vec3d == vec3da)); + CHECK(vec3d == vec3da); } - template<> template<> - void v3dmath_object::test<3>() + TEST_CASE("v3dmath_h: setValue and getValue round trip", "[v3dmath_h]") { F64 a = 232345521.411132; LLSD llsd(a); @@ -84,11 +81,10 @@ namespace vec3d.setValue(llsd); LLSD sd = vec3d.getValue(); LLVector3d vec3da(sd); - ensure("1:setValue:Fail to initialize ", (vec3d == vec3da)); + CHECK(vec3d == vec3da); } - template<> template<> - void v3dmath_object::test<4>() + TEST_CASE("v3dmath_h: cast from an LLSD array copies each element", "[v3dmath_h]") { F64 a[3] = {222231.43222, 12345.2343, -434343.33222}; LLSD llsd; @@ -97,183 +93,171 @@ namespace llsd[2] = a[2]; LLVector3d vec3D; vec3D = (LLVector3d)llsd; - ensure("1:operator=:Fail to initialize ", ((llsd[0].asReal()== vec3D.mdV[VX]) && (llsd[1].asReal() == vec3D.mdV[VY]) && (llsd[2].asReal() == vec3D.mdV[VZ]))); + CHECK(llsd[0].asReal()== vec3D.mdV[VX]); + CHECK(llsd[1].asReal() == vec3D.mdV[VY]); + CHECK(llsd[2].asReal() == vec3D.mdV[VZ]); } - template<> template<> - void v3dmath_object::test<5>() + TEST_CASE("v3dmath_h: clearVec/zeroVec and the setVec overloads", "[v3dmath_h]") { F64 x = 2.32f, y = 1.212f, z = -.12f; LLVector3d vec3D(x,y,z); vec3D.clearVec(); - ensure("1:clearVec:Fail to initialize ", ((0 == vec3D.mdV[VX]) && (0 == vec3D.mdV[VY]) && (0 == vec3D.mdV[VZ]))); + CHECK(vec3D.mdV[VX] == 0); + CHECK(vec3D.mdV[VY] == 0); + CHECK(vec3D.mdV[VZ] == 0); + vec3D.setVec(x,y,z); - ensure("2:setVec:Fail to initialize ", ((x == vec3D.mdV[VX]) && (y == vec3D.mdV[VY]) && (z == vec3D.mdV[VZ]))); + CHECK(x == vec3D.mdV[VX]); + CHECK(y == vec3D.mdV[VY]); + CHECK(z == vec3D.mdV[VZ]); + vec3D.zeroVec(); - ensure("3:zeroVec:Fail to initialize ", ((0 == vec3D.mdV[VX]) && (0 == vec3D.mdV[VY]) && (0 == vec3D.mdV[VZ]))); + CHECK(vec3D.mdV[VX] == 0); + CHECK(vec3D.mdV[VY] == 0); + CHECK(vec3D.mdV[VZ] == 0); + vec3D.clearVec(); LLVector3 vec3((F32)x,(F32)y,(F32)z); vec3D.setVec(vec3); - ensure("4:setVec:Fail to initialize ", ((x == vec3D.mdV[VX]) && (y == vec3D.mdV[VY]) && (z == vec3D.mdV[VZ]))); + CHECK(x == vec3D.mdV[VX]); + CHECK(y == vec3D.mdV[VY]); + CHECK(z == vec3D.mdV[VZ]); + vec3D.clearVec(); const F64 vec[3] = {x,y,z}; vec3D.setVec(vec); - ensure("5:setVec:Fail to initialize ", ((x == vec3D.mdV[VX]) && (y == vec3D.mdV[VY]) && (z == vec3D.mdV[VZ]))); + CHECK(x == vec3D.mdV[VX]); + CHECK(y == vec3D.mdV[VY]); + CHECK(z == vec3D.mdV[VZ]); + LLVector3d vec3Da; vec3Da.setVec(vec3D); - ensure_equals("6:setVec: Fail to initialize", vec3D, vec3Da); + CHECK(vec3D == vec3Da); } - template<> template<> - void v3dmath_object::test<6>() + TEST_CASE("v3dmath_h: abs then isNull and isExactlyZero", "[v3dmath_h]") { F64 x = -2.32, y = 1.212, z = -.12; LLVector3d vec3D(x,y,z); vec3D.abs(); - ensure("1:abs:Fail ", ((-x == vec3D.mdV[VX]) && (y == vec3D.mdV[VY]) && (-z == vec3D.mdV[VZ]))); - ensure("2:isNull():Fail ", (false == vec3D.isNull())); + CHECK(-x == vec3D.mdV[VX]); + CHECK(y == vec3D.mdV[VY]); + CHECK(-z == vec3D.mdV[VZ]); + + CHECK_FALSE(vec3D.isNull()); vec3D.clearVec(); x =.00000001, y = .000001001, z = .000001001; vec3D.setVec(x,y,z); - ensure("3:isNull():Fail ", (true == vec3D.isNull())); - ensure("4:isExactlyZero():Fail ", (false == vec3D.isExactlyZero())); + CHECK(vec3D.isNull()); + CHECK_FALSE(vec3D.isExactlyZero()); x =.0000000, y = .00000000, z = .00000000; vec3D.setVec(x,y,z); - ensure("5:isExactlyZero():Fail ", (true == vec3D.isExactlyZero())); + CHECK(vec3D.isExactlyZero()); } - template<> template<> - void v3dmath_object::test<7>() + TEST_CASE("v3dmath_h: subscript operator reads components and returns references", "[v3dmath_h]") { F64 x = -2.32, y = 1.212, z = -.12; LLVector3d vec3D(x,y,z); - ensure("1:operator [] failed",( x == vec3D[0])); - ensure("2:operator [] failed",( y == vec3D[1])); - ensure("3:operator [] failed",( z == vec3D[2])); + CHECK(x == vec3D[0]); + CHECK(y == vec3D[1]); + CHECK(z == vec3D[2]); vec3D.clearVec(); x = 23.23, y = -.2361, z = 3.25; vec3D.setVec(x,y,z); F64 &ref1 = vec3D[0]; - ensure("4:operator [] failed",( ref1 == vec3D[0])); + CHECK(ref1 == vec3D[0]); F64 &ref2 = vec3D[1]; - ensure("5:operator [] failed",( ref2 == vec3D[1])); + CHECK(ref2 == vec3D[1]); F64 &ref3 = vec3D[2]; - ensure("6:operator [] failed",( ref3 == vec3D[2])); + CHECK(ref3 == vec3D[2]); } - template<> template<> - void v3dmath_object::test<8>() + TEST_CASE("v3dmath_h: assignment from LLVector4", "[v3dmath_h]") { F32 x = 1.f, y = 2.f, z = -1.f; LLVector4 vec4(x,y,z); LLVector3d vec3D; vec3D = vec4; - ensure("1:operator=:Fail to initialize ", ((vec4.mV[VX] == vec3D.mdV[VX]) && (vec4.mV[VY] == vec3D.mdV[VY]) && (vec4.mV[VZ] == vec3D.mdV[VZ]))); + CHECK(vec4.mV[VX] == vec3D.mdV[VX]); + CHECK(vec4.mV[VY] == vec3D.mdV[VY]); + CHECK(vec4.mV[VZ] == vec3D.mdV[VZ]); } - template<> template<> - void v3dmath_object::test<9>() + TEST_CASE("v3dmath_h: operator+ and operator+=", "[v3dmath_h]") { F64 x1 = 1.78787878, y1 = 232322.2121, z1 = -12121.121212; F64 x2 = 1.2, y2 = 2.5, z2 = 1.; LLVector3d vec3D(x1,y1,z1),vec3Da(x2,y2,z2),vec3Db; vec3Db = vec3Da+ vec3D; - ensure("1:operator+:Fail to initialize ", ((x1+x2 == vec3Db.mdV[VX]) && (y1+y2 == vec3Db.mdV[VY]) && (z1+z2 == vec3Db.mdV[VZ]))); + CHECK(x1+x2 == vec3Db.mdV[VX]); + CHECK(y1+y2 == vec3Db.mdV[VY]); + CHECK(z1+z2 == vec3Db.mdV[VZ]); + x1 = -2.45, y1 = 2.1, z1 = 3.0; vec3D.clearVec(); vec3Da.clearVec(); vec3D.setVec(x1,y1,z1); vec3Da += vec3D; - ensure_equals("2:operator+=: Fail to initialize", vec3Da,vec3D); + CHECK(vec3Da == vec3D); vec3Da += vec3D; - ensure("3:operator+=:Fail to initialize ", ((2*x1 == vec3Da.mdV[VX]) && (2*y1 == vec3Da.mdV[VY]) && (2*z1 == vec3Da.mdV[VZ]))); + CHECK(2*x1 == vec3Da.mdV[VX]); + CHECK(2*y1 == vec3Da.mdV[VY]); + CHECK(2*z1 == vec3Da.mdV[VZ]); } - template<> template<> - void v3dmath_object::test<10>() + TEST_CASE("v3dmath_h: operator- and operator-=", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1; F64 x2 = 1.2, y2 = 2.5, z2 = 1.; LLVector3d vec3D(x1,y1,z1),vec3Da(x2,y2,z2),vec3Db; vec3Db = vec3Da - vec3D; - ensure("1:operator-:Fail to initialize ", ((x2-x1 == vec3Db.mdV[VX]) && (y2-y1 == vec3Db.mdV[VY]) && (z2-z1 == vec3Db.mdV[VZ]))); + CHECK(x2-x1 == vec3Db.mdV[VX]); + CHECK(y2-y1 == vec3Db.mdV[VY]); + CHECK(z2-z1 == vec3Db.mdV[VZ]); + x1 = -2.45, y1 = 2.1, z1 = 3.0; vec3D.clearVec(); vec3Da.clearVec(); vec3D.setVec(x1,y1,z1); vec3Da -=vec3D; - ensure("2:operator-=:Fail to initialize ", ((2.45 == vec3Da.mdV[VX]) && (-2.1 == vec3Da.mdV[VY]) && (-3.0 == vec3Da.mdV[VZ]))); + CHECK(vec3Da.mdV[VX] == 2.45); + CHECK(vec3Da.mdV[VY] == -2.1); + CHECK(vec3Da.mdV[VZ] == -3.0); + vec3Da -= vec3D; - ensure("3:operator-=:Fail to initialize ", ((-2*x1 == vec3Da.mdV[VX]) && (-2*y1 == vec3Da.mdV[VY]) && (-2*z1 == vec3Da.mdV[VZ]))); + CHECK(-2*x1 == vec3Da.mdV[VX]); + CHECK(-2*y1 == vec3Da.mdV[VY]); + CHECK(-2*z1 == vec3Da.mdV[VZ]); } - template<> template<> - void v3dmath_object::test<11>() + + TEST_CASE("v3dmath_h: operator* dot and scalar products and operator*=", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1; F64 x2 = 1.2, y2 = 2.5, z2 = 1.; LLVector3d vec3D(x1,y1,z1),vec3Da(x2,y2,z2); F64 res = vec3D * vec3Da; - ensure_approximately_equals( - "1:operator* failed", - res, - (x1*x2 + y1*y2 + z1*z2), - 8); + CHECK_THAT(res, ll_test::WithinBits(x1*x2 + y1*y2 + z1*z2, 8)); vec3Da.clearVec(); F64 mulVal = 4.2; vec3Da = vec3D * mulVal; - ensure_approximately_equals( - "2a:operator* failed", - vec3Da.mdV[VX], - x1*mulVal, - 8); - ensure_approximately_equals( - "2b:operator* failed", - vec3Da.mdV[VY], - y1*mulVal, - 8); - ensure_approximately_equals( - "2c:operator* failed", - vec3Da.mdV[VZ], - z1*mulVal, - 8); + CHECK_THAT(vec3Da.mdV[VX], ll_test::WithinBits(x1*mulVal, 8)); + CHECK_THAT(vec3Da.mdV[VY], ll_test::WithinBits(y1*mulVal, 8)); + CHECK_THAT(vec3Da.mdV[VZ], ll_test::WithinBits(z1*mulVal, 8)); vec3Da.clearVec(); vec3Da = mulVal * vec3D; - ensure_approximately_equals( - "3a:operator* failed", - vec3Da.mdV[VX], - x1*mulVal, - 8); - ensure_approximately_equals( - "3b:operator* failed", - vec3Da.mdV[VY], - y1*mulVal, - 8); - ensure_approximately_equals( - "3c:operator* failed", - vec3Da.mdV[VZ], - z1*mulVal, - 8); + CHECK_THAT(vec3Da.mdV[VX], ll_test::WithinBits(x1*mulVal, 8)); + CHECK_THAT(vec3Da.mdV[VY], ll_test::WithinBits(y1*mulVal, 8)); + CHECK_THAT(vec3Da.mdV[VZ], ll_test::WithinBits(z1*mulVal, 8)); vec3D *= mulVal; - ensure_approximately_equals( - "4a:operator*= failed", - vec3D.mdV[VX], - x1*mulVal, - 8); - ensure_approximately_equals( - "4b:operator*= failed", - vec3D.mdV[VY], - y1*mulVal, - 8); - ensure_approximately_equals( - "4c:operator*= failed", - vec3D.mdV[VZ], - z1*mulVal, - 8); + CHECK_THAT(vec3D.mdV[VX], ll_test::WithinBits(x1*mulVal, 8)); + CHECK_THAT(vec3D.mdV[VY], ll_test::WithinBits(y1*mulVal, 8)); + CHECK_THAT(vec3D.mdV[VZ], ll_test::WithinBits(z1*mulVal, 8)); } - template<> template<> - void v3dmath_object::test<12>() + TEST_CASE("v3dmath_h: operator% and operator%= cross products", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1; F64 x2 = 1.2, y2 = 2.5, z2 = 1.; @@ -283,93 +267,56 @@ namespace val1 = y1*z2 - y2*z1; val2 = z1*x2 -z2*x1; val3 = x1*y2-x2*y1; - ensure("1:operator% failed",(val1 == vec3Db.mdV[VX]) && (val2 == vec3Db.mdV[VY]) && (val3 == vec3Db.mdV[VZ])); + CHECK(val1 == vec3Db.mdV[VX]); + CHECK(val2 == vec3Db.mdV[VY]); + CHECK(val3 == vec3Db.mdV[VZ]); + vec3D %= vec3Da; - ensure("2:operator%= failed", - is_approx_equal(vec3D.mdV[VX],vec3Db.mdV[VX]) && - is_approx_equal(vec3D.mdV[VY],vec3Db.mdV[VY]) && - is_approx_equal(vec3D.mdV[VZ],vec3Db.mdV[VZ]) ); + CHECK(is_approx_equal(vec3D.mdV[VX],vec3Db.mdV[VX])); + CHECK(is_approx_equal(vec3D.mdV[VY],vec3Db.mdV[VY])); + CHECK(is_approx_equal(vec3D.mdV[VZ],vec3Db.mdV[VZ])); } - template<> template<> - void v3dmath_object::test<13>() + TEST_CASE("v3dmath_h: operator/ and operator/= divide by a scalar", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1,div = 4.2; F64 t = 1.f / div; LLVector3d vec3D(x1,y1,z1), vec3Da; vec3Da = vec3D/div; - ensure_approximately_equals( - "1a:operator/ failed", - vec3Da.mdV[VX], - x1*t, - 8); - ensure_approximately_equals( - "1b:operator/ failed", - vec3Da.mdV[VY], - y1*t, - 8); - ensure_approximately_equals( - "1c:operator/ failed", - vec3Da.mdV[VZ], - z1*t, - 8); + CHECK_THAT(vec3Da.mdV[VX], ll_test::WithinBits(x1*t, 8)); + CHECK_THAT(vec3Da.mdV[VY], ll_test::WithinBits(y1*t, 8)); + CHECK_THAT(vec3Da.mdV[VZ], ll_test::WithinBits(z1*t, 8)); x1 = 1.23, y1 = 4., z1 = -2.32; vec3D.clearVec(); vec3Da.clearVec(); vec3D.setVec(x1,y1,z1); vec3Da = vec3D/div; - ensure_approximately_equals( - "2a:operator/ failed", - vec3Da.mdV[VX], - x1*t, - 8); - ensure_approximately_equals( - "2b:operator/ failed", - vec3Da.mdV[VY], - y1*t, - 8); - ensure_approximately_equals( - "2c:operator/ failed", - vec3Da.mdV[VZ], - z1*t, - 8); + CHECK_THAT(vec3Da.mdV[VX], ll_test::WithinBits(x1*t, 8)); + CHECK_THAT(vec3Da.mdV[VY], ll_test::WithinBits(y1*t, 8)); + CHECK_THAT(vec3Da.mdV[VZ], ll_test::WithinBits(z1*t, 8)); vec3D /= div; - ensure_approximately_equals( - "3a:operator/= failed", - vec3D.mdV[VX], - x1*t, - 8); - ensure_approximately_equals( - "3b:operator/= failed", - vec3D.mdV[VY], - y1*t, - 8); - ensure_approximately_equals( - "3c:operator/= failed", - vec3D.mdV[VZ], - z1*t, - 8); + CHECK_THAT(vec3D.mdV[VX], ll_test::WithinBits(x1*t, 8)); + CHECK_THAT(vec3D.mdV[VY], ll_test::WithinBits(y1*t, 8)); + CHECK_THAT(vec3D.mdV[VZ], ll_test::WithinBits(z1*t, 8)); } - template<> template<> - void v3dmath_object::test<14>() + TEST_CASE("v3dmath_h: operator== and operator!=", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1; LLVector3d vec3D(x1,y1,z1), vec3Da; - ensure("1:operator!= failed",(true == (vec3D !=vec3Da))); + CHECK(vec3D !=vec3Da); vec3Da = vec3D; - ensure("2:operator== failed",(vec3D ==vec3Da)); + CHECK(vec3D ==vec3Da); vec3D.clearVec(); vec3Da.clearVec(); x1 = .211, y1 = 21.111, z1 = 23.22; vec3D.setVec(x1,y1,z1); vec3Da.setVec(x1,y1,z1); - ensure("3:operator== failed",(vec3D ==vec3Da)); - ensure("4:operator!= failed",(false == (vec3D !=vec3Da))); + CHECK(vec3D ==vec3Da); + CHECK_FALSE(vec3D !=vec3Da); } - template<> template<> - void v3dmath_object::test<15>() + TEST_CASE("v3dmath_h: operator<< prints equal vectors alike", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1; LLVector3d vec3D(x1,y1,z1), vec3Da; @@ -377,84 +324,58 @@ namespace stream1 << vec3D; vec3Da.setVec(x1,y1,z1); stream2 << vec3Da; - ensure("1:operator << failed",(stream1.str() == stream2.str())); + CHECK(stream1.str() == stream2.str()); } - template<> template<> - void v3dmath_object::test<16>() + TEST_CASE("v3dmath_h: parseVector3d reads three numbers", "[v3dmath_h]") { F64 x1 = 1.23, y1 = 2.0, z1 = 4.; std::string buf("1.23 2. 4"); LLVector3d vec3D, vec3Da(x1,y1,z1); LLVector3d::parseVector3d(buf, &vec3D); - ensure_equals("1:parseVector3d: failed " , vec3D, vec3Da); + CHECK(vec3D == vec3Da); } - template<> template<> - void v3dmath_object::test<17>() + TEST_CASE("v3dmath_h: unary operator- negates", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1; LLVector3d vec3D(x1,y1,z1), vec3Da; vec3Da = -vec3D; - ensure("1:operator- failed", (vec3D == - vec3Da)); + CHECK(vec3D == - vec3Da); } - template<> template<> - void v3dmath_object::test<18>() + TEST_CASE("v3dmath_h: magVecSquared and magVec", "[v3dmath_h]") { F64 x = 1., y = 2., z = -1.1; LLVector3d vec3D(x,y,z); F64 res = (x*x + y*y + z*z) - vec3D.magVecSquared(); - ensure("1:magVecSquared:Fail ", ((-F_APPROXIMATELY_ZERO <= res)&& (res <=F_APPROXIMATELY_ZERO))); + CHECK(-F_APPROXIMATELY_ZERO <= res); + CHECK(res <=F_APPROXIMATELY_ZERO); + res = (F32) sqrt(x*x + y*y + z*z) - vec3D.magVec(); - ensure("2:magVec: Fail ", ((-F_APPROXIMATELY_ZERO <= res)&& (res <=F_APPROXIMATELY_ZERO))); + CHECK(-F_APPROXIMATELY_ZERO <= res); + CHECK(res <=F_APPROXIMATELY_ZERO); } - template<> template<> - void v3dmath_object::test<19>() + TEST_CASE("v3dmath_h: normVec normalises long and tiny vectors", "[v3dmath_h]") { F64 x = 1., y = 2., z = -1.1; LLVector3d vec3D(x,y,z); F64 mag = vec3D.normVec(); mag = 1.f/ mag; - ensure_approximately_equals( - "1a:normVec: Fail ", - vec3D.mdV[VX], - x * mag, - 8); - ensure_approximately_equals( - "1b:normVec: Fail ", - vec3D.mdV[VY], - y * mag, - 8); - ensure_approximately_equals( - "1c:normVec: Fail ", - vec3D.mdV[VZ], - z * mag, - 8); + CHECK_THAT(vec3D.mdV[VX], ll_test::WithinBits(x * mag, 8)); + CHECK_THAT(vec3D.mdV[VY], ll_test::WithinBits(y * mag, 8)); + CHECK_THAT(vec3D.mdV[VZ], ll_test::WithinBits(z * mag, 8)); x = 0.000000001, y = 0.000000001, z = 0.000000001; vec3D.clearVec(); vec3D.setVec(x,y,z); mag = vec3D.normVec(); - ensure_approximately_equals( - "2a:normVec: Fail ", - vec3D.mdV[VX], - x * mag, - 8); - ensure_approximately_equals( - "2b:normVec: Fail ", - vec3D.mdV[VY], - y * mag, - 8); - ensure_approximately_equals( - "2c:normVec: Fail ", - vec3D.mdV[VZ], - z * mag, - 8); + CHECK_THAT(vec3D.mdV[VX], ll_test::WithinBits(x * mag, 8)); + CHECK_THAT(vec3D.mdV[VY], ll_test::WithinBits(y * mag, 8)); + CHECK_THAT(vec3D.mdV[VZ], ll_test::WithinBits(z * mag, 8)); } - template<> template<> - void v3dmath_object::test<20>() + TEST_CASE("v3dmath_h: parseVector3d reads large values exactly", "[v3dmath_h]") { F64 x1 = 1111.232222; F64 y1 = 2222222222.22; @@ -462,11 +383,10 @@ namespace std::string buf("1111.232222 2222222222.22 422222222222"); LLVector3d vec3Da, vec3Db(x1,y1,z1); LLVector3d::parseVector3d(buf, &vec3Da); - ensure_equals("1:parseVector3 failed", vec3Da, vec3Db); + CHECK(vec3Da == vec3Db); } - template<> template<> - void v3dmath_object::test<21>() + TEST_CASE("v3dmath_h: lerp between two vectors", "[v3dmath_h]") { F64 x1 = 1., y1 = 2., z1 = -1.1; F64 x2 = 1.2, y2 = 2.5, z2 = 1.; @@ -476,42 +396,41 @@ namespace val3 = z1 + (z2 - z1)* val; LLVector3d vec3Da(x1,y1,z1),vec3Db(x2,y2,z2); LLVector3d vec3d = lerp(vec3Da,vec3Db,val); - ensure("1:lerp failed", ((val1 ==vec3d.mdV[VX])&& (val2 ==vec3d.mdV[VY]) && (val3 ==vec3d.mdV[VZ]))); + CHECK(val1 ==vec3d.mdV[VX]); + CHECK(val2 ==vec3d.mdV[VY]); + CHECK(val3 ==vec3d.mdV[VZ]); } - template<> template<> - void v3dmath_object::test<22>() + TEST_CASE("v3dmath_h: clamp reports a component out of range", "[v3dmath_h]") { F64 x = 2.32, y = 1.212, z = -.12; F64 min = 0.0001, max = 3.0; LLVector3d vec3d(x,y,z); - ensure("1:clamp:Fail ", (true == (vec3d.clamp(min, max)))); + CHECK(vec3d.clamp(min, max)); x = 0.000001f, z = 5.3f; vec3d.setVec(x,y,z); - ensure("2:clamp:Fail ", (true == (vec3d.clamp(min, max)))); + CHECK(vec3d.clamp(min, max)); } - template<> template<> - void v3dmath_object::test<23>() + TEST_CASE("v3dmath_h: are_parallel for equal and non-parallel vectors", "[v3dmath_h]") { F64 x = 10., y = 20., z = -15.; F64 epsilon = .23425; LLVector3d vec3Da(x,y,z), vec3Db(x,y,z); - ensure("1:are_parallel: Fail ", (true == are_parallel(vec3Da,vec3Db,epsilon))); + CHECK(are_parallel(vec3Da,vec3Db,epsilon)); F64 x1 = -12., y1 = -20., z1 = -100.; vec3Db.clearVec(); vec3Db.setVec(x1,y1,z1); - ensure("2:are_parallel: Fail ", (false == are_parallel(vec3Da,vec3Db,epsilon))); + CHECK_FALSE(are_parallel(vec3Da,vec3Db,epsilon)); } - template<> template<> - void v3dmath_object::test<24>() + TEST_CASE("v3dmath_h: angle_between is zero for equal vectors and matches acos", "[v3dmath_h]") { F64 x = 10., y = 20., z = -15.; F64 angle1, angle2; LLVector3d vec3Da(x,y,z), vec3Db(x,y,z); angle1 = angle_between(vec3Da, vec3Db); - ensure("1:angle_between: Fail ", (0 == angle1)); + CHECK(angle1 == 0); F64 x1 = -1., y1 = -20., z1 = -1.; vec3Da.clearVec(); vec3Da.setVec(x1,y1,z1); @@ -520,6 +439,6 @@ namespace vec3Da.normVec(); F64 angle = vec3Db*vec3Da; angle = acos(angle); - ensure("2:angle_between: Fail ", (angle == angle2)); + CHECK(angle == angle2); } } diff --git a/indra/llmath/tests/v3math_test.cpp b/indra/llmath/tests/v3math_test.cpp index 805d692114..18ded63c88 100644 --- a/indra/llmath/tests/v3math_test.cpp +++ b/indra/llmath/tests/v3math_test.cpp @@ -27,7 +27,7 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "llsd.h" #include "../v3dmath.h" @@ -38,151 +38,162 @@ #include "../llquantize.h" -namespace tut -{ namespace { - struct v3math_data - { - }; - typedef test_group v3math_test; - typedef v3math_test::object v3math_object; - tut::v3math_test v3math_testcase("v3math_h"); -} - - template<> template<> - void v3math_object::test<1>() + TEST_CASE("v3math_h: constructors set every component", "[v3math_h]") { LLVector3 vec3; - ensure("1:LLVector3:Fail to initialize ", ((0.f == vec3.mV[VX]) && (0.f == vec3.mV[VY]) && (0.f == vec3.mV[VZ]))); + CHECK(vec3.mV[VX] == 0.f); + CHECK(vec3.mV[VY] == 0.f); + CHECK(vec3.mV[VZ] == 0.f); + F32 x = 2.32f, y = 1.212f, z = -.12f; LLVector3 vec3a(x,y,z); - ensure("2:LLVector3:Fail to initialize ", ((2.32f == vec3a.mV[VX]) && (1.212f == vec3a.mV[VY]) && (-.12f == vec3a.mV[VZ]))); + CHECK(vec3a.mV[VX] == 2.32f); + CHECK(vec3a.mV[VY] == 1.212f); + CHECK(vec3a.mV[VZ] == -.12f); + const F32 vec[3] = {1.2f ,3.2f, -4.2f}; LLVector3 vec3b(vec); - ensure("3:LLVector3:Fail to initialize ", ((1.2f == vec3b.mV[VX]) && (3.2f == vec3b.mV[VY]) && (-4.2f == vec3b.mV[VZ]))); + CHECK(vec3b.mV[VX] == 1.2f); + CHECK(vec3b.mV[VY] == 3.2f); + CHECK(vec3b.mV[VZ] == -4.2f); } - template<> template<> - void v3math_object::test<2>() + TEST_CASE("v3math_h: conversion from LLVector3d and LLVector4", "[v3math_h]") { F32 x = 2.32f, y = 1.212f, z = -.12f; LLVector3 vec3(x,y,z); LLVector3d vector3d(vec3); LLVector3 vec3a(vector3d); - ensure("1:LLVector3:Fail to initialize ", vec3 == vec3a); + CHECK(vec3 == vec3a); LLVector4 vector4(vec3); LLVector3 vec3b(vector4); - ensure("2:LLVector3:Fail to initialize ", vec3 == vec3b); + CHECK(vec3 == vec3b); } - template<> template<> - void v3math_object::test<3>() + TEST_CASE("v3math_h: LLSD constructor and getValue round trip", "[v3math_h]") { S32 a = 231; LLSD llsd(a); LLVector3 vec3(llsd); LLSD sd = vec3.getValue(); LLVector3 vec3a(sd); - ensure("1:LLVector3:Fail to initialize ", (vec3 == vec3a)); + CHECK(vec3 == vec3a); } - template<> template<> - void v3math_object::test<4>() + TEST_CASE("v3math_h: assignment copies every component", "[v3math_h]") { S32 a = 231; LLSD llsd(a); LLVector3 vec3(llsd),vec3a; vec3a = vec3; - ensure("1:Operator= Fail to initialize " ,(vec3 == vec3a)); + CHECK(vec3 == vec3a); } - template<> template<> - void v3math_object::test<5>() + TEST_CASE("v3math_h: isFinite and the clearVec/setVec/zeroVec setters", "[v3math_h]") { F32 x = 2.32f, y = 1.212f, z = -.12f; LLVector3 vec3(x,y,z); - ensure("1:isFinite= Fail to initialize ", (true == vec3.isFinite()));//need more test cases: + CHECK(vec3.isFinite());//need more test cases: vec3.clearVec(); - ensure("2:clearVec:Fail to set values ", ((0.f == vec3.mV[VX]) && (0.f == vec3.mV[VY]) && (0.f == vec3.mV[VZ]))); + CHECK(vec3.mV[VX] == 0.f); + CHECK(vec3.mV[VY] == 0.f); + CHECK(vec3.mV[VZ] == 0.f); + vec3.setVec(x,y,z); - ensure("3:setVec:Fail to set values ", ((2.32f == vec3.mV[VX]) && (1.212f == vec3.mV[VY]) && (-.12f == vec3.mV[VZ]))); + CHECK(vec3.mV[VX] == 2.32f); + CHECK(vec3.mV[VY] == 1.212f); + CHECK(vec3.mV[VZ] == -.12f); + vec3.zeroVec(); - ensure("4:zeroVec:Fail to set values ", ((0.f == vec3.mV[VX]) && (0.f == vec3.mV[VY]) && (0.f == vec3.mV[VZ]))); + CHECK(vec3.mV[VX] == 0.f); + CHECK(vec3.mV[VY] == 0.f); + CHECK(vec3.mV[VZ] == 0.f); } - template<> template<> - void v3math_object::test<6>() + TEST_CASE("v3math_h: abs and the setVec overloads", "[v3math_h]") { F32 x = 2.32f, y = 1.212f, z = -.12f; LLVector3 vec3(x,y,z),vec3a; vec3.abs(); - ensure("1:abs:Fail ", ((x == vec3.mV[VX]) && (y == vec3.mV[VY]) && (-z == vec3.mV[VZ]))); + CHECK(x == vec3.mV[VX]); + CHECK(y == vec3.mV[VY]); + CHECK(-z == vec3.mV[VZ]); + vec3a.setVec(vec3); - ensure("2:setVec:Fail to initialize ", (vec3a == vec3)); + CHECK(vec3a == vec3); const F32 vec[3] = {1.2f ,3.2f, -4.2f}; vec3.clearVec(); vec3.setVec(vec); - ensure("3:setVec:Fail to initialize ", ((1.2f == vec3.mV[VX]) && (3.2f == vec3.mV[VY]) && (-4.2f == vec3.mV[VZ]))); + CHECK(vec3.mV[VX] == 1.2f); + CHECK(vec3.mV[VY] == 3.2f); + CHECK(vec3.mV[VZ] == -4.2f); + vec3a.clearVec(); LLVector3d vector3d(vec3); vec3a.setVec(vector3d); - ensure("4:setVec:Fail to initialize ", (vec3 == vec3a)); + CHECK(vec3 == vec3a); LLVector4 vector4(vec3); vec3a.clearVec(); vec3a.setVec(vector4); - ensure("5:setVec:Fail to initialize ", (vec3 == vec3a)); + CHECK(vec3 == vec3a); } - template<> template<> - void v3math_object::test<7>() + TEST_CASE("v3math_h: clamp limits components and reports a change", "[v3math_h]") { F32 x = 2.32f, y = 3.212f, z = -.12f; F32 min = 0.0001f, max = 3.0f; LLVector3 vec3(x,y,z); - ensure("1:clamp:Fail ", true == vec3.clamp(min, max) && x == vec3.mV[VX] && max == vec3.mV[VY] && min == vec3.mV[VZ]); + CHECK(vec3.clamp(min, max)); + CHECK(x == vec3.mV[VX]); + CHECK(max == vec3.mV[VY]); + CHECK(min == vec3.mV[VZ]); + x = 1.f, y = 2.2f, z = 2.8f; vec3.setVec(x,y,z); - ensure("2:clamp:Fail ", false == vec3.clamp(min, max)); + CHECK_FALSE(vec3.clamp(min, max)); } - template<> template<> - void v3math_object::test<8>() + TEST_CASE("v3math_h: magVecSquared and magVec", "[v3math_h]") { F32 x = 2.32f, y = 1.212f, z = -.12f; LLVector3 vec3(x,y,z); - ensure("1:magVecSquared:Fail ", is_approx_equal(vec3.magVecSquared(), (x*x + y*y + z*z))); - ensure("2:magVec:Fail ", is_approx_equal(vec3.magVec(), (F32) sqrt(x*x + y*y + z*z))); + CHECK(is_approx_equal(vec3.magVecSquared(), (x*x + y*y + z*z))); + CHECK(is_approx_equal(vec3.magVec(), (F32) sqrt(x*x + y*y + z*z))); } - template<> template<> - void v3math_object::test<9>() + TEST_CASE("v3math_h: abs then isNull on long and very short vectors", "[v3math_h]") { F32 x =-2.0f, y = -3.0f, z = 1.23f ; LLVector3 vec3(x,y,z); - ensure("1:abs():Fail ", (true == vec3.abs())); - ensure("2:isNull():Fail", (false == vec3.isNull())); //Returns true if vector has a _very_small_ length + CHECK(vec3.abs()); + CHECK_FALSE(vec3.isNull()); //Returns true if vector has a _very_small_ length x =.00000001f, y = .000001001f, z = .000001001f; vec3.setVec(x,y,z); - ensure("3:isNull(): Fail ", (true == vec3.isNull())); + CHECK(vec3.isNull()); } - template<> template<> - void v3math_object::test<10>() + TEST_CASE("v3math_h: isExactlyZero and scaleVec", "[v3math_h]") { F32 x =-2.0f, y = -3.0f, z = 1.f ; LLVector3 vec3(x,y,z),vec3a; - ensure("1:isExactlyZero():Fail ", (true == vec3a.isExactlyZero())); + CHECK(vec3a.isExactlyZero()); vec3a = vec3a.scaleVec(vec3); - ensure("2:scaleVec: Fail ", vec3a.mV[VX] == 0.f && vec3a.mV[VY] == 0.f && vec3a.mV[VZ] == 0.f); + CHECK(vec3a.mV[VX] == 0.f); + CHECK(vec3a.mV[VY] == 0.f); + CHECK(vec3a.mV[VZ] == 0.f); + vec3a.setVec(x,y,z); vec3a = vec3a.scaleVec(vec3); - ensure("3:scaleVec: Fail ", ((4 == vec3a.mV[VX]) && (9 == vec3a.mV[VY]) &&(1 == vec3a.mV[VZ]))); - ensure("4:isExactlyZero():Fail ", (false == vec3.isExactlyZero())); + CHECK(vec3a.mV[VX] == 4); + CHECK(vec3a.mV[VY] == 9); + CHECK(vec3a.mV[VZ] == 1); + + CHECK_FALSE(vec3.isExactlyZero()); } - template<> template<> - void v3math_object::test<11>() + TEST_CASE("v3math_h: rotVec by an angle about an axis is repeatable", "[v3math_h]") { F32 x =20.0f, y = 30.0f, z = 15.f ; F32 angle = 100.f; @@ -190,31 +201,29 @@ namespace vec3a = vec3a.rotVec(angle, vec3); LLVector3 vec3b(1.f,2.f,3.f); vec3b = vec3b.rotVec(angle, vec3); - ensure_equals("rotVec():Fail" ,vec3b,vec3a); + CHECK(vec3b == vec3a); } - template<> template<> - void v3math_object::test<12>() + TEST_CASE("v3math_h: subscript operator reads components and returns references", "[v3math_h]") { F32 x =-2.0f, y = -3.0f, z = 1.f ; LLVector3 vec3(x,y,z); - ensure("1:operator [] failed",( x == vec3[0])); - ensure("2:operator [] failed",( y == vec3[1])); - ensure("3:operator [] failed",( z == vec3[2])); + CHECK(x == vec3[0]); + CHECK(y == vec3[1]); + CHECK(z == vec3[2]); vec3.clearVec(); x = 23.f, y = -.2361f, z = 3.25; vec3.setVec(x,y,z); F32 &ref1 = vec3[0]; - ensure("4:operator [] failed",( ref1 == vec3[0])); + CHECK(ref1 == vec3[0]); F32 &ref2 = vec3[1]; - ensure("5:operator [] failed",( ref2 == vec3[1])); + CHECK(ref2 == vec3[1]); F32 &ref3 = vec3[2]; - ensure("6:operator [] failed",( ref3 == vec3[2])); + CHECK(ref3 == vec3[2]); } - template<> template<> - void v3math_object::test<13>() + TEST_CASE("v3math_h: operator+ adds componentwise", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 = -2.3f, y2 = 1.11f, z2 = 1234.234f; F32 val1, val2, val3; @@ -223,7 +232,9 @@ namespace val1 = x1+x2; val2 = y1+y2; val3 = z1+z2; - ensure("1:operator+ failed",(val1 == vec3b.mV[VX]) && (val2 == vec3b.mV[VY]) && (val3 == vec3b.mV[VZ])); + CHECK(val1 == vec3b.mV[VX]); + CHECK(val2 == vec3b.mV[VY]); + CHECK(val3 == vec3b.mV[VZ]); vec3.clearVec(); vec3a.clearVec(); @@ -235,11 +246,12 @@ namespace val1 = x1+x2; val2 = y1+y2; val3 = z1+z2; - ensure("2:operator+ failed",(val1 == vec3b.mV[VX]) && (val2 == vec3b.mV[VY]) && (val3 == vec3b.mV[VZ])); + CHECK(val1 == vec3b.mV[VX]); + CHECK(val2 == vec3b.mV[VY]); + CHECK(val3 == vec3b.mV[VZ]); } - template<> template<> - void v3math_object::test<14>() + TEST_CASE("v3math_h: operator- subtracts componentwise", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 = -2.3f, y2 = 1.11f, z2 = 1234.234f; F32 val1, val2, val3; @@ -248,7 +260,9 @@ namespace val1 = x1-x2; val2 = y1-y2; val3 = z1-z2; - ensure("1:operator- failed",(val1 == vec3b.mV[VX]) && (val2 == vec3b.mV[VY]) && (val3 == vec3b.mV[VZ])); + CHECK(val1 == vec3b.mV[VX]); + CHECK(val2 == vec3b.mV[VY]); + CHECK(val3 == vec3b.mV[VZ]); vec3.clearVec(); vec3a.clearVec(); @@ -260,18 +274,19 @@ namespace val1 = x1-x2; val2 = y1-y2; val3 = z1-z2; - ensure("2:operator- failed",(val1 == vec3b.mV[VX]) && (val2 == vec3b.mV[VY]) && (val3 == vec3b.mV[VZ])); + CHECK(val1 == vec3b.mV[VX]); + CHECK(val2 == vec3b.mV[VY]); + CHECK(val3 == vec3b.mV[VZ]); } - template<> template<> - void v3math_object::test<15>() + TEST_CASE("v3math_h: operator* as dot product and scalar product", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 = -2.3f, y2 = 1.11f, z2 = 1234.234f; F32 val1, val2, val3; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); val1 = vec3 * vec3a; val2 = x1*x2 + y1*y2 + z1*z2; - ensure_equals("1:operator* failed",val1,val2); + CHECK(val1 == val2); vec3a.clearVec(); F32 mulVal = 4.332f; @@ -279,14 +294,18 @@ namespace val1 = x1*mulVal; val2 = y1*mulVal; val3 = z1*mulVal; - ensure("2:operator* failed",(val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY])&& (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); + vec3a.clearVec(); vec3a = mulVal * vec3; - ensure("3:operator* failed ", (val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY])&& (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); } - template<> template<> - void v3math_object::test<16>() + TEST_CASE("v3math_h: operator% is the cross product", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 = -2.3f, y2 = 1.11f, z2 = 1234.234f; F32 val1, val2, val3; @@ -295,7 +314,9 @@ namespace val1 = y1*z2 - y2*z1; val2 = z1*x2 -z2*x1; val3 = x1*y2-x2*y1; - ensure("1:operator% failed",(val1 == vec3b.mV[VX]) && (val2 == vec3b.mV[VY]) && (val3 == vec3b.mV[VZ])); + CHECK(val1 == vec3b.mV[VX]); + CHECK(val2 == vec3b.mV[VY]); + CHECK(val3 == vec3b.mV[VZ]); vec3.clearVec(); vec3a.clearVec(); @@ -307,11 +328,12 @@ namespace val1 = y1*z2 - y2*z1; val2 = z1*x2 -z2*x1; val3 = x1*y2-x2*y1; - ensure("2:operator% failed ", (val1 == vec3b.mV[VX]) && (val2 == vec3b.mV[VY]) && (val3 == vec3b.mV[VZ])); + CHECK(val1 == vec3b.mV[VX]); + CHECK(val2 == vec3b.mV[VY]); + CHECK(val3 == vec3b.mV[VZ]); } - template<> template<> - void v3math_object::test<17>() + TEST_CASE("v3math_h: operator/ divides by a scalar", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, div = 3.2f; F32 t = 1.f / div, val1, val2, val3; @@ -320,7 +342,9 @@ namespace val1 = x1 * t; val2 = y1 * t; val3 = z1 *t; - ensure("1:operator/ failed",(val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY]) && (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); vec3a.clearVec(); x1 = -.235f, y1 = -24.32f, z1 = .342f, div = -2.2f; @@ -330,15 +354,16 @@ namespace val1 = x1 * t; val2 = y1 * t; val3 = z1 *t; - ensure("2:operator/ failed",(val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY]) && (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); } - template<> template<> - void v3math_object::test<18>() + TEST_CASE("v3math_h: operator== for equal vectors", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f; LLVector3 vec3(x1,y1,z1), vec3a(x1,y1,z1); - ensure("1:operator== failed",(vec3 == vec3a)); + CHECK(vec3 == vec3a); vec3a.clearVec(); x1 = -.235f, y1 = -24.32f, z1 = .342f; @@ -346,24 +371,22 @@ namespace vec3a.clearVec(); vec3.setVec(x1,y1,z1); vec3a.setVec(x1,y1,z1); - ensure("2:operator== failed ", (vec3 == vec3a)); + CHECK(vec3 == vec3a); } - template<> template<> - void v3math_object::test<19>() + TEST_CASE("v3math_h: operator!= for different and equal vectors", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.234f,z2 = 11.2f;; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); - ensure("1:operator!= failed",(vec3a != vec3)); + CHECK(vec3a != vec3); vec3.clearVec(); vec3.clearVec(); vec3a.setVec(vec3); - ensure("2:operator!= failed", ( false == (vec3a != vec3))); + CHECK_FALSE(vec3a != vec3); } - template<> template<> - void v3math_object::test<20>() + TEST_CASE("v3math_h: operator+= adds componentwise", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.2f,z2 = 11.2f;; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); @@ -372,11 +395,12 @@ namespace val1 = x1+x2; val2 = y1+y2; val3 = z1+z2; - ensure("1:operator+= failed",(val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY])&& (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); } - template<> template<> - void v3math_object::test<21>() + TEST_CASE("v3math_h: operator-= subtracts componentwise", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.2f,z2 = 11.2f;; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); @@ -385,11 +409,12 @@ namespace val1 = x2-x1; val2 = y2-y1; val3 = z2-z1; - ensure("1:operator-= failed",(val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY])&& (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); } - template<> template<> - void v3math_object::test<22>() + TEST_CASE("v3math_h: operator*= by a vector and by a scalar", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 = -2.3f, y2 = 1.11f, z2 = 1234.234f; F32 val1,val2,val3; @@ -398,28 +423,30 @@ namespace val1 = x1*x2; val2 = y1*y2; val3 = z1*z2; - ensure("1:operator*= failed",(val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY])&& (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); F32 mulVal = 4.332f; vec3 *=mulVal; val1 = x1*mulVal; val2 = y1*mulVal; val3 = z1*mulVal; - ensure("2:operator*= failed ", is_approx_equal(val1, vec3.mV[VX]) && is_approx_equal(val2, vec3.mV[VY]) && is_approx_equal(val3, vec3.mV[VZ])); + CHECK(is_approx_equal(val1, vec3.mV[VX])); + CHECK(is_approx_equal(val2, vec3.mV[VY])); + CHECK(is_approx_equal(val3, vec3.mV[VZ])); } - template<> template<> - void v3math_object::test<23>() + TEST_CASE("v3math_h: operator%= matches operator%", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 = -2.3f, y2 = 1.11f, z2 = 1234.234f; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2),vec3b; vec3b = vec3a % vec3; vec3a %= vec3; - ensure_equals("1:operator%= failed",vec3a,vec3b); + CHECK(vec3a == vec3b); } - template<> template<> - void v3math_object::test<24>() + TEST_CASE("v3math_h: operator/= divides by a scalar", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, div = 3.2f; F32 t = 1.f / div, val1, val2, val3; @@ -428,20 +455,20 @@ namespace val1 = x1 * t; val2 = y1 * t; val3 = z1 *t; - ensure("1:operator/= failed",(val1 == vec3a.mV[VX]) && (val2 == vec3a.mV[VY]) && (val3 == vec3a.mV[VZ])); + CHECK(val1 == vec3a.mV[VX]); + CHECK(val2 == vec3a.mV[VY]); + CHECK(val3 == vec3a.mV[VZ]); } - template<> template<> - void v3math_object::test<25>() + TEST_CASE("v3math_h: unary operator- negates", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f; LLVector3 vec3(x1,y1,z1), vec3a; vec3a = -vec3; - ensure("1:operator- failed",(-vec3a == vec3)); + CHECK(-vec3a == vec3); } - template<> template<> - void v3math_object::test<26>() + TEST_CASE("v3math_h: operator<< prints equal vectors alike", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 1.2f; std::ostringstream stream1, stream2; @@ -449,51 +476,49 @@ namespace stream1 << vec3; vec3a.setVec(x1,y1,z1); stream2 << vec3a; - ensure("1:operator << failed",(stream1.str() == stream2.str())); + CHECK(stream1.str() == stream2.str()); } - template<> template<> - void v3math_object::test<27>() + TEST_CASE("v3math_h: operator< orders vectors by x first", "[v3math_h]") { F32 x1 =-2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, y2 = 1.11f, z2 = 1234.234f; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); - ensure("1:operator< failed", (true == (vec3 < vec3a))); + CHECK(vec3 < vec3a); x1 =-2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, y2 = 2.f, z2 = 1234.234f; vec3.setVec(x1,y1,z1); vec3a.setVec(x2,y2,z2); - ensure("2:operator< failed ", (true == (vec3 < vec3a))); + CHECK(vec3 < vec3a); x1 =2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, vec3.setVec(x1,y1,z1); vec3a.setVec(x2,y2,z2); - ensure("3:operator< failed ", (false == (vec3 < vec3a))); + CHECK_FALSE(vec3 < vec3a); } - template<> template<> - void v3math_object::test<28>() + TEST_CASE("v3math_h: parseVector3 reads three numbers", "[v3math_h]") { F32 x1 =1.23f, y1 = 2.f,z1 = 4.f; std::string buf("1.23 2. 4"); LLVector3 vec3, vec3a(x1,y1,z1); LLVector3::parseVector3(buf, &vec3); - ensure_equals("1:parseVector3 failed", vec3, vec3a); + CHECK(vec3 == vec3a); } - template<> template<> - void v3math_object::test<29>() + TEST_CASE("v3math_h: scaleVec multiplies componentwise", "[v3math_h]") { F32 x1 =1.f, y1 = 2.f,z1 = 4.f; LLVector3 vec3(x1,y1,z1),vec3a,vec3b; vec3a.setVec(1,1,1); vec3a.scaleVec(vec3); - ensure_equals("1:scaleVec failed", vec3, vec3a); + CHECK(vec3 == vec3a); vec3a.clearVec(); vec3a.setVec(x1,y1,z1); vec3a.scaleVec(vec3); - ensure("2:scaleVec failed", ((1.f ==vec3a.mV[VX])&& (4.f ==vec3a.mV[VY]) && (16.f ==vec3a.mV[VZ]))); + CHECK(vec3a.mV[VX] == 1.f); + CHECK(vec3a.mV[VY] == 4.f); + CHECK(vec3a.mV[VZ] == 16.f); } - template<> template<> - void v3math_object::test<30>() + TEST_CASE("v3math_h: lerp between two vectors", "[v3math_h]") { F32 x1 =-2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, y2 = 1.11f, z2 = 1234.234f; F32 val = 2.3f,val1,val2,val3; @@ -502,54 +527,60 @@ namespace val3 = z1 + (z2 - z1)* val; LLVector3 vec3(x1,y1,z1),vec3a(x2,y2,z2); LLVector3 vec3b = lerp(vec3,vec3a,val); - ensure("1:lerp failed", ((val1 ==vec3b.mV[VX])&& (val2 ==vec3b.mV[VY]) && (val3 ==vec3b.mV[VZ]))); + CHECK(val1 ==vec3b.mV[VX]); + CHECK(val2 ==vec3b.mV[VY]); + CHECK(val3 ==vec3b.mV[VZ]); } - template<> template<> - void v3math_object::test<31>() + TEST_CASE("v3math_h: dist_vec and its squared and 2D forms", "[v3math_h]") { F32 x1 =-2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, y2 = 1.f, z2 = 1.f; F32 val1,val2; LLVector3 vec3(x1,y1,z1),vec3a(x2,y2,z2); val1 = dist_vec(vec3,vec3a); val2 = (F32) sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)* (y1 - y2) + (z1 - z2)* (z1 -z2)); - ensure_equals("1:dist_vec: Fail ",val2, val1); + CHECK(val2 == val1); val1 = dist_vec_squared(vec3,vec3a); val2 =((x1 - x2)*(x1 - x2) + (y1 - y2)* (y1 - y2) + (z1 - z2)* (z1 -z2)); - ensure_equals("2:dist_vec_squared: Fail ",val2, val1); + CHECK(val2 == val1); val1 = dist_vec_squared2D(vec3, vec3a); val2 =(x1 - x2)*(x1 - x2) + (y1 - y2)* (y1 - y2); - ensure_equals("3:dist_vec_squared2D: Fail ",val2, val1); + CHECK(val2 == val1); } - template<> template<> - void v3math_object::test<32>() + TEST_CASE("v3math_h: normVec normalises and zeroes a tiny vector", "[v3math_h]") { F32 x =12.3524f, y = -342.f,z = 4.126341f; LLVector3 vec3(x,y,z); F32 mag = vec3.normVec(); mag = 1.f/ mag; F32 val1 = x* mag, val2 = y* mag, val3 = z* mag; - ensure("1:normVec: Fail ", is_approx_equal(val1, vec3.mV[VX]) && is_approx_equal(val2, vec3.mV[VY]) && is_approx_equal(val3, vec3.mV[VZ])); + CHECK(is_approx_equal(val1, vec3.mV[VX])); + CHECK(is_approx_equal(val2, vec3.mV[VY])); + CHECK(is_approx_equal(val3, vec3.mV[VZ])); + x = 0.000000001f, y = 0.f, z = 0.f; vec3.clearVec(); vec3.setVec(x,y,z); mag = vec3.normVec(); val1 = x* mag, val2 = y* mag, val3 = z* mag; - ensure("2:normVec: Fail ", (mag == 0.) && (0. == vec3.mV[VX]) && (0. == vec3.mV[VY])&& (0. == vec3.mV[VZ])); + CHECK(mag == 0.); + CHECK(vec3.mV[VX] == 0.); + CHECK(vec3.mV[VY] == 0.); + CHECK(vec3.mV[VZ] == 0.); } - template<> template<> - void v3math_object::test<33>() + TEST_CASE("v3math_h: snap rounds to the given decimal places", "[v3math_h]") { F32 x = -202.23412f, y = 123.2312f, z = -89.f; LLVector3 vec(x,y,z); vec.snap(2); - ensure("1:snap: Fail ", is_approx_equal(-202.23f, vec.mV[VX]) && is_approx_equal(123.23f, vec.mV[VY]) && is_approx_equal(-89.f, vec.mV[VZ])); + CHECK(is_approx_equal(-202.23f, vec.mV[VX])); + CHECK(is_approx_equal(123.23f, vec.mV[VY])); + CHECK(is_approx_equal(-89.f, vec.mV[VZ])); } - template<> template<> - void v3math_object::test<34>() + TEST_CASE("v3math_h: quantize16 and quantize8 match the U16 and U8 round trips", "[v3math_h]") { F32 x = 10.f, y = 20.f, z = -15.f; F32 x1, y1, z1; @@ -559,30 +590,40 @@ namespace x1 = U16_to_F32(F32_to_U16(x, lowerxy, upperxy), lowerxy, upperxy); y1 = U16_to_F32(F32_to_U16(y, lowerxy, upperxy), lowerxy, upperxy); z1 = U16_to_F32(F32_to_U16(z, lowerz, upperz), lowerz, upperz); - ensure("1:quantize16: Fail ", is_approx_equal(x1, vec3.mV[VX]) && is_approx_equal(y1, vec3.mV[VY]) && is_approx_equal(z1, vec3.mV[VZ])); + CHECK(is_approx_equal(x1, vec3.mV[VX])); + CHECK(is_approx_equal(y1, vec3.mV[VY])); + CHECK(is_approx_equal(z1, vec3.mV[VZ])); + LLVector3 vec3a(x,y,z); vec3a.quantize8(lowerxy,upperxy,lowerz,upperz); x1 = U8_to_F32(F32_to_U8(x, lowerxy, upperxy), lowerxy, upperxy); y1 = U8_to_F32(F32_to_U8(y, lowerxy, upperxy), lowerxy, upperxy); z1 = U8_to_F32(F32_to_U8(z, lowerz, upperz), lowerz, upperz); - ensure("2:quantize8: Fail ", is_approx_equal(x1, vec3a.mV[VX]) && is_approx_equal(y1, vec3a.mV[VY]) && is_approx_equal(z1, vec3a.mV[VZ])); + CHECK(is_approx_equal(x1, vec3a.mV[VX])); + CHECK(is_approx_equal(y1, vec3a.mV[VY])); + CHECK(is_approx_equal(z1, vec3a.mV[VZ])); } - template<> template<> - void v3math_object::test<35>() + TEST_CASE("v3math_h: LLSD arrays shorter than three leave the rest zero", "[v3math_h]") { LLSD sd = LLSD::emptyArray(); sd[0] = 1.f; LLVector3 parsed_1(sd); - ensure("1:LLSD parse: Fail ", is_approx_equal(parsed_1.mV[VX], 1.f) && is_approx_equal(parsed_1.mV[VY], 0.f) && is_approx_equal(parsed_1.mV[VZ], 0.f)); + CHECK(is_approx_equal(parsed_1.mV[VX], 1.f)); + CHECK(is_approx_equal(parsed_1.mV[VY], 0.f)); + CHECK(is_approx_equal(parsed_1.mV[VZ], 0.f)); sd[1] = 2.f; LLVector3 parsed_2(sd); - ensure("2:LLSD parse: Fail ", is_approx_equal(parsed_2.mV[VX], 1.f) && is_approx_equal(parsed_2.mV[VY], 2.f) && is_approx_equal(parsed_2.mV[VZ], 0.f)); + CHECK(is_approx_equal(parsed_2.mV[VX], 1.f)); + CHECK(is_approx_equal(parsed_2.mV[VY], 2.f)); + CHECK(is_approx_equal(parsed_2.mV[VZ], 0.f)); sd[2] = 3.f; LLVector3 parsed_3(sd); - ensure("3:LLSD parse: Fail ", is_approx_equal(parsed_3.mV[VX], 1.f) && is_approx_equal(parsed_3.mV[VY], 2.f) && is_approx_equal(parsed_3.mV[VZ], 3.f)); + CHECK(is_approx_equal(parsed_3.mV[VX], 1.f)); + CHECK(is_approx_equal(parsed_3.mV[VY], 2.f)); + CHECK(is_approx_equal(parsed_3.mV[VZ], 3.f)); } } diff --git a/indra/llmath/tests/v4color_test.cpp b/indra/llmath/tests/v4color_test.cpp index b70a43f62f..3f72e62c75 100644 --- a/indra/llmath/tests/v4color_test.cpp +++ b/indra/llmath/tests/v4color_test.cpp @@ -28,7 +28,7 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../v4coloru.h" #include "llsd.h" @@ -36,133 +36,155 @@ #include "../v4color.h" -namespace tut -{ namespace { - struct v4color_data - { - }; - typedef test_group v4color_test; - typedef v4color_test::object v4color_object; - tut::v4color_test v4color_testcase("v4color_h"); -} - - template<> template<> - void v4color_object::test<1>() + TEST_CASE("v4color_h: constructors from values or an array or LLColor3 or LLSD or LLColor4U", "[v4color_h]") { LLColor4 llcolor4; - ensure("1:LLColor4:Fail to initialize ", ((0 == llcolor4.mV[VRED]) && (0 == llcolor4.mV[VGREEN]) && (0 == llcolor4.mV[VBLUE])&& (1.0f == llcolor4.mV[VALPHA]))); + CHECK(llcolor4.mV[VRED] == 0); + CHECK(llcolor4.mV[VGREEN] == 0); + CHECK(llcolor4.mV[VBLUE] == 0); + CHECK(llcolor4.mV[VALPHA] == 1.0f); F32 r = 0x20, g = 0xFFFF, b = 0xFF, a = 0xAF; LLColor4 llcolor4a(r,g,b); - ensure("2:LLColor4:Fail to initialize ", ((r == llcolor4a.mV[VRED]) && (g == llcolor4a.mV[VGREEN]) && (b == llcolor4a.mV[VBLUE])&& (1.0f == llcolor4a.mV[VALPHA]))); + CHECK(r == llcolor4a.mV[VRED]); + CHECK(g == llcolor4a.mV[VGREEN]); + CHECK(b == llcolor4a.mV[VBLUE]); + CHECK(llcolor4a.mV[VALPHA] == 1.0f); LLColor4 llcolor4b(r,g,b,a); - ensure("3:LLColor4:Fail to initialize ", ((r == llcolor4b.mV[VRED]) && (g == llcolor4b.mV[VGREEN]) && (b == llcolor4b.mV[VBLUE])&& (a == llcolor4b.mV[VALPHA]))); + CHECK(r == llcolor4b.mV[VRED]); + CHECK(g == llcolor4b.mV[VGREEN]); + CHECK(b == llcolor4b.mV[VBLUE]); + CHECK(a == llcolor4b.mV[VALPHA]); const F32 vec[4] = {.112f ,23.2f, -4.2f, -.0001f}; LLColor4 llcolor4c(vec); - ensure("4:LLColor4:Fail to initialize ", ((vec[0] == llcolor4c.mV[VRED]) && (vec[1] == llcolor4c.mV[VGREEN]) && (vec[2] == llcolor4c.mV[VBLUE])&& (vec[3] == llcolor4c.mV[VALPHA]))); + CHECK(vec[0] == llcolor4c.mV[VRED]); + CHECK(vec[1] == llcolor4c.mV[VGREEN]); + CHECK(vec[2] == llcolor4c.mV[VBLUE]); + CHECK(vec[3] == llcolor4c.mV[VALPHA]); LLColor3 llcolor3(-2.23f,1.01f,42.3f); F32 val = -.1f; LLColor4 llcolor4d(llcolor3,val); - ensure("5:LLColor4:Fail to initialize ", ((llcolor3.mV[VRED] == llcolor4d.mV[VRED]) && (llcolor3.mV[VGREEN] == llcolor4d.mV[VGREEN]) && (llcolor3.mV[VBLUE] == llcolor4d.mV[VBLUE])&& (val == llcolor4d.mV[VALPHA]))); + CHECK(llcolor3.mV[VRED] == llcolor4d.mV[VRED]); + CHECK(llcolor3.mV[VGREEN] == llcolor4d.mV[VGREEN]); + CHECK(llcolor3.mV[VBLUE] == llcolor4d.mV[VBLUE]); + CHECK(val == llcolor4d.mV[VALPHA]); LLSD sd = llcolor4d.getValue(); LLColor4 llcolor4e(sd); - ensure_equals("6:LLColor4:(LLSD) failed ", llcolor4d, llcolor4e); + CHECK(llcolor4d == llcolor4e); U8 r1 = 0xF2, g1 = 0xFA, b1 = 0xBF; LLColor4U color4u(r1,g1,b1); LLColor4 llcolor4g(color4u); const F32 SCALE = 1.f/255.f; F32 r2 = r1*SCALE, g2 = g1* SCALE, b2 = b1* SCALE; - ensure("7:LLColor4:Fail to initialize ", ((r2 == llcolor4g.mV[VRED]) && (g2 == llcolor4g.mV[VGREEN]) && (b2 == llcolor4g.mV[VBLUE]))); + CHECK(r2 == llcolor4g.mV[VRED]); + CHECK(g2 == llcolor4g.mV[VGREEN]); + CHECK(b2 == llcolor4g.mV[VBLUE]); } - template<> template<> - void v4color_object::test<2>() + TEST_CASE("v4color_h: setValue and getValue round-trip through LLSD", "[v4color_h]") { LLColor4 llcolor(1.0, 2.0, 3.0, 4.0); LLSD llsd = llcolor.getValue(); LLColor4 llcolor4(llsd), llcolor4a; llcolor4a.setValue(llsd); - ensure("setValue: failed", (llcolor4 == llcolor4a)); + CHECK(llcolor4 == llcolor4a); LLSD sd = llcolor4a.getValue(); LLColor4 llcolor4b(sd); - ensure("getValue: Failed ", (llcolor4b == llcolor4a)); + CHECK(llcolor4b == llcolor4a); } - template<> template<> - void v4color_object::test<3>() + TEST_CASE("v4color_h: setToBlack and setToWhite leave alpha at 1", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF,a = 0xAF; LLColor4 llcolor4(r,g,b,a); llcolor4.setToBlack(); - ensure("setToBlack:Fail to set the black ", ((0 == llcolor4.mV[VRED]) && (0 == llcolor4.mV[VGREEN]) && (0 == llcolor4.mV[VBLUE])&& (1.0f == llcolor4.mV[VALPHA]))); + CHECK(llcolor4.mV[VRED] == 0); + CHECK(llcolor4.mV[VGREEN] == 0); + CHECK(llcolor4.mV[VBLUE] == 0); + CHECK(llcolor4.mV[VALPHA] == 1.0f); llcolor4.setToWhite(); - ensure("setToWhite:Fail to set the white ", ((1.f == llcolor4.mV[VRED]) && (1.f == llcolor4.mV[VGREEN]) && (1.f == llcolor4.mV[VBLUE])&& (1.0f == llcolor4.mV[VALPHA]))); + CHECK(llcolor4.mV[VRED] == 1.f); + CHECK(llcolor4.mV[VGREEN] == 1.f); + CHECK(llcolor4.mV[VBLUE] == 1.f); + CHECK(llcolor4.mV[VALPHA] == 1.0f); } - template<> template<> - void v4color_object::test<4>() + TEST_CASE("v4color_h: setVec from values or colors or an array or LLColor4U", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF, a = 0xAF; LLColor4 llcolor4; llcolor4.setVec(r,g,b); - ensure("1:setVec:Fail to set the values ", ((r == llcolor4.mV[VRED]) && (g == llcolor4.mV[VGREEN]) && (b == llcolor4.mV[VBLUE])&& (1.f == llcolor4.mV[VALPHA]))); + CHECK(r == llcolor4.mV[VRED]); + CHECK(g == llcolor4.mV[VGREEN]); + CHECK(b == llcolor4.mV[VBLUE]); + CHECK(llcolor4.mV[VALPHA] == 1.f); llcolor4.setVec(r,g,b,a); - ensure("2:setVec:Fail to set the values ", ((r == llcolor4.mV[VRED]) && (g == llcolor4.mV[VGREEN]) && (b == llcolor4.mV[VBLUE])&& (a == llcolor4.mV[VALPHA]))); + CHECK(r == llcolor4.mV[VRED]); + CHECK(g == llcolor4.mV[VGREEN]); + CHECK(b == llcolor4.mV[VBLUE]); + CHECK(a == llcolor4.mV[VALPHA]); LLColor4 llcolor4a; llcolor4a.setVec(llcolor4); - ensure_equals("3:setVec:Fail to set the values ", llcolor4a,llcolor4); + CHECK(llcolor4a == llcolor4); LLColor3 llcolor3(-2.23f,1.01f,42.3f); llcolor4a.setVec(llcolor3); - ensure("4:setVec:Fail to set the values ", ((llcolor3.mV[VRED] == llcolor4a.mV[VRED]) && (llcolor3.mV[VGREEN] == llcolor4a.mV[VGREEN]) && (llcolor3.mV[VBLUE] == llcolor4a.mV[VBLUE]))); + CHECK(llcolor3.mV[VRED] == llcolor4a.mV[VRED]); + CHECK(llcolor3.mV[VGREEN] == llcolor4a.mV[VGREEN]); + CHECK(llcolor3.mV[VBLUE] == llcolor4a.mV[VBLUE]); F32 val = -.33f; llcolor4a.setVec(llcolor3,val); - ensure("4:setVec:Fail to set the values ", ((llcolor3.mV[VRED] == llcolor4a.mV[VRED]) && (llcolor3.mV[VGREEN] == llcolor4a.mV[VGREEN]) && (llcolor3.mV[VBLUE] == llcolor4a.mV[VBLUE]) && (val == llcolor4a.mV[VALPHA]))); + CHECK(llcolor3.mV[VRED] == llcolor4a.mV[VRED]); + CHECK(llcolor3.mV[VGREEN] == llcolor4a.mV[VGREEN]); + CHECK(llcolor3.mV[VBLUE] == llcolor4a.mV[VBLUE]); + CHECK(val == llcolor4a.mV[VALPHA]); const F32 vec[4] = {.112f ,23.2f, -4.2f, -.0001f}; LLColor4 llcolor4c; llcolor4c.setVec(vec); - ensure("5:setVec:Fail to initialize ", ((vec[0] == llcolor4c.mV[VRED]) && (vec[1] == llcolor4c.mV[VGREEN]) && (vec[2] == llcolor4c.mV[VBLUE])&& (vec[3] == llcolor4c.mV[VALPHA]))); + CHECK(vec[0] == llcolor4c.mV[VRED]); + CHECK(vec[1] == llcolor4c.mV[VGREEN]); + CHECK(vec[2] == llcolor4c.mV[VBLUE]); + CHECK(vec[3] == llcolor4c.mV[VALPHA]); U8 r1 = 0xF2, g1 = 0xFA, b1= 0xBF; LLColor4U color4u(r1,g1,b1); llcolor4.setVec(color4u); const F32 SCALE = 1.f/255.f; F32 r2 = r1*SCALE, g2 = g1* SCALE, b2 = b1* SCALE; - ensure("6:setVec:Fail to initialize ", ((r2 == llcolor4.mV[VRED]) && (g2 == llcolor4.mV[VGREEN]) && (b2 == llcolor4.mV[VBLUE]))); + CHECK(r2 == llcolor4.mV[VRED]); + CHECK(g2 == llcolor4.mV[VGREEN]); + CHECK(b2 == llcolor4.mV[VBLUE]); } - template<> template<> - void v4color_object::test<5>() + TEST_CASE("v4color_h: setAlpha sets the alpha", "[v4color_h]") { F32 alpha = 0xAF; LLColor4 llcolor4; llcolor4.setAlpha(alpha); - ensure("setAlpha:Fail to initialize ", (alpha == llcolor4.mV[VALPHA])); + CHECK(alpha == llcolor4.mV[VALPHA]); } - template<> template<> - void v4color_object::test<6>() + TEST_CASE("v4color_h: magVecSquared and magVec", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF; LLColor4 llcolor4(r,g,b); - ensure("magVecSquared:Fail ", is_approx_equal(llcolor4.magVecSquared(), (r*r + g*g + b*b))); - ensure("magVec:Fail ", is_approx_equal(llcolor4.magVec(), (F32) sqrt(r*r + g*g + b*b))); + CHECK(is_approx_equal(llcolor4.magVecSquared(), (r*r + g*g + b*b))); + CHECK(is_approx_equal(llcolor4.magVec(), (F32) sqrt(r*r + g*g + b*b))); } - template<> template<> - void v4color_object::test<7>() + TEST_CASE("v4color_h: normVec normalizes the color and returns its magnitude", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF; LLColor4 llcolor4(r,g,b); @@ -170,56 +192,57 @@ namespace F32 mag = (F32) sqrt(r*r + g*g + b*b); F32 oomag = 1.f / mag; F32 val1 = r * oomag, val2 = g * oomag, val3 = b * oomag; - ensure("1:normVec failed ", (is_approx_equal(val1, llcolor4.mV[0]) && is_approx_equal(val2, llcolor4.mV[1]) && is_approx_equal(val3, llcolor4.mV[2]) && is_approx_equal(vecMag, mag))); + CHECK(is_approx_equal(val1, llcolor4.mV[0])); + CHECK(is_approx_equal(val2, llcolor4.mV[1])); + CHECK(is_approx_equal(val3, llcolor4.mV[2])); + CHECK(is_approx_equal(vecMag, mag)); } - template<> template<> - void v4color_object::test<8>() + TEST_CASE("v4color_h: isOpaque only when alpha is exactly 1", "[v4color_h]") { LLColor4 llcolor4; - ensure("1:isOpaque failed ",(1 == llcolor4.isOpaque())); + CHECK(llcolor4.isOpaque() == 1); F32 r = 0x20, g = 0xFFFF, b = 0xFF,a = 1.f; llcolor4.setVec(r,g,b,a); - ensure("2:isOpaque failed ",(1 == llcolor4.isOpaque())); + CHECK(llcolor4.isOpaque() == 1); a = 2.f; llcolor4.setVec(r,g,b,a); - ensure("3:isOpaque failed ",(0 == llcolor4.isOpaque())); + CHECK(llcolor4.isOpaque() == 0); } - template<> template<> - void v4color_object::test<9>() + TEST_CASE("v4color_h: subscript operator reads components and gives references", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF; LLColor4 llcolor4(r,g,b); - ensure("1:operator [] failed",( r == llcolor4[0])); - ensure("2:operator [] failed",( g == llcolor4[1])); - ensure("3:operator [] failed",( b == llcolor4[2])); + CHECK(r == llcolor4[0]); + CHECK(g == llcolor4[1]); + CHECK(b == llcolor4[2]); r = 0xA20, g = 0xFBFF, b = 0xFFF; llcolor4.setVec(r,g,b); F32 &ref1 = llcolor4[0]; - ensure("4:operator [] failed",( ref1 == llcolor4[0])); + CHECK(ref1 == llcolor4[0]); F32 &ref2 = llcolor4[1]; - ensure("5:operator [] failed",( ref2 == llcolor4[1])); + CHECK(ref2 == llcolor4[1]); F32 &ref3 = llcolor4[2]; - ensure("6:operator [] failed",( ref3 == llcolor4[2])); + CHECK(ref3 == llcolor4[2]); } - template<> template<> - void v4color_object::test<10>() + TEST_CASE("v4color_h: assignment from LLColor3 and construction from LLSD", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF; LLColor3 llcolor3(r,g,b); LLColor4 llcolor4a,llcolor4b; llcolor4a = llcolor3; - ensure("Operator=:Fail to initialize ", ((llcolor3.mV[0] == llcolor4a.mV[VRED]) && (llcolor3.mV[1] == llcolor4a.mV[VGREEN]) && (llcolor3.mV[2] == llcolor4a.mV[VBLUE]))); + CHECK(llcolor3.mV[0] == llcolor4a.mV[VRED]); + CHECK(llcolor3.mV[1] == llcolor4a.mV[VGREEN]); + CHECK(llcolor3.mV[2] == llcolor4a.mV[VBLUE]); LLSD sd = llcolor4a.getValue(); llcolor4b = LLColor4(sd); - ensure_equals("Operator= LLSD:Fail ", llcolor4a, llcolor4b); + CHECK(llcolor4a == llcolor4b); } - template<> template<> - void v4color_object::test<11>() + TEST_CASE("v4color_h: operator<< prints equal colors the same", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF; std::ostringstream stream1, stream2; @@ -227,136 +250,155 @@ namespace stream1 << llcolor4a; llcolor4b.setVec(r,g,b); stream2 << llcolor4b; - ensure("operator << failed ", (stream1.str() == stream2.str())); + CHECK(stream1.str() == stream2.str()); } - template<> template<> - void v4color_object::test<12>() + TEST_CASE("v4color_h: operator+ and operator+=", "[v4color_h]") { F32 r1 = 0x20, g1 = 0xFFFF, b1 = 0xFF; F32 r2 = 0xABF, g2 = 0xFB, b2 = 0xFFF; LLColor4 llcolor4a(r1,g1,b1),llcolor4b(r2,g2,b2),llcolor4c; llcolor4c = llcolor4b + llcolor4a; - ensure("operator+:Fail to Add the values ", (is_approx_equal(r1+r2,llcolor4c.mV[VRED]) && is_approx_equal(g1+g2,llcolor4c.mV[VGREEN]) && is_approx_equal(b1+b2,llcolor4c.mV[VBLUE]))); + CHECK(is_approx_equal(r1+r2,llcolor4c.mV[VRED])); + CHECK(is_approx_equal(g1+g2,llcolor4c.mV[VGREEN])); + CHECK(is_approx_equal(b1+b2,llcolor4c.mV[VBLUE])); llcolor4b += llcolor4a; - ensure("operator+=:Fail to Add the values ", (is_approx_equal(r1+r2,llcolor4b.mV[VRED]) && is_approx_equal(g1+g2,llcolor4b.mV[VGREEN]) && is_approx_equal(b1+b2,llcolor4b.mV[VBLUE]))); + CHECK(is_approx_equal(r1+r2,llcolor4b.mV[VRED])); + CHECK(is_approx_equal(g1+g2,llcolor4b.mV[VGREEN])); + CHECK(is_approx_equal(b1+b2,llcolor4b.mV[VBLUE])); } - template<> template<> - void v4color_object::test<13>() + TEST_CASE("v4color_h: operator- and operator-=", "[v4color_h]") { F32 r1 = 0x20, g1 = 0xFFFF, b1 = 0xFF; F32 r2 = 0xABF, g2 = 0xFB, b2 = 0xFFF; LLColor4 llcolor4a(r1,g1,b1),llcolor4b(r2,g2,b2),llcolor4c; llcolor4c = llcolor4a - llcolor4b; - ensure("operator-:Fail to subtract the values ", (is_approx_equal(r1-r2,llcolor4c.mV[VRED]) && is_approx_equal(g1-g2,llcolor4c.mV[VGREEN]) && is_approx_equal(b1-b2,llcolor4c.mV[VBLUE]))); + CHECK(is_approx_equal(r1-r2,llcolor4c.mV[VRED])); + CHECK(is_approx_equal(g1-g2,llcolor4c.mV[VGREEN])); + CHECK(is_approx_equal(b1-b2,llcolor4c.mV[VBLUE])); llcolor4a -= llcolor4b; - ensure("operator-=:Fail to subtract the values ", (is_approx_equal(r1-r2,llcolor4a.mV[VRED]) && is_approx_equal(g1-g2,llcolor4a.mV[VGREEN]) && is_approx_equal(b1-b2,llcolor4a.mV[VBLUE]))); + CHECK(is_approx_equal(r1-r2,llcolor4a.mV[VRED])); + CHECK(is_approx_equal(g1-g2,llcolor4a.mV[VGREEN])); + CHECK(is_approx_equal(b1-b2,llcolor4a.mV[VBLUE])); } - template<> template<> - void v4color_object::test<14>() + TEST_CASE("v4color_h: operator* and operator*= by a color and by a scalar", "[v4color_h]") { F32 r1 = 0x20, g1 = 0xFFFF, b1 = 0xFF; F32 r2 = 0xABF, g2 = 0xFB, b2 = 0xFFF; LLColor4 llcolor4a(r1,g1,b1),llcolor4b(r2,g2,b2),llcolor4c; llcolor4c = llcolor4a * llcolor4b; - ensure("1:operator*:Fail to multiply the values", (is_approx_equal(r1*r2,llcolor4c.mV[VRED]) && is_approx_equal(g1*g2,llcolor4c.mV[VGREEN]) && is_approx_equal(b1*b2,llcolor4c.mV[VBLUE]))); + CHECK(is_approx_equal(r1*r2,llcolor4c.mV[VRED])); + CHECK(is_approx_equal(g1*g2,llcolor4c.mV[VGREEN])); + CHECK(is_approx_equal(b1*b2,llcolor4c.mV[VBLUE])); F32 mulVal = 3.33f; llcolor4c = llcolor4a * mulVal; - ensure("2:operator*:Fail ", (is_approx_equal(r1*mulVal,llcolor4c.mV[VRED]) && is_approx_equal(g1*mulVal,llcolor4c.mV[VGREEN]) && is_approx_equal(b1*mulVal,llcolor4c.mV[VBLUE]))); + CHECK(is_approx_equal(r1*mulVal,llcolor4c.mV[VRED])); + CHECK(is_approx_equal(g1*mulVal,llcolor4c.mV[VGREEN])); + CHECK(is_approx_equal(b1*mulVal,llcolor4c.mV[VBLUE])); llcolor4c = mulVal * llcolor4a; - ensure("3:operator*:Fail to multiply the values", (is_approx_equal(r1*mulVal,llcolor4c.mV[VRED]) && is_approx_equal(g1*mulVal,llcolor4c.mV[VGREEN]) && is_approx_equal(b1*mulVal,llcolor4c.mV[VBLUE]))); + CHECK(is_approx_equal(r1*mulVal,llcolor4c.mV[VRED])); + CHECK(is_approx_equal(g1*mulVal,llcolor4c.mV[VGREEN])); + CHECK(is_approx_equal(b1*mulVal,llcolor4c.mV[VBLUE])); llcolor4a *= mulVal; - ensure("4:operator*=:Fail to multiply the values ", (is_approx_equal(r1*mulVal,llcolor4a.mV[VRED]) && is_approx_equal(g1*mulVal,llcolor4a.mV[VGREEN]) && is_approx_equal(b1*mulVal,llcolor4a.mV[VBLUE]))); + CHECK(is_approx_equal(r1*mulVal,llcolor4a.mV[VRED])); + CHECK(is_approx_equal(g1*mulVal,llcolor4a.mV[VGREEN])); + CHECK(is_approx_equal(b1*mulVal,llcolor4a.mV[VBLUE])); LLColor4 llcolor4d(r1,g1,b1),llcolor4e(r2,g2,b2); llcolor4e *= llcolor4d; - ensure("5:operator*=:Fail to multiply the values ", (is_approx_equal(r1*r2,llcolor4e.mV[VRED]) && is_approx_equal(g1*g2,llcolor4e.mV[VGREEN]) && is_approx_equal(b1*b2,llcolor4e.mV[VBLUE]))); + CHECK(is_approx_equal(r1*r2,llcolor4e.mV[VRED])); + CHECK(is_approx_equal(g1*g2,llcolor4e.mV[VGREEN])); + CHECK(is_approx_equal(b1*b2,llcolor4e.mV[VBLUE])); } - template<> template<> - void v4color_object::test<15>() + TEST_CASE("v4color_h: operator% and operator%= scale only the alpha", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF,a = 0x30; F32 div = 12.345f; LLColor4 llcolor4a(r,g,b,a),llcolor4b; llcolor4b = llcolor4a % div;//chnage only alpha value nor r,g,b; - ensure("1operator%:Fail ", (is_approx_equal(r,llcolor4b.mV[VRED]) && is_approx_equal(g,llcolor4b.mV[VGREEN]) && is_approx_equal(b,llcolor4b.mV[VBLUE])&& is_approx_equal(div*a,llcolor4b.mV[VALPHA]))); + CHECK(is_approx_equal(r,llcolor4b.mV[VRED])); + CHECK(is_approx_equal(g,llcolor4b.mV[VGREEN])); + CHECK(is_approx_equal(b,llcolor4b.mV[VBLUE])); + CHECK(is_approx_equal(div*a,llcolor4b.mV[VALPHA])); llcolor4b = div % llcolor4a; - ensure("2operator%:Fail ", (is_approx_equal(r,llcolor4b.mV[VRED]) && is_approx_equal(g,llcolor4b.mV[VGREEN]) && is_approx_equal(b,llcolor4b.mV[VBLUE])&& is_approx_equal(div*a,llcolor4b.mV[VALPHA]))); + CHECK(is_approx_equal(r,llcolor4b.mV[VRED])); + CHECK(is_approx_equal(g,llcolor4b.mV[VGREEN])); + CHECK(is_approx_equal(b,llcolor4b.mV[VBLUE])); + CHECK(is_approx_equal(div*a,llcolor4b.mV[VALPHA])); llcolor4a %= div; - ensure("operator%=:Fail ", (is_approx_equal(a*div,llcolor4a.mV[VALPHA]))); + CHECK(is_approx_equal(a*div,llcolor4a.mV[VALPHA])); } - template<> template<> - void v4color_object::test<16>() + TEST_CASE("v4color_h: operator== and operator!= against LLColor4 and LLColor3", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF,a = 0x30; LLColor4 llcolor4a(r,g,b,a),llcolor4b; llcolor4b = llcolor4a; - ensure("1:operator== failed to ensure the equality ", (llcolor4b == llcolor4a)); + CHECK(llcolor4b == llcolor4a); F32 r1 = 0x2, g1 = 0xFF, b1 = 0xFA; LLColor3 llcolor3(r1,g1,b1); llcolor4b = llcolor3; - ensure("2:operator== failed to ensure the equality ", (llcolor4b == llcolor3)); - ensure("2:operator!= failed to ensure the equality ", (llcolor4a != llcolor3)); + CHECK(llcolor4b == llcolor3); + CHECK(llcolor4a != llcolor3); } - template<> template<> - void v4color_object::test<17>() + TEST_CASE("v4color_h: vec4to3 and vec3to4", "[v4color_h]") { F32 r = 0x20, g = 0xFFFF, b = 0xFF; LLColor4 llcolor4a(r,g,b),llcolor4b; LLColor3 llcolor3 = vec4to3(llcolor4a); - ensure("vec4to3:Fail to convert vec4 to vec3 ", (is_approx_equal(llcolor3.mV[VRED],llcolor4a.mV[VRED]) && is_approx_equal(llcolor3.mV[VGREEN],llcolor4a.mV[VGREEN]) && is_approx_equal(llcolor3.mV[VBLUE],llcolor4a.mV[VBLUE]))); + CHECK(is_approx_equal(llcolor3.mV[VRED],llcolor4a.mV[VRED])); + CHECK(is_approx_equal(llcolor3.mV[VGREEN],llcolor4a.mV[VGREEN])); + CHECK(is_approx_equal(llcolor3.mV[VBLUE],llcolor4a.mV[VBLUE])); llcolor4b = vec3to4(llcolor3); - ensure_equals("vec3to4:Fail to convert vec3 to vec4 ", llcolor4b, llcolor4a); + CHECK(llcolor4b == llcolor4a); } - template<> template<> - void v4color_object::test<18>() + TEST_CASE("v4color_h: lerp interpolates every component", "[v4color_h]") { F32 r1 = 0x20, g1 = 0xFFFF, b1 = 0xFF, val = 0x20; F32 r2 = 0xABF, g2 = 0xFB, b2 = 0xFFF; LLColor4 llcolor4a(r1,g1,b1),llcolor4b(r2,g2,b2),llcolor4c; llcolor4c = lerp(llcolor4a,llcolor4b,val); - ensure("lerp:Fail ", (is_approx_equal(r1 + (r2 - r1)* val,llcolor4c.mV[VRED]) && is_approx_equal(g1 + (g2 - g1)* val,llcolor4c.mV[VGREEN]) && is_approx_equal(b1 + (b2 - b1)* val,llcolor4c.mV[VBLUE]))); + CHECK(is_approx_equal(r1 + (r2 - r1)* val,llcolor4c.mV[VRED])); + CHECK(is_approx_equal(g1 + (g2 - g1)* val,llcolor4c.mV[VGREEN])); + CHECK(is_approx_equal(b1 + (b2 - b1)* val,llcolor4c.mV[VBLUE])); } - template<> template<> - void v4color_object::test<19>() + TEST_CASE("v4color_h: parseColor reads color names and 0 to 255 components", "[v4color_h]") { F32 r = 12.0f, g = -2.3f, b = 1.32f, a = 5.0f; LLColor4 llcolor4a(r,g,b,a),llcolor4b; std::string color("red"); LLColor4::parseColor(color, &llcolor4b); - ensure_equals("1:parseColor() failed to parse the color value ", llcolor4b, LLColor4::red); + CHECK(llcolor4b == LLColor4::red); color = "12.0, -2.3, 1.32, 5.0"; LLColor4::parseColor(color, &llcolor4b); llcolor4a = llcolor4a * (1.f / 255.f); - ensure_equals("2:parseColor() failed to parse the color value ", llcolor4a,llcolor4b); + CHECK(llcolor4a == llcolor4b); color = "yellow5"; llcolor4a.setVec(r,g,b); LLColor4::parseColor(color, &llcolor4a); - ensure_equals("3:parseColor() failed to parse the color value ", llcolor4a, LLColor4::yellow5); + CHECK(llcolor4a == LLColor4::yellow5); } - template<> template<> - void v4color_object::test<20>() + TEST_CASE("v4color_h: parseColor4 reads four floats", "[v4color_h]") { F32 r = 12.0f, g = -2.3f, b = 1.32f, a = 5.0f; LLColor4 llcolor4a(r,g,b,a),llcolor4b; std::string color("12.0, -2.3, 1.32, 5.0"); LLColor4::parseColor4(color, &llcolor4b); - ensure_equals("parseColor4() failed to parse the color value ", llcolor4a, llcolor4b); + CHECK(llcolor4a == llcolor4b); } } diff --git a/indra/llmath/tests/v4coloru_test.cpp b/indra/llmath/tests/v4coloru_test.cpp index 1dd48d801a..3f2a7a984d 100644 --- a/indra/llmath/tests/v4coloru_test.cpp +++ b/indra/llmath/tests/v4coloru_test.cpp @@ -28,121 +28,130 @@ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "llsd.h" #include "../v4coloru.h" -namespace tut -{ namespace { - struct v4coloru_data - { - }; - typedef test_group v4coloru_test; - typedef v4coloru_test::object v4coloru_object; - tut::v4coloru_test v4coloru_testcase("v4coloru_h"); -} - - template<> template<> - void v4coloru_object::test<1>() + TEST_CASE("v4coloru_h: constructors set components and default alpha to 255", "[v4coloru_h]") { LLColor4U llcolor4u; - ensure("1:LLColor4u:Fail to initialize ", ((0 == llcolor4u.mV[VRED]) && (0 == llcolor4u.mV[VGREEN]) && (0 == llcolor4u.mV[VBLUE])&& (255 == llcolor4u.mV[VALPHA]))); + CHECK(llcolor4u.mV[VRED] == 0); + CHECK(llcolor4u.mV[VGREEN] == 0); + CHECK(llcolor4u.mV[VBLUE] == 0); + CHECK(llcolor4u.mV[VALPHA] == 255); U8 r = 0x12, g = 0xFF, b = 0xAF, a = 0x23; LLColor4U llcolor4u1(r,g,b); - ensure("2:LLColor4u:Fail to initialize ", ((r == llcolor4u1.mV[VRED]) && (g == llcolor4u1.mV[VGREEN]) && (b == llcolor4u1.mV[VBLUE])&& (255 == llcolor4u1.mV[VALPHA]))); + CHECK(r == llcolor4u1.mV[VRED]); + CHECK(g == llcolor4u1.mV[VGREEN]); + CHECK(b == llcolor4u1.mV[VBLUE]); + CHECK(llcolor4u1.mV[VALPHA] == 255); LLColor4U llcolor4u2(r,g,b,a); - ensure("3:LLColor4u:Fail to initialize ", ((r == llcolor4u2.mV[VRED]) && (g == llcolor4u2.mV[VGREEN]) && (b == llcolor4u2.mV[VBLUE])&& (a == llcolor4u2.mV[VALPHA]))); + CHECK(r == llcolor4u2.mV[VRED]); + CHECK(g == llcolor4u2.mV[VGREEN]); + CHECK(b == llcolor4u2.mV[VBLUE]); + CHECK(a == llcolor4u2.mV[VALPHA]); const U8 vec[4] = {0x12,0xFF,0xAF,0x23}; LLColor4U llcolor4u3(vec); - ensure("4:LLColor4u:Fail to initialize ", ((vec[0] == llcolor4u3.mV[VRED]) && (vec[1] == llcolor4u3.mV[VGREEN]) && (vec[2] == llcolor4u3.mV[VBLUE])&& (vec[3] == llcolor4u3.mV[VALPHA]))); + CHECK(vec[0] == llcolor4u3.mV[VRED]); + CHECK(vec[1] == llcolor4u3.mV[VGREEN]); + CHECK(vec[2] == llcolor4u3.mV[VBLUE]); + CHECK(vec[3] == llcolor4u3.mV[VALPHA]); LLSD sd = llcolor4u3.getValue(); LLColor4U llcolor4u4(sd); - ensure_equals("5:LLColor4u (LLSD) Failed ", llcolor4u4, llcolor4u3); + CHECK(llcolor4u4 == llcolor4u3); } - template<> template<> - void v4coloru_object::test<2>() + TEST_CASE("v4coloru_h: setValue reads back what getValue wrote", "[v4coloru_h]") { LLColor4U llcolor4ua(1, 2, 3, 4); LLSD sd = llcolor4ua.getValue(); LLColor4U llcolor4u; llcolor4u.setValue(sd); - ensure_equals("setValue(LLSD)/getValue Failed ", llcolor4u, llcolor4ua); + CHECK(llcolor4u == llcolor4ua); } - template<> template<> - void v4coloru_object::test<3>() + TEST_CASE("v4coloru_h: setToBlack and setToWhite leave alpha at 255", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF, a = 0x23; LLColor4U llcolor4u(r,g,b,a); llcolor4u.setToBlack(); - ensure("setToBlack:Fail to set black ", ((0 == llcolor4u.mV[VRED]) && (0 == llcolor4u.mV[VGREEN]) && (0 == llcolor4u.mV[VBLUE])&& (255 == llcolor4u.mV[VALPHA]))); + CHECK(llcolor4u.mV[VRED] == 0); + CHECK(llcolor4u.mV[VGREEN] == 0); + CHECK(llcolor4u.mV[VBLUE] == 0); + CHECK(llcolor4u.mV[VALPHA] == 255); llcolor4u.setToWhite(); - ensure("setToWhite:Fail to white ", ((255 == llcolor4u.mV[VRED]) && (255 == llcolor4u.mV[VGREEN]) && (255 == llcolor4u.mV[VBLUE])&& (255 == llcolor4u.mV[VALPHA]))); + CHECK(llcolor4u.mV[VRED] == 255); + CHECK(llcolor4u.mV[VGREEN] == 255); + CHECK(llcolor4u.mV[VBLUE] == 255); + CHECK(llcolor4u.mV[VALPHA] == 255); } - template<> template<> - void v4coloru_object::test<4>() + TEST_CASE("v4coloru_h: cast from LLSD reads back what getValue wrote", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF, a = 0x23; LLColor4U llcolor4ua(r,g,b,a); LLSD sd = llcolor4ua.getValue(); LLColor4U llcolor4u = (LLColor4U)sd; - ensure_equals("Operator=(LLSD) Failed ", llcolor4u, llcolor4ua); + CHECK(llcolor4u == llcolor4ua); } - template<> template<> - void v4coloru_object::test<5>() + TEST_CASE("v4coloru_h: setVec from values or another color or an array", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF, a = 0x23; LLColor4U llcolor4u; llcolor4u.setVec(r,g,b,a); - ensure("1:setVec:Fail to set the values ", ((r == llcolor4u.mV[VRED]) && (g == llcolor4u.mV[VGREEN]) && (b == llcolor4u.mV[VBLUE])&& (a == llcolor4u.mV[VALPHA]))); + CHECK(r == llcolor4u.mV[VRED]); + CHECK(g == llcolor4u.mV[VGREEN]); + CHECK(b == llcolor4u.mV[VBLUE]); + CHECK(a == llcolor4u.mV[VALPHA]); llcolor4u.setToBlack(); llcolor4u.setVec(r,g,b); - ensure("2:setVec:Fail to set the values ", ((r == llcolor4u.mV[VRED]) && (g == llcolor4u.mV[VGREEN]) && (b == llcolor4u.mV[VBLUE])&& (255 == llcolor4u.mV[VALPHA]))); + CHECK(r == llcolor4u.mV[VRED]); + CHECK(g == llcolor4u.mV[VGREEN]); + CHECK(b == llcolor4u.mV[VBLUE]); + CHECK(llcolor4u.mV[VALPHA] == 255); LLColor4U llcolor4u1; llcolor4u1.setVec(llcolor4u); - ensure_equals("3:setVec:Fail to set the values ", llcolor4u1,llcolor4u); + CHECK(llcolor4u1 == llcolor4u); const U8 vec[4] = {0x12,0xFF,0xAF,0x23}; LLColor4U llcolor4u2; llcolor4u2.setVec(vec); - ensure("4:setVec:Fail to set the values ", ((vec[0] == llcolor4u2.mV[VRED]) && (vec[1] == llcolor4u2.mV[VGREEN]) && (vec[2] == llcolor4u2.mV[VBLUE])&& (vec[3] == llcolor4u2.mV[VALPHA]))); + CHECK(vec[0] == llcolor4u2.mV[VRED]); + CHECK(vec[1] == llcolor4u2.mV[VGREEN]); + CHECK(vec[2] == llcolor4u2.mV[VBLUE]); + CHECK(vec[3] == llcolor4u2.mV[VALPHA]); } - template<> template<> - void v4coloru_object::test<6>() + TEST_CASE("v4coloru_h: setAlpha sets the alpha", "[v4coloru_h]") { U8 alpha = 0x12; LLColor4U llcolor4u; llcolor4u.setAlpha(alpha); - ensure("setAlpha:Fail to set alpha value ", (alpha == llcolor4u.mV[VALPHA])); + CHECK(alpha == llcolor4u.mV[VALPHA]); } - template<> template<> - void v4coloru_object::test<7>() + TEST_CASE("v4coloru_h: magVecSquared and magVec", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF; LLColor4U llcolor4u(r,g,b); - ensure("magVecSquared:Fail ", is_approx_equal(llcolor4u.magVecSquared(), (F32)(r*r + g*g + b*b))); - ensure("magVec:Fail ", is_approx_equal(llcolor4u.magVec(), (F32) sqrt((F32) (r*r + g*g + b*b)))); + CHECK(is_approx_equal(llcolor4u.magVecSquared(), (F32)(r*r + g*g + b*b))); + CHECK(is_approx_equal(llcolor4u.magVec(), (F32) sqrt((F32) (r*r + g*g + b*b)))); } - template<> template<> - void v4coloru_object::test<8>() + TEST_CASE("v4coloru_h: operator<< prints equal colors the same", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF; std::ostringstream stream1, stream2; @@ -150,154 +159,96 @@ namespace stream1 << llcolor4u1; llcolor4u2.setVec(r,g,b); stream2 << llcolor4u2; - ensure("operator << failed ", (stream1.str() == stream2.str())); + CHECK(stream1.str() == stream2.str()); } - template<> template<> - void v4coloru_object::test<9>() + TEST_CASE("v4coloru_h: operator+ and operator+= wrap modulo 256", "[v4coloru_h]") { U8 r1 = 0x12, g1 = 0xFF, b1 = 0xAF; U8 r2 = 0x1C, g2 = 0x9A, b2 = 0x1B; LLColor4U llcolor4u1(r1,g1,b1), llcolor4u2(r2,g2,b2),llcolor4u3; llcolor4u3 = llcolor4u1 + llcolor4u2; - ensure_equals( - "1a.operator+:Fail to Add the values ", - llcolor4u3.mV[VRED], - (U8)(r1+r2)); - ensure_equals( - "1b.operator+:Fail to Add the values ", - llcolor4u3.mV[VGREEN], - (U8)(g1+g2)); - ensure_equals( - "1c.operator+:Fail to Add the values ", - llcolor4u3.mV[VBLUE], - (U8)(b1+b2)); + CHECK(llcolor4u3.mV[VRED] == (U8)(r1+r2)); + CHECK(llcolor4u3.mV[VGREEN] == (U8)(g1+g2)); + CHECK(llcolor4u3.mV[VBLUE] == (U8)(b1+b2)); llcolor4u2 += llcolor4u1; - ensure_equals( - "2a.operator+=:Fail to Add the values ", - llcolor4u2.mV[VRED], - (U8)(r1+r2)); - ensure_equals( - "2b.operator+=:Fail to Add the values ", - llcolor4u2.mV[VGREEN], - (U8)(g1+g2)); - ensure_equals( - "2c.operator+=:Fail to Add the values ", - llcolor4u2.mV[VBLUE], - (U8)(b1+b2)); + CHECK(llcolor4u2.mV[VRED] == (U8)(r1+r2)); + CHECK(llcolor4u2.mV[VGREEN] == (U8)(g1+g2)); + CHECK(llcolor4u2.mV[VBLUE] == (U8)(b1+b2)); } - template<> template<> - void v4coloru_object::test<10>() + TEST_CASE("v4coloru_h: operator- and operator-= wrap modulo 256", "[v4coloru_h]") { U8 r1 = 0x12, g1 = 0xFF, b1 = 0xAF; U8 r2 = 0x1C, g2 = 0x9A, b2 = 0x1B; LLColor4U llcolor4u1(r1,g1,b1), llcolor4u2(r2,g2,b2),llcolor4u3; llcolor4u3 = llcolor4u1 - llcolor4u2; - ensure_equals( - "1a. operator-:Fail to Add the values ", - llcolor4u3.mV[VRED], - (U8)(r1-r2)); - ensure_equals( - "1b. operator-:Fail to Add the values ", - llcolor4u3.mV[VGREEN], - (U8)(g1-g2)); - ensure_equals( - "1c. operator-:Fail to Add the values ", - llcolor4u3.mV[VBLUE], - (U8)(b1-b2)); + CHECK(llcolor4u3.mV[VRED] == (U8)(r1-r2)); + CHECK(llcolor4u3.mV[VGREEN] == (U8)(g1-g2)); + CHECK(llcolor4u3.mV[VBLUE] == (U8)(b1-b2)); llcolor4u1 -= llcolor4u2; - ensure_equals( - "2a. operator-=:Fail to Add the values ", - llcolor4u1.mV[VRED], - (U8)(r1-r2)); - ensure_equals( - "2b. operator-=:Fail to Add the values ", - llcolor4u1.mV[VGREEN], - (U8)(g1-g2)); - ensure_equals( - "2c. operator-=:Fail to Add the values ", - llcolor4u1.mV[VBLUE], - (U8)(b1-b2)); + CHECK(llcolor4u1.mV[VRED] == (U8)(r1-r2)); + CHECK(llcolor4u1.mV[VGREEN] == (U8)(g1-g2)); + CHECK(llcolor4u1.mV[VBLUE] == (U8)(b1-b2)); } - template<> template<> - void v4coloru_object::test<11>() + TEST_CASE("v4coloru_h: operator* by a color and operator*= by a scalar wrap modulo 256", "[v4coloru_h]") { U8 r1 = 0x12, g1 = 0xFF, b1 = 0xAF; U8 r2 = 0x1C, g2 = 0x9A, b2 = 0x1B; LLColor4U llcolor4u1(r1,g1,b1), llcolor4u2(r2,g2,b2),llcolor4u3; llcolor4u3 = llcolor4u1 * llcolor4u2; - ensure_equals( - "1a. operator*:Fail to multiply the values", - llcolor4u3.mV[VRED], - (U8)(r1*r2)); - ensure_equals( - "1b. operator*:Fail to multiply the values", - llcolor4u3.mV[VGREEN], - (U8)(g1*g2)); - ensure_equals( - "1c. operator*:Fail to multiply the values", - llcolor4u3.mV[VBLUE], - (U8)(b1*b2)); + CHECK(llcolor4u3.mV[VRED] == (U8)(r1*r2)); + CHECK(llcolor4u3.mV[VGREEN] == (U8)(g1*g2)); + CHECK(llcolor4u3.mV[VBLUE] == (U8)(b1*b2)); U8 mulVal = 123; llcolor4u1 *= mulVal; - ensure_equals( - "2a. operator*=:Fail to multiply the values", - llcolor4u1.mV[VRED], - (U8)(r1*mulVal)); - ensure_equals( - "2b. operator*=:Fail to multiply the values", - llcolor4u1.mV[VGREEN], - (U8)(g1*mulVal)); - ensure_equals( - "2c. operator*=:Fail to multiply the values", - llcolor4u1.mV[VBLUE], - (U8)(b1*mulVal)); + CHECK(llcolor4u1.mV[VRED] == (U8)(r1*mulVal)); + CHECK(llcolor4u1.mV[VGREEN] == (U8)(g1*mulVal)); + CHECK(llcolor4u1.mV[VBLUE] == (U8)(b1*mulVal)); } - template<> template<> - void v4coloru_object::test<12>() + TEST_CASE("v4coloru_h: operator== and operator!=", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF; LLColor4U llcolor4u(r,g,b),llcolor4u1; llcolor4u1 = llcolor4u; - ensure("operator== failed to ensure the equality ", (llcolor4u1 == llcolor4u)); + CHECK(llcolor4u1 == llcolor4u); llcolor4u1.setToBlack(); - ensure("operator!= failed to ensure the equality ", (llcolor4u1 != llcolor4u)); + CHECK(llcolor4u1 != llcolor4u); } - template<> template<> - void v4coloru_object::test<13>() + TEST_CASE("v4coloru_h: operator%= multiplies the alpha", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF, a = 12; LLColor4U llcolor4u(r,g,b,a); U8 modVal = 45; llcolor4u %= modVal; - ensure_equals("operator%=:Fail ", llcolor4u.mV[VALPHA], (U8)(a * modVal)); + CHECK(llcolor4u.mV[VALPHA] == (U8)(a * modVal)); } - template<> template<> - void v4coloru_object::test<14>() + TEST_CASE("v4coloru_h: parseColor4U reads four values and rejects fewer", "[v4coloru_h]") { U8 r = 0x12, g = 0xFF, b = 0xAF, a = 12; LLColor4U llcolor4u1(r,g,b,a); std::string color("12, 23, 132, 50"); LLColor4U::parseColor4U(color, &llcolor4u1); - ensure("parseColor4U() failed to parse the color value ", ((12 == llcolor4u1.mV[VRED]) && (23 == llcolor4u1.mV[VGREEN]) && (132 == llcolor4u1.mV[VBLUE])&& (50 == llcolor4u1.mV[VALPHA]))); + CHECK(llcolor4u1.mV[VRED] == 12); + CHECK(llcolor4u1.mV[VGREEN] == 23); + CHECK(llcolor4u1.mV[VBLUE] == 132); + CHECK(llcolor4u1.mV[VALPHA] == 50); color = "12, 23, 132"; - ensure("2:parseColor4U() failed to parse the color value ", (false == LLColor4U::parseColor4U(color, &llcolor4u1))); + CHECK_FALSE(LLColor4U::parseColor4U(color, &llcolor4u1)); color = "12"; - ensure("2:parseColor4U() failed to parse the color value ", (false == LLColor4U::parseColor4U(color, &llcolor4u1))); + CHECK_FALSE(LLColor4U::parseColor4U(color, &llcolor4u1)); } - template<> template<> - void v4coloru_object::test<15>() + TEST_CASE("v4coloru_h: multAll rounds and clamps each channel", "[v4coloru_h]") { U8 r = 12, g = 123, b = 3, a = 2; LLColor4U llcolor4u(r,g,b,a),llcolor4u1; @@ -307,31 +258,34 @@ namespace // Previously the test used (U8)ll_round(...) directly, which wraps // modulo 256 for products > 255 (e.g. 123 * 3 = 369 -> 113), locking // in the wrap bug. multAll now clamps, so green saturates at 255. - ensure("multAll:Fail to multiply ", - ((U8)llclampb(ll_round(r * fVal)) == llcolor4u1.mV[VRED]) - && ((U8)llclampb(ll_round(g * fVal)) == llcolor4u1.mV[VGREEN]) - && ((U8)llclampb(ll_round(b * fVal)) == llcolor4u1.mV[VBLUE]) - && ((U8)llclampb(ll_round(a * fVal)) == llcolor4u1.mV[VALPHA])); + CHECK((U8)llclampb(ll_round(r * fVal)) == llcolor4u1.mV[VRED]); + CHECK((U8)llclampb(ll_round(g * fVal)) == llcolor4u1.mV[VGREEN]); + CHECK((U8)llclampb(ll_round(b * fVal)) == llcolor4u1.mV[VBLUE]); + CHECK((U8)llclampb(ll_round(a * fVal)) == llcolor4u1.mV[VALPHA]); } - template<> template<> - void v4coloru_object::test<16>() + TEST_CASE("v4coloru_h: addClampMax adds and clamps each channel at 255", "[v4coloru_h]") { U8 r1 = 12, g1 = 123, b1 = 3, a1 = 2; U8 r2 = 23, g2 = 230, b2 = 124, a2 = 255; LLColor4U llcolor4u(r1,g1,b1,a1),llcolor4u1(r2,g2,b2,a2); llcolor4u1 = llcolor4u1.addClampMax(llcolor4u); - ensure("1:addClampMax():Fail to add the value ", ((r1+r2 == llcolor4u1.mV[VRED]) && (255 == llcolor4u1.mV[VGREEN]) && (b1+b2 == llcolor4u1.mV[VBLUE])&& (255 == llcolor4u1.mV[VALPHA]))); + CHECK(r1+r2 == llcolor4u1.mV[VRED]); + CHECK(llcolor4u1.mV[VGREEN] == 255); + CHECK(b1+b2 == llcolor4u1.mV[VBLUE]); + CHECK(llcolor4u1.mV[VALPHA] == 255); r1 = 132, g1 = 3, b1 = 3, a1 = 2; r2 = 123, g2 = 230, b2 = 154, a2 = 25; LLColor4U llcolor4u2(r1,g1,b1,a1),llcolor4u3(r2,g2,b2,a2); llcolor4u3 = llcolor4u3.addClampMax(llcolor4u2); - ensure("2:addClampMax():Fail to add the value ", ((255 == llcolor4u3.mV[VRED]) && (g1+g2 == llcolor4u3.mV[VGREEN]) && (b1+b2 == llcolor4u3.mV[VBLUE])&& (a1+a2 == llcolor4u3.mV[VALPHA]))); + CHECK(llcolor4u3.mV[VRED] == 255); + CHECK(g1+g2 == llcolor4u3.mV[VGREEN]); + CHECK(b1+b2 == llcolor4u3.mV[VBLUE]); + CHECK(a1+a2 == llcolor4u3.mV[VALPHA]); } - template<> template<> - void v4coloru_object::test<17>() + TEST_CASE("v4coloru_h: setVecScaleClamp scales the brightest channel to 255 and clamps at 0", "[v4coloru_h]") { F32 r = 23.f, g = 12.32f, b = -12.3f; LLColor3 color3(r,g,b); @@ -341,6 +295,9 @@ namespace F32 color_scale_factor = MAX_COLOR/r; S32 r2 = ll_round(r * color_scale_factor); S32 g2 = ll_round(g * color_scale_factor); - ensure("setVecScaleClamp():Fail to add the value ", ((r2 == llcolor4u.mV[VRED]) && (g2 == llcolor4u.mV[VGREEN]) && (0 == llcolor4u.mV[VBLUE])&& (255 == llcolor4u.mV[VALPHA]))); + CHECK(r2 == llcolor4u.mV[VRED]); + CHECK(g2 == llcolor4u.mV[VGREEN]); + CHECK(llcolor4u.mV[VBLUE] == 0); + CHECK(llcolor4u.mV[VALPHA] == 255); } } diff --git a/indra/llmath/tests/v4math_test.cpp b/indra/llmath/tests/v4math_test.cpp index aba953563f..ac96ff8515 100644 --- a/indra/llmath/tests/v4math_test.cpp +++ b/indra/llmath/tests/v4math_test.cpp @@ -27,142 +27,174 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include +#include "altestmatchers.h" #include "llsd.h" #include "../m4math.h" #include "../v4math.h" #include "../llquaternion.h" -namespace tut -{ namespace { - struct v4math_data - { - }; - typedef test_group v4math_test; - typedef v4math_test::object v4math_object; - tut::v4math_test v4math_testcase("v4math_h"); -} - - template<> template<> - void v4math_object::test<1>() + TEST_CASE("v4math_h: constructors set every component and default w to 1", "[v4math_h]") { LLVector4 vec4; - ensure("1:LLVector4:Fail to initialize " ,((0 == vec4.mV[VX]) && (0 == vec4.mV[VY]) && (0 == vec4.mV[VZ])&& (1.0f == vec4.mV[VW]))); + CHECK(vec4.mV[VX] == 0); + CHECK(vec4.mV[VY] == 0); + CHECK(vec4.mV[VZ] == 0); + CHECK(vec4.mV[VW] == 1.0f); F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; LLVector4 vec4a(x,y,z); - ensure("2:LLVector4:Fail to initialize " ,((x == vec4a.mV[VX]) && (y == vec4a.mV[VY]) && (z == vec4a.mV[VZ])&& (1.0f == vec4a.mV[VW]))); + CHECK(x == vec4a.mV[VX]); + CHECK(y == vec4a.mV[VY]); + CHECK(z == vec4a.mV[VZ]); + CHECK(vec4a.mV[VW] == 1.0f); LLVector4 vec4b(x,y,z,w); - ensure("3:LLVector4:Fail to initialize " ,((x == vec4b.mV[VX]) && (y == vec4b.mV[VY]) && (z == vec4b.mV[VZ])&& (w == vec4b.mV[VW]))); + CHECK(x == vec4b.mV[VX]); + CHECK(y == vec4b.mV[VY]); + CHECK(z == vec4b.mV[VZ]); + CHECK(w == vec4b.mV[VW]); const F32 vec[4] = {.112f ,23.2f, -4.2f, -.0001f}; LLVector4 vec4c(vec); - ensure("4:LLVector4:Fail to initialize " ,((vec[0] == vec4c.mV[VX]) && (vec[1] == vec4c.mV[VY]) && (vec[2] == vec4c.mV[VZ])&& (vec[3] == vec4c.mV[VW]))); + CHECK(vec[0] == vec4c.mV[VX]); + CHECK(vec[1] == vec4c.mV[VY]); + CHECK(vec[2] == vec4c.mV[VZ]); + CHECK(vec[3] == vec4c.mV[VW]); LLVector3 vec3(-2.23f,1.01f,42.3f); LLVector4 vec4d(vec3); - ensure("5:LLVector4:Fail to initialize " ,((vec3.mV[VX] == vec4d.mV[VX]) && (vec3.mV[VY] == vec4d.mV[VY]) && (vec3.mV[VZ] == vec4d.mV[VZ])&& (1.f == vec4d.mV[VW]))); + CHECK(vec3.mV[VX] == vec4d.mV[VX]); + CHECK(vec3.mV[VY] == vec4d.mV[VY]); + CHECK(vec3.mV[VZ] == vec4d.mV[VZ]); + CHECK(vec4d.mV[VW] == 1.f); F32 w1 = -.234f; LLVector4 vec4e(vec3,w1); - ensure("6:LLVector4:Fail to initialize " ,((vec3.mV[VX] == vec4e.mV[VX]) && (vec3.mV[VY] == vec4e.mV[VY]) && (vec3.mV[VZ] == vec4e.mV[VZ])&& (w1 == vec4e.mV[VW]))); + CHECK(vec3.mV[VX] == vec4e.mV[VX]); + CHECK(vec3.mV[VY] == vec4e.mV[VY]); + CHECK(vec3.mV[VZ] == vec4e.mV[VZ]); + CHECK(w1 == vec4e.mV[VW]); } - template<> template<> - void v4math_object::test<2>() + TEST_CASE("v4math_h: setVec sets components and clearVec/zeroVec reset them", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; LLVector4 vec4; vec4.setVec(x,y,z); - ensure("1:setVec:Fail to initialize " ,((x == vec4.mV[VX]) && (y == vec4.mV[VY]) && (z == vec4.mV[VZ])&& (1.0f == vec4.mV[VW]))); + CHECK(x == vec4.mV[VX]); + CHECK(y == vec4.mV[VY]); + CHECK(z == vec4.mV[VZ]); + CHECK(vec4.mV[VW] == 1.0f); vec4.clearVec(); - ensure("2:clearVec:Fail " ,((0 == vec4.mV[VX]) && (0 == vec4.mV[VY]) && (0 == vec4.mV[VZ])&& (1.0f == vec4.mV[VW]))); + CHECK(vec4.mV[VX] == 0); + CHECK(vec4.mV[VY] == 0); + CHECK(vec4.mV[VZ] == 0); + CHECK(vec4.mV[VW] == 1.0f); vec4.setVec(x,y,z,w); - ensure("3:setVec:Fail to initialize " ,((x == vec4.mV[VX]) && (y == vec4.mV[VY]) && (z == vec4.mV[VZ])&& (w == vec4.mV[VW]))); + CHECK(x == vec4.mV[VX]); + CHECK(y == vec4.mV[VY]); + CHECK(z == vec4.mV[VZ]); + CHECK(w == vec4.mV[VW]); vec4.zeroVec(); - ensure("4:zeroVec:Fail " ,((0 == vec4.mV[VX]) && (0 == vec4.mV[VY]) && (0 == vec4.mV[VZ])&& (0 == vec4.mV[VW]))); + CHECK(vec4.mV[VX] == 0); + CHECK(vec4.mV[VY] == 0); + CHECK(vec4.mV[VZ] == 0); + CHECK(vec4.mV[VW] == 0); LLVector3 vec3(-2.23f,1.01f,42.3f); vec4.clearVec(); vec4.setVec(vec3); - ensure("5:setVec:Fail to initialize " ,((vec3.mV[VX] == vec4.mV[VX]) && (vec3.mV[VY] == vec4.mV[VY]) && (vec3.mV[VZ] == vec4.mV[VZ])&& (1.f == vec4.mV[VW]))); + CHECK(vec3.mV[VX] == vec4.mV[VX]); + CHECK(vec3.mV[VY] == vec4.mV[VY]); + CHECK(vec3.mV[VZ] == vec4.mV[VZ]); + CHECK(vec4.mV[VW] == 1.f); F32 w1 = -.234f; vec4.zeroVec(); vec4.setVec(vec3,w1); - ensure("6:setVec:Fail to initialize " ,((vec3.mV[VX] == vec4.mV[VX]) && (vec3.mV[VY] == vec4.mV[VY]) && (vec3.mV[VZ] == vec4.mV[VZ])&& (w1 == vec4.mV[VW]))); + CHECK(vec3.mV[VX] == vec4.mV[VX]); + CHECK(vec3.mV[VY] == vec4.mV[VY]); + CHECK(vec3.mV[VZ] == vec4.mV[VZ]); + CHECK(w1 == vec4.mV[VW]); const F32 vec[4] = {.112f ,23.2f, -4.2f, -.0001f}; LLVector4 vec4a; vec4a.setVec(vec); - ensure("7:setVec:Fail to initialize " ,((vec[0] == vec4a.mV[VX]) && (vec[1] == vec4a.mV[VY]) && (vec[2] == vec4a.mV[VZ])&& (vec[3] == vec4a.mV[VW]))); + CHECK(vec[0] == vec4a.mV[VX]); + CHECK(vec[1] == vec4a.mV[VY]); + CHECK(vec[2] == vec4a.mV[VZ]); + CHECK(vec[3] == vec4a.mV[VW]); } - template<> template<> - void v4math_object::test<3>() + TEST_CASE("v4math_h: magVec and magVecSquared", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f; LLVector4 vec4(x,y,z); - ensure("magVec:Fail ", is_approx_equal(vec4.magVec(), (F32) sqrt(x*x + y*y + z*z))); - ensure("magVecSquared:Fail ", is_approx_equal(vec4.magVecSquared(), (x*x + y*y + z*z))); + CHECK(is_approx_equal(vec4.magVec(), (F32) sqrt(x*x + y*y + z*z))); + CHECK(is_approx_equal(vec4.magVecSquared(), (x*x + y*y + z*z))); } - template<> template<> - void v4math_object::test<4>() + TEST_CASE("v4math_h: normVec normalizes and zeroes a near-zero vector", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f; LLVector4 vec4(x,y,z); F32 mag = vec4.normVec(); mag = 1.f/ mag; - ensure("1:normVec: Fail " ,is_approx_equal(mag*x,vec4.mV[VX]) && is_approx_equal(mag*y, vec4.mV[VY])&& is_approx_equal(mag*z, vec4.mV[VZ])); + CHECK(is_approx_equal(mag*x,vec4.mV[VX])); + CHECK(is_approx_equal(mag*y, vec4.mV[VY])); + CHECK(is_approx_equal(mag*z, vec4.mV[VZ])); x = 0.000000001f, y = 0.000000001f, z = 0.000000001f; vec4.clearVec(); vec4.setVec(x,y,z); mag = vec4.normVec(); - ensure("2:normVec: Fail " ,is_approx_equal(mag*x,vec4.mV[VX]) && is_approx_equal(mag*y, vec4.mV[VY])&& is_approx_equal(mag*z, vec4.mV[VZ])); + CHECK(is_approx_equal(mag*x,vec4.mV[VX])); + CHECK(is_approx_equal(mag*y, vec4.mV[VY])); + CHECK(is_approx_equal(mag*z, vec4.mV[VZ])); } - template<> template<> - void v4math_object::test<5>() + TEST_CASE("v4math_h: abs drops signs; isExactlyClear and isExactlyZero", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; LLVector4 vec4(x,y,z,w); vec4.abs(); - ensure("abs:Fail " ,((x == vec4.mV[VX]) && (-y == vec4.mV[VY]) && (-z == vec4.mV[VZ])&& (-w == vec4.mV[VW]))); + CHECK(x == vec4.mV[VX]); + CHECK(-y == vec4.mV[VY]); + CHECK(-z == vec4.mV[VZ]); + CHECK(-w == vec4.mV[VW]); vec4.clearVec(); - ensure("isExactlyClear:Fail " ,(true == vec4.isExactlyClear())); + CHECK(vec4.isExactlyClear()); vec4.zeroVec(); - ensure("isExactlyZero:Fail " ,(true == vec4.isExactlyZero())); + CHECK(vec4.isExactlyZero()); } - template<> template<> - void v4math_object::test<6>() + TEST_CASE("v4math_h: scaleVec multiplies component-wise", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; LLVector4 vec4(x,y,z,w),vec4a; vec4a = vec4.scaleVec(vec4); - ensure("scaleVec:Fail " ,(is_approx_equal(x*x, vec4a.mV[VX]) && is_approx_equal(y*y, vec4a.mV[VY]) && is_approx_equal(z*z, vec4a.mV[VZ])&& is_approx_equal(w*w, vec4a.mV[VW]))); + CHECK(is_approx_equal(x*x, vec4a.mV[VX])); + CHECK(is_approx_equal(y*y, vec4a.mV[VY])); + CHECK(is_approx_equal(z*z, vec4a.mV[VZ])); + CHECK(is_approx_equal(w*w, vec4a.mV[VW])); } - template<> template<> - void v4math_object::test<7>() + TEST_CASE("v4math_h: subscript operator reads components and gives references", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; LLVector4 vec4(x,y,z,w); - ensure("1:operator [] failed " ,( x == vec4[0])); - ensure("2:operator [] failed " ,( y == vec4[1])); - ensure("3:operator [] failed " ,( z == vec4[2])); - ensure("4:operator [] failed " ,( w == vec4[3])); + CHECK(x == vec4[0]); + CHECK(y == vec4[1]); + CHECK(z == vec4[2]); + CHECK(w == vec4[3]); x = 23.f, y = -.2361f, z = 3.25; vec4.setVec(x,y,z); F32 &ref1 = vec4[0]; - ensure("5:operator [] failed " ,( ref1 == vec4[0])); + CHECK(ref1 == vec4[0]); F32 &ref2 = vec4[1]; - ensure("6:operator [] failed " ,( ref2 == vec4[1])); + CHECK(ref2 == vec4[1]); F32 &ref3 = vec4[2]; - ensure("7:operator [] failed " ,( ref3 == vec4[2])); + CHECK(ref3 == vec4[2]); F32 &ref4 = vec4[3]; - ensure("8:operator [] failed " ,( ref4 == vec4[3])); + CHECK(ref4 == vec4[3]); } - template<> template<> - void v4math_object::test<8>() + TEST_CASE("v4math_h: rotVec by a matrix and by a quaternion is repeatable", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; const F32 val[16] = { @@ -176,18 +208,17 @@ namespace vec4.rotVec(mat); vec4a.setVec(x,y,z,w); vec4a.rotVec(mat); - ensure_equals("1:rotVec: Fail " ,vec4a, vec4); + CHECK(vec4a == vec4); F32 a = 2.32f, b = -23.2f, c = -34.1112f, d = 1.010112f; LLQuaternion q(a,b,c,d); LLVector4 vec4b(a,b,c,d),vec4c; vec4b.rotVec(q); vec4c.setVec(a, b, c, d); vec4c.rotVec(q); - ensure_equals("2:rotVec: Fail " ,vec4b, vec4c); + CHECK(vec4b == vec4c); } - template<> template<> - void v4math_object::test<9>() + TEST_CASE("v4math_h: operator<< prints equal vectors the same", "[v4math_h]") { F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; LLVector4 vec4(x,y,z,w),vec4a;; @@ -195,126 +226,139 @@ namespace stream1 << vec4; vec4a.setVec(x,y,z,w); stream2 << vec4a; - ensure("operator << failed",(stream1.str() == stream2.str())); + CHECK(stream1.str() == stream2.str()); } - template<> template<> - void v4math_object::test<10>() + TEST_CASE("v4math_h: operator+ and operator+=", "[v4math_h]") { F32 x1 = 1.f, y1 = 2.f, z1 = -1.1f, w1 = .23f; F32 x2 = 1.2f, y2 = 2.5f, z2 = 1.f, w2 = 1.3f; LLVector4 vec4(x1,y1,z1,w1),vec4a(x2,y2,z2,w2),vec4b; vec4b = vec4a + vec4; - ensure("1:operator+:Fail to initialize " ,(is_approx_equal(x1+x2,vec4b.mV[VX]) && is_approx_equal(y1+y2,vec4b.mV[VY]) && is_approx_equal(z1+z2,vec4b.mV[VZ]))); + CHECK(is_approx_equal(x1+x2,vec4b.mV[VX])); + CHECK(is_approx_equal(y1+y2,vec4b.mV[VY])); + CHECK(is_approx_equal(z1+z2,vec4b.mV[VZ])); x1 = -2.45f, y1 = 2.1f, z1 = 3.0f; vec4.clearVec(); vec4a.clearVec(); vec4.setVec(x1,y1,z1); vec4a +=vec4; - ensure_equals("2:operator+=: Fail to initialize", vec4a,vec4); + CHECK(vec4a == vec4); vec4a += vec4; - ensure("3:operator+=:Fail to initialize " ,(is_approx_equal(2*x1,vec4a.mV[VX]) && is_approx_equal(2*y1,vec4a.mV[VY]) && is_approx_equal(2*z1,vec4a.mV[VZ]))); + CHECK(is_approx_equal(2*x1,vec4a.mV[VX])); + CHECK(is_approx_equal(2*y1,vec4a.mV[VY])); + CHECK(is_approx_equal(2*z1,vec4a.mV[VZ])); } - template<> template<> - void v4math_object::test<11>() + TEST_CASE("v4math_h: operator- and operator-=", "[v4math_h]") { F32 x1 = 1.f, y1 = 2.f, z1 = -1.1f, w1 = .23f; F32 x2 = 1.2f, y2 = 2.5f, z2 = 1.f, w2 = 1.3f; LLVector4 vec4(x1,y1,z1,w1),vec4a(x2,y2,z2,w2),vec4b; vec4b = vec4a - vec4; - ensure("1:operator-:Fail to initialize " ,(is_approx_equal(x2-x1,vec4b.mV[VX]) && is_approx_equal(y2-y1,vec4b.mV[VY]) && is_approx_equal(z2-z1,vec4b.mV[VZ]))); + CHECK(is_approx_equal(x2-x1,vec4b.mV[VX])); + CHECK(is_approx_equal(y2-y1,vec4b.mV[VY])); + CHECK(is_approx_equal(z2-z1,vec4b.mV[VZ])); x1 = -2.45f, y1 = 2.1f, z1 = 3.0f; vec4.clearVec(); vec4a.clearVec(); vec4.setVec(x1,y1,z1); vec4a -=vec4; - ensure_equals("2:operator-=: Fail to initialize" , vec4a,-vec4); + CHECK(vec4a == -vec4); vec4a -=vec4; - ensure("3:operator-=:Fail to initialize " ,(is_approx_equal(-2*x1,vec4a.mV[VX]) && is_approx_equal(-2*y1,vec4a.mV[VY]) && is_approx_equal(-2*z1,vec4a.mV[VZ]))); + CHECK(is_approx_equal(-2*x1,vec4a.mV[VX])); + CHECK(is_approx_equal(-2*y1,vec4a.mV[VY])); + CHECK(is_approx_equal(-2*z1,vec4a.mV[VZ])); } - template<> template<> - void v4math_object::test<12>() + TEST_CASE("v4math_h: dot product and scalar multiplication", "[v4math_h]") { F32 x1 = 1.f, y1 = 2.f, z1 = -1.1f; F32 x2 = 1.2f, y2 = 2.5f, z2 = 1.f; LLVector4 vec4(x1,y1,z1),vec4a(x2,y2,z2); F32 res = vec4 * vec4a; - ensure("1:operator* failed " ,is_approx_equal(res, x1*x2 + y1*y2 + z1*z2)); + CHECK(is_approx_equal(res, x1*x2 + y1*y2 + z1*z2)); vec4a.clearVec(); F32 mulVal = 4.2f; vec4a = vec4 * mulVal; - ensure("2:operator* failed " ,is_approx_equal(x1*mulVal,vec4a.mV[VX]) && is_approx_equal(y1*mulVal, vec4a.mV[VY])&& is_approx_equal(z1*mulVal, vec4a.mV[VZ])); + CHECK(is_approx_equal(x1*mulVal,vec4a.mV[VX])); + CHECK(is_approx_equal(y1*mulVal, vec4a.mV[VY])); + CHECK(is_approx_equal(z1*mulVal, vec4a.mV[VZ])); vec4a.clearVec(); vec4a = mulVal * vec4 ; - ensure("3:operator* failed " ,is_approx_equal(x1*mulVal, vec4a.mV[VX]) && is_approx_equal(y1*mulVal, vec4a.mV[VY])&& is_approx_equal(z1*mulVal, vec4a.mV[VZ])); + CHECK(is_approx_equal(x1*mulVal, vec4a.mV[VX])); + CHECK(is_approx_equal(y1*mulVal, vec4a.mV[VY])); + CHECK(is_approx_equal(z1*mulVal, vec4a.mV[VZ])); vec4 *= mulVal; - ensure("4:operator*= failed " ,is_approx_equal(x1*mulVal, vec4.mV[VX]) && is_approx_equal(y1*mulVal, vec4.mV[VY])&& is_approx_equal(z1*mulVal, vec4.mV[VZ])); + CHECK(is_approx_equal(x1*mulVal, vec4.mV[VX])); + CHECK(is_approx_equal(y1*mulVal, vec4.mV[VY])); + CHECK(is_approx_equal(z1*mulVal, vec4.mV[VZ])); } - template<> template<> - void v4math_object::test<13>() + TEST_CASE("v4math_h: cross product operator% and operator%=", "[v4math_h]") { F32 x1 = 1.f, y1 = 2.f, z1 = -1.1f; F32 x2 = 1.2f, y2 = 2.5f, z2 = 1.f; LLVector4 vec4(x1,y1,z1),vec4a(x2,y2,z2),vec4b; vec4b = vec4 % vec4a; - ensure("1:operator% failed " ,is_approx_equal(y1*z2 - y2*z1, vec4b.mV[VX]) && is_approx_equal(z1*x2 -z2*x1, vec4b.mV[VY]) && is_approx_equal(x1*y2-x2*y1, vec4b.mV[VZ])); + CHECK(is_approx_equal(y1*z2 - y2*z1, vec4b.mV[VX])); + CHECK(is_approx_equal(z1*x2 -z2*x1, vec4b.mV[VY])); + CHECK(is_approx_equal(x1*y2-x2*y1, vec4b.mV[VZ])); vec4 %= vec4a; - ensure_equals("operator%= failed " ,vec4,vec4b); + CHECK(vec4 == vec4b); } - template<> template<> - void v4math_object::test<14>() + TEST_CASE("v4math_h: operator/ and operator/= by a scalar", "[v4math_h]") { F32 x = 1.f, y = 2.f, z = -1.1f,div = 4.2f; F32 t = 1.f / div; LLVector4 vec4(x,y,z), vec4a; vec4a = vec4/div; - ensure("1:operator/ failed " ,is_approx_equal(x*t, vec4a.mV[VX]) && is_approx_equal(y*t, vec4a.mV[VY])&& is_approx_equal(z*t, vec4a.mV[VZ])); + CHECK(is_approx_equal(x*t, vec4a.mV[VX])); + CHECK(is_approx_equal(y*t, vec4a.mV[VY])); + CHECK(is_approx_equal(z*t, vec4a.mV[VZ])); x = 1.23f, y = 4.f, z = -2.32f; vec4.clearVec(); vec4a.clearVec(); vec4.setVec(x,y,z); vec4a = vec4/div; - ensure("2:operator/ failed " ,is_approx_equal(x*t, vec4a.mV[VX]) && is_approx_equal(y*t, vec4a.mV[VY])&& is_approx_equal(z*t, vec4a.mV[VZ])); + CHECK(is_approx_equal(x*t, vec4a.mV[VX])); + CHECK(is_approx_equal(y*t, vec4a.mV[VY])); + CHECK(is_approx_equal(z*t, vec4a.mV[VZ])); vec4 /= div; - ensure("3:operator/ failed " ,is_approx_equal(x*t, vec4.mV[VX]) && is_approx_equal(y*t, vec4.mV[VY])&& is_approx_equal(z*t, vec4.mV[VZ])); + CHECK(is_approx_equal(x*t, vec4.mV[VX])); + CHECK(is_approx_equal(y*t, vec4.mV[VY])); + CHECK(is_approx_equal(z*t, vec4.mV[VZ])); } - template<> template<> - void v4math_object::test<15>() + TEST_CASE("v4math_h: operator== and operator!=", "[v4math_h]") { F32 x = 1.f, y = 2.f, z = -1.1f; LLVector4 vec4(x,y,z), vec4a; - ensure("operator!= failed " ,(vec4 != vec4a)); + CHECK(vec4 != vec4a); vec4a = vec4; - ensure("operator== failed " ,(vec4 ==vec4a)); + CHECK(vec4 ==vec4a); } - template<> template<> - void v4math_object::test<16>() + TEST_CASE("v4math_h: unary minus negates", "[v4math_h]") { F32 x = 1.f, y = 2.f, z = -1.1f; LLVector4 vec4(x,y,z), vec4a; vec4a = - vec4; - ensure("operator- failed " , (vec4 == - vec4a)); + CHECK(vec4 == - vec4a); } - template<> template<> - void v4math_object::test<17>() + TEST_CASE("v4math_h: are_parallel within an epsilon", "[v4math_h]") { F32 x = 1.f, y = 2.f, z = -1.1f,epsilon = .23425f; LLVector4 vec4(x,y,z), vec4a(x,y,z); - ensure("1:are_parallel: Fail " ,(true == are_parallel(vec4a,vec4,epsilon))); + CHECK(are_parallel(vec4a,vec4,epsilon)); x = 21.f, y = 12.f, z = -123.1f; vec4a.clearVec(); vec4a.setVec(x,y,z); - ensure("2:are_parallel: Fail " ,(false == are_parallel(vec4a,vec4,epsilon))); + CHECK_FALSE(are_parallel(vec4a,vec4,epsilon)); } - template<> template<> - void v4math_object::test<18>() + TEST_CASE("v4math_h: angle_between matches acos of the normalized dot product", "[v4math_h]") { F32 x = 1.f, y = 2.f, z = -1.1f; F32 angle1, angle2; @@ -323,32 +367,30 @@ namespace vec4.normVec(); vec4a.normVec(); angle2 = acos(vec4 * vec4a); - ensure_approximately_equals("1:angle_between: Fail " ,angle1,angle2,8); + CHECK_THAT(angle1, ll_test::WithinBits(angle2, 8)); F32 x1 = 21.f, y1 = 2.23f, z1 = -1.1f; LLVector4 vec4b(x,y,z), vec4c(x1,y1,z1); angle1 = angle_between(vec4b, vec4c); vec4b.normVec(); vec4c.normVec(); angle2 = acos(vec4b * vec4c); - ensure_approximately_equals("2:angle_between: Fail " ,angle1,angle2,8); + CHECK_THAT(angle1, ll_test::WithinBits(angle2, 8)); } - template<> template<> - void v4math_object::test<19>() + TEST_CASE("v4math_h: dist_vec and dist_vec_squared", "[v4math_h]") { F32 x1 =-2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, y2 = 1.f, z2 = 1.f; F32 val1,val2; LLVector4 vec4(x1,y1,z1),vec4a(x2,y2,z2); val1 = dist_vec(vec4,vec4a); val2 = (F32) sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)* (y1 - y2) + (z1 - z2)* (z1 -z2)); - ensure_equals("dist_vec: Fail ",val2, val1); + CHECK(val2 == val1); val1 = dist_vec_squared(vec4,vec4a); val2 =((x1 - x2)*(x1 - x2) + (y1 - y2)* (y1 - y2) + (z1 - z2)* (z1 -z2)); - ensure_equals("dist_vec_squared: Fail ",val2, val1); + CHECK(val2 == val1); } - template<> template<> - void v4math_object::test<20>() + TEST_CASE("v4math_h: lerp interpolates every component", "[v4math_h]") { F32 x1 =-2.3f, y1 = 2.f,z1 = 1.2f, w1 = -.23f, x2 = 1.3f, y2 = 1.f, z2 = 1.f,w2 = .12f; F32 val = 2.3f,val1,val2,val3,val4; @@ -359,28 +401,28 @@ namespace val4 = w1 + (w2 - w1)* val; LLVector4 vec4b = lerp(vec4,vec4a,val); LLVector4 check(val1, val2, val3, val4); - ensure_equals("lerp failed", check, vec4b); + CHECK(check == vec4b); } - template<> template<> - void v4math_object::test<21>() + TEST_CASE("v4math_h: vec4to3 and vec3to4", "[v4math_h]") { F32 x = 1.f, y = 2.f, z = -1.1f; LLVector4 vec4(x,y,z); LLVector3 vec3 = vec4to3(vec4); - ensure("vec4to3 failed", ((x == vec3.mV[VX])&& (y == vec3.mV[VY]) && (z == vec3.mV[VZ]))); + CHECK(x == vec3.mV[VX]); + CHECK(y == vec3.mV[VY]); + CHECK(z == vec3.mV[VZ]); LLVector4 vec4a = vec3to4(vec3); - ensure_equals("vec3to4 failed",vec4a,vec4); + CHECK(vec4a == vec4); } - template<> template<> - void v4math_object::test<22>() + TEST_CASE("v4math_h: getValue round-trips through LLVector3", "[v4math_h]") { F32 x = 1.f, y = 2.f, z = -1.1f; LLVector4 vec4(x,y,z); LLSD llsd = vec4.getValue(); LLVector3 vec3(llsd); LLVector4 vec4a = vec3to4(vec3); - ensure_equals("getValue failed",vec4a,vec4); + CHECK(vec4a == vec4); } } diff --git a/indra/llmath/tests/xform_test.cpp b/indra/llmath/tests/xform_test.cpp index 39a72a1cc5..bfda263550 100644 --- a/indra/llmath/tests/xform_test.cpp +++ b/indra/llmath/tests/xform_test.cpp @@ -27,37 +27,27 @@ */ #include "linden_common.h" -#include "../test/lltut.h" +#include #include "../xform.h" -namespace tut -{ namespace { - struct xform_test - { - }; - typedef test_group xform_test_t; - typedef xform_test_t::object xform_test_object_t; - tut::xform_test_t tut_xform_test("LLXForm"); -} - //test case for init(), getParent(), getRotation(), getPositionW(), getWorldRotation() fns. - template<> template<> - void xform_test_object_t::test<1>() + TEST_CASE("LLXForm: a default LLXform is an unparented identity transform", "[LLXForm]") { LLXform xform_obj; LLVector3 emptyVec(0.f,0.f,0.f); LLVector3 initialScaleVec(1.f,1.f,1.f); - ensure("LLXform empty constructor failed: ", !xform_obj.getParent() && !xform_obj.isChanged() && - xform_obj.getPosition() == emptyVec && - (xform_obj.getRotation()).isIdentity() && - xform_obj.getScale() == initialScaleVec && - xform_obj.getPositionW() == emptyVec && - (xform_obj.getWorldRotation()).isIdentity() && - !xform_obj.getScaleChildOffset()); + CHECK_FALSE(xform_obj.getParent()); + CHECK_FALSE(xform_obj.isChanged()); + CHECK(xform_obj.getPosition() == emptyVec); + CHECK((xform_obj.getRotation()).isIdentity()); + CHECK(xform_obj.getScale() == initialScaleVec); + CHECK(xform_obj.getPositionW() == emptyVec); + CHECK((xform_obj.getWorldRotation()).isIdentity()); + CHECK_FALSE(xform_obj.getScaleChildOffset()); } // test cases for @@ -66,8 +56,7 @@ namespace // setRotation(const F32 x, const F32 y, const F32 z) // setPosition(const F32 x, const F32 y, const F32 z) // getLocalMat4(LLMatrix4 &mat) - template<> template<> - void xform_test_object_t::test<2>() + TEST_CASE("LLXForm: scale/position/rotation setters store what they are given", "[LLXForm]") { LLMatrix4 llmat4; LLXform xform_obj; @@ -80,10 +69,10 @@ namespace LLVector3 vec(x, y, z); xform_obj.setScale(x, y, z); xform_obj.setPosition(x, y, posz); - ensure("setScale failed: ", xform_obj.getScale() == vec); + CHECK(xform_obj.getScale() == vec); vec.setVec(x, y, posz); - ensure("getPosition failed: ", xform_obj.getPosition() == vec); + CHECK(xform_obj.getPosition() == vec); x = x * 2.f; y = y + 2.3f; @@ -92,30 +81,30 @@ namespace xform_obj.setPositionX(x); xform_obj.setPositionY(y); xform_obj.setPositionZ(z); - ensure("setPositionX/Y/Z failed: ", xform_obj.getPosition() == vec); + CHECK(xform_obj.getPosition() == vec); xform_obj.setScaleChildOffset(true); - ensure("setScaleChildOffset failed: ", xform_obj.getScaleChildOffset()); + CHECK(xform_obj.getScaleChildOffset()); vec.setVec(x, y, z); xform_obj.addPosition(vec); vec += vec; - ensure("addPosition failed: ", xform_obj.getPosition() == vec); + CHECK(xform_obj.getPosition() == vec); xform_obj.setScale(vec); - ensure("setScale vector failed: ", xform_obj.getScale() == vec); + CHECK(xform_obj.getScale() == vec); LLQuaternion quat(x, y, z, w); xform_obj.setRotation(quat); - ensure("setRotation quat failed: ", xform_obj.getRotation() == quat); + CHECK(xform_obj.getRotation() == quat); xform_obj.setRotation(x, y, z, w); - ensure("getRotation 2 failed: ", xform_obj.getRotation() == quat); + CHECK(xform_obj.getRotation() == quat); xform_obj.setRotation(x, y, z); quat.setQuat(x,y,z); - ensure("setRotation xyz failed: ", xform_obj.getRotation() == quat); + CHECK(xform_obj.getRotation() == quat); // LLXform::setRotation(const F32 x, const F32 y, const F32 z) // Does normalization @@ -125,90 +114,91 @@ namespace } // test cases for inline bool setParent(LLXform *parent) and getParent() fn. - template<> template<> - void xform_test_object_t::test<3>() + TEST_CASE("LLXForm: setParent builds a chain that getRoot and isRoot follow", "[LLXForm]") { LLXform xform_obj; LLXform par; LLXform grandpar; xform_obj.setParent(&par); par.setParent(&grandpar); - ensure("setParent/getParent failed: ", &par == xform_obj.getParent()); - ensure("getRoot failed: ", &grandpar == xform_obj.getRoot()); - ensure("isRoot failed: ", grandpar.isRoot() && !par.isRoot() && !xform_obj.isRoot()); - ensure("isRootEdit failed: ", grandpar.isRootEdit() && !par.isRootEdit() && !xform_obj.isRootEdit()); + CHECK(&par == xform_obj.getParent()); + CHECK(&grandpar == xform_obj.getRoot()); + CHECK(grandpar.isRoot()); + CHECK_FALSE(par.isRoot()); + CHECK_FALSE(xform_obj.isRoot()); + CHECK(grandpar.isRootEdit()); + CHECK_FALSE(par.isRootEdit()); + CHECK_FALSE(xform_obj.isRootEdit()); } - template<> template<> - void xform_test_object_t::test<4>() + TEST_CASE("LLXForm: setters raise their changed flags and clearChanged clears them", "[LLXForm]") { LLXform xform_obj; xform_obj.setChanged(LLXform::TRANSLATED | LLXform::ROTATED | LLXform::SCALED); - ensure("setChanged/isChanged failed: ", xform_obj.isChanged()); + CHECK(xform_obj.isChanged()); xform_obj.clearChanged(LLXform::TRANSLATED | LLXform::ROTATED | LLXform::SCALED); - ensure("clearChanged failed: ", !xform_obj.isChanged()); + CHECK_FALSE(xform_obj.isChanged()); LLVector3 llvect3(12.4f, -5.6f, 0.34f); xform_obj.setScale(llvect3); - ensure("setScale did not set SCALED flag: ", xform_obj.isChanged(LLXform::SCALED)); + CHECK(xform_obj.isChanged(LLXform::SCALED)); xform_obj.setPosition(1.2f, 2.3f, 3.4f); - ensure("setScale did not set TRANSLATED flag: ", xform_obj.isChanged(LLXform::TRANSLATED)); - ensure("TRANSLATED reset SCALED flag: ", xform_obj.isChanged(LLXform::TRANSLATED | LLXform::SCALED)); + CHECK(xform_obj.isChanged(LLXform::TRANSLATED)); + UNSCOPED_INFO("setPosition must not clear SCALED"); + CHECK(xform_obj.isChanged(LLXform::TRANSLATED | LLXform::SCALED)); xform_obj.clearChanged(LLXform::SCALED); - ensure("reset SCALED failed: ", !xform_obj.isChanged(LLXform::SCALED)); + CHECK_FALSE(xform_obj.isChanged(LLXform::SCALED)); xform_obj.setRotation(1, 2, 3, 4); - ensure("ROTATION flag not set ", xform_obj.isChanged(LLXform::TRANSLATED | LLXform::ROTATED)); + UNSCOPED_INFO("setRotation must set ROTATED"); + CHECK(xform_obj.isChanged(LLXform::TRANSLATED | LLXform::ROTATED)); xform_obj.setScale(llvect3); - ensure("ROTATION flag not set ", xform_obj.isChanged(LLXform::MOVED)); + CHECK(xform_obj.isChanged(LLXform::MOVED)); } //to test init() and getWorldMatrix() fns. - template<> template<> - void xform_test_object_t::test<5>() + TEST_CASE("LLXForm: LLXformMatrix init leaves an identity world matrix", "[LLXForm]") { LLXformMatrix formMatrix_obj; formMatrix_obj.init(); LLMatrix4 mat4_obj; - ensure("1. The value is not NULL", 1.f == formMatrix_obj.getWorldMatrix().mMatrix[0][0]); - ensure("2. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[0][1]); - ensure("3. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[0][2]); - ensure("4. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[0][3]); - ensure("5. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[1][0]); - ensure("6. The value is not NULL", 1.f == formMatrix_obj.getWorldMatrix().mMatrix[1][1]); - ensure("7. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[1][2]); - ensure("8. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[1][3]); - ensure("9. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[2][0]); - ensure("10. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[2][1]); - ensure("11. The value is not NULL", 1.f == formMatrix_obj.getWorldMatrix().mMatrix[2][2]); - ensure("12. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[2][3]); - ensure("13. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[3][0]); - ensure("14. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[3][1]); - ensure("15. The value is not NULL", 0.f == formMatrix_obj.getWorldMatrix().mMatrix[3][2]); - ensure("16. The value is not NULL", 1.f == formMatrix_obj.getWorldMatrix().mMatrix[3][3]); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[0][0] == 1.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[0][1] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[0][2] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[0][3] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[1][0] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[1][1] == 1.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[1][2] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[1][3] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[2][0] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[2][1] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[2][2] == 1.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[2][3] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[3][0] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[3][1] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[3][2] == 0.f); + CHECK(formMatrix_obj.getWorldMatrix().mMatrix[3][3] == 1.f); } //to test mMin.clearVec() and mMax.clearVec() fns - template<> template<> - void xform_test_object_t::test<6>() + TEST_CASE("LLXForm: getMinMax after init puts the minimum corner at the origin", "[LLXForm]") { LLXformMatrix formMatrix_obj; formMatrix_obj.init(); LLVector3 llmin_vec3; LLVector3 llmax_vec3; formMatrix_obj.getMinMax(llmin_vec3, llmax_vec3); - ensure("1. The value is not NULL", 0.f == llmin_vec3.mV[0]); - ensure("2. The value is not NULL", 0.f == llmin_vec3.mV[1]); - ensure("3. The value is not NULL", 0.f == llmin_vec3.mV[2]); - ensure("4. The value is not NULL", 0.f == llmin_vec3.mV[0]); - ensure("5. The value is not NULL", 0.f == llmin_vec3.mV[1]); - ensure("6. The value is not NULL", 0.f == llmin_vec3.mV[2]); + CHECK(llmin_vec3.mV[0] == 0.f); + CHECK(llmin_vec3.mV[1] == 0.f); + CHECK(llmin_vec3.mV[2] == 0.f); + CHECK(llmin_vec3.mV[0] == 0.f); + CHECK(llmin_vec3.mV[1] == 0.f); + CHECK(llmin_vec3.mV[2] == 0.f); } //test case of update() fn. - template<> template<> - void xform_test_object_t::test<7>() + TEST_CASE("LLXForm: update composes world position and rotation with the parent", "[LLXForm]") { LLXformMatrix formMatrix_obj; @@ -238,11 +228,11 @@ namespace LLQuaternion worldRot = quat * quatparent; - ensure("getWorldPosition failed: ", formMatrix_obj.getWorldPosition() == worldPos); - ensure("getWorldRotation failed: ", formMatrix_obj.getWorldRotation() == worldRot); + CHECK(formMatrix_obj.getWorldPosition() == worldPos); + CHECK(formMatrix_obj.getWorldRotation() == worldRot); - ensure("getWorldPosition for parent failed: ", parent.getWorldPosition() == llvecpospar); - ensure("getWorldRotation for parent failed: ", parent.getWorldRotation() == quatparent); + CHECK(parent.getWorldPosition() == llvecpospar); + CHECK(parent.getWorldRotation() == quatparent); } } diff --git a/indra/test/CMakeLists.txt b/indra/test/CMakeLists.txt index 6ed15048d8..ab1dce528a 100644 --- a/indra/test/CMakeLists.txt +++ b/indra/test/CMakeLists.txt @@ -24,3 +24,153 @@ target_sources( target_link_libraries(lltut_runner_lib PRIVATE al::flags PUBLIC llcommon ll::tut) set_target_properties(lltut_runner_lib PROPERTIES FOLDER "Tests") + +# The Catch2 harness: the main, what an LL_ERRS does, LOGFAIL's replay, the +# state guard, the teardown, the matchers, the running case's name and the +# failure type. An OBJECT library, which each test binary links directly: in +# a static archive the listeners' registrations, which nothing references, +# would be dropped by the linker. +add_library(al_test_support OBJECT) + +target_sources( + al_test_support + PRIVATE + altestfatal.cpp + altestlogreplay.cpp + altestmain.cpp + altestmatchers.cpp + altestname.cpp + alteststateguard.cpp + altestteardown.cpp +) + +target_sources( + al_test_support + PUBLIC + FILE_SET HEADERS + FILES + altestfailure.h + altestfatal.h + altestlogreplay.h + altestmatchers.h + altestname.h + alteststateguard.h + altestteardown.h +) + +# The matchers compare as llmath's is_approx_equal_fraction() does, a header +# function, so the tests need llmath's headers but not its library. +target_include_directories(al_test_support PUBLIC ${INDRA_SOURCE_DIR}/llmath) +target_link_libraries(al_test_support PRIVATE al::flags PUBLIC llcommon ll::catch2) + +set_target_properties(al_test_support PROPERTIES FOLDER "Tests") + +# The harness's own tests, the first Catch2 binary. Its hidden test cases are +# run one at a time by the entries below, which check the exit code and the +# output CTest sees from the harness: the state guard and what a fixture may +# tell it, LOGFAIL's replay, a crash and an uncaught LL_ERRS, a fixture's end +# failing, the skip and no-match codes, the ISA gate, and a listing needing no +# setup. +al_add_test_binary( + test_support_tests + PROJECT test + SOURCES tests/altestsupport_test.cpp + LIBRARIES llcommon + COMBINED +) + +function(al_expect_from_test_support name) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "ARGS;EXIT;OUTPUT;REJECT" "") + set( + defines + "-DEXE=$" + "-DARGS=${arg_ARGS}" + "-DEXPECT_EXIT=${arg_EXIT}" + ) + if(DEFINED arg_OUTPUT) + list(APPEND defines "-DEXPECT_OUTPUT=${arg_OUTPUT}") + endif() + if(DEFINED arg_REJECT) + list(APPEND defines "-DREJECT_OUTPUT=${arg_REJECT}") + endif() + add_test( + NAME test_support_tests/${name} + COMMAND ${CMAKE_COMMAND} ${defines} -P ${CMAKE_CURRENT_SOURCE_DIR}/run_and_expect.cmake + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + # LOGTEST, if a developer has it set, would print every log line and blur + # what LOGFAIL alone does. + set_tests_properties( + test_support_tests/${name} + PROPERTIES + ENVIRONMENT "LOGTEST=;LOGFAIL=DEBUG" + ENVIRONMENT_MODIFICATION "${AL_TEST_ENVIRONMENT}" + LABELS test + TIMEOUT 60 + ) +endfunction() + +al_expect_from_test_support( + guard_names_the_case + ARGS "[.guard-leak]" + EXIT 1 + OUTPUT "a test case that leaves the environment changed. left the environment changed.*AL_TEST_SUPPORT_LEAK=1" +) +al_expect_from_test_support( + replay_on_failure + ARGS "[.replay-fail]" + EXIT 42 + OUTPUT "log of .a failing test case that logged.*the line a failing case logged" +) +al_expect_from_test_support( + no_replay_on_pass + ARGS "[.replay-pass]" + EXIT 0 + REJECT "the line a passing case logged" +) +al_expect_from_test_support( + guard_accepts_a_fixture + ARGS "[.guard-accept]" + EXIT 0 + REJECT "State guard" +) +al_expect_from_test_support( + replay_on_crash + ARGS "[.crash-replay]" + EXIT nonzero + OUTPUT "log of .a test case that logged, then crashes.*the line a crashing case logged" +) +al_expect_from_test_support( + terminate_names_the_case + ARGS "[.thread-errs]" + EXIT nonzero + OUTPUT "in test case .a test case whose thread hits an LL_ERRS..*an error off the main thread" +) +al_expect_from_test_support( + teardown_failure_fails + ARGS "[.teardown-fails]" + EXIT 1 + OUTPUT "Teardown step threw: LL_ERRS: .*a teardown step that fails" +) +al_expect_from_test_support(all_skipped_is_125 ARGS "[.skip]" EXIT 125) +al_expect_from_test_support( + guard_fails_a_skipped_run + ARGS "[.skip-leak]" + EXIT 1 + OUTPUT "left the environment changed.*AL_TEST_SUPPORT_SKIP_LEAK=1" +) +al_expect_from_test_support(nothing_matched_fails ARGS "no such test case" EXIT 2) +al_expect_from_test_support( + isa_gate_skips + ARGS "--isa-level|9" + EXIT 125 + OUTPUT "does not run x86-64 level 9" +) +# The ISA level is one no host runs: a listing that set up first, or held to +# the gate, would skip rather than list. +al_expect_from_test_support( + listing_needs_no_setup + ARGS "--list-tests|--isa-level|9" + EXIT 0 + OUTPUT "LLSDEquals compares all the way down" +) diff --git a/indra/test/altestfailure.h b/indra/test/altestfailure.h new file mode 100644 index 0000000000..01c0f891ef --- /dev/null +++ b/indra/test/altestfailure.h @@ -0,0 +1,41 @@ +/** + * @file altestfailure.h + * @brief What a failed assertion throws, for a test that catches it to add to + * the report and rethrow, as tests caught tut::failure. + * + * $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$ + */ + +#ifndef AL_ALTESTFAILURE_H +#define AL_ALTESTFAILURE_H + +#include + +namespace ll_test +{ + // Thrown by a failed REQUIRE or a FAIL, after Catch2 has recorded the + // failure: catching it can add to what the case reports, or tidy up, + // and must rethrow, since the failure stands either way. It carries no + // message; Catch2 has reported that already. One alias, so the tests + // that catch it do not each name Catch2's internal header. + using Failure = Catch::TestFailureException; +} + +#endif // AL_ALTESTFAILURE_H diff --git a/indra/test/altestfatal.cpp b/indra/test/altestfatal.cpp new file mode 100644 index 0000000000..c2aaf11481 --- /dev/null +++ b/indra/test/altestfatal.cpp @@ -0,0 +1,102 @@ +/** + * @file altestfatal.cpp + * @brief What an LL_ERRS does in a Catch2 test. + * + * $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 "altestfatal.h" +#include "altestlogreplay.h" +#include "altestname.h" + +#include "llerrorcontrol.h" + +#include +#include +#include +#include + +namespace ll_test +{ + FatalError::FatalError(const std::string& message) + : std::runtime_error("LL_ERRS: " + message) + { + } + + namespace + { + void throwFatalError(const std::string& message) + { + throw FatalError(message); + } + } + + void installFatalFunction() + { + LLError::setFatalFunction(throwFatalError); + } + + void installTerminateHandler() + { +#if LL_WINDOWS + // No abort dialog for CTest to wait out, from any thread. + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#endif + std::set_terminate( + [] + { + std::cerr << "std::terminate in test case \"" << currentTestName() << "\""; + if (std::exception_ptr in_flight = std::current_exception()) + { + try + { + std::rethrow_exception(in_flight); + } + catch (const std::exception& e) + { + std::cerr << ": " << e.what(); + } + catch (...) + { + std::cerr << ": a non-standard exception"; + } + } + std::cerr << std::endl; + std::abort(); + }); +#if LL_WINDOWS + // The CRT keeps a terminate handler a thread, so the one above serves + // the main thread only, and a thread of a test's own ends in the + // default one's abort(). The CRT's SIGABRT handler is the process's, + // and Catch2, which handles structured exceptions on Windows, leaves + // it alone: there the case is named, and LOGFAIL's log replayed, as + // Catch2 does for a crash. What was thrown is no longer in reach, but + // an LL_ERRS logged it. + std::signal(SIGABRT, + [](int) + { + std::cerr << "abort in test case \"" << currentTestName() << "\"" << std::endl; + LogReplay::replay(std::cerr, currentTestName(), true); + }); +#endif + } +} diff --git a/indra/test/altestfatal.h b/indra/test/altestfatal.h new file mode 100644 index 0000000000..ca45a5fb84 --- /dev/null +++ b/indra/test/altestfatal.h @@ -0,0 +1,63 @@ +/** + * @file altestfatal.h + * @brief What an LL_ERRS does in a Catch2 test: it throws, so the test case fails, + * or catches it if it expects one. + * + * $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$ + */ + +#ifndef AL_ALTESTFATAL_H +#define AL_ALTESTFATAL_H + +#include +#include + +namespace ll_test +{ + // Thrown by an LL_ERRS, or a failed llassert, in a test process. Uncaught, + // its test case fails with the message; a test that means to provoke one + // catches it. The TUT runner threw tut::failure the same way, and code + // under test that catches and rethrows exceptions -- a coroutine's + // top level, for one -- carries it to the test as it carried that. + class FatalError : public std::runtime_error + { + public: + explicit FatalError(const std::string& message); + }; + + // Makes every LL_ERRS from here on throw FatalError, on whichever thread + // it happens. Off the main thread, on a thread of the test's own, nothing + // catches it and the process ends through std::terminate; see + // installTerminateHandler. An LLThread is different: outside Windows it + // catches what its run() throws and posts it to the "mainloop" WorkQueue, + // to be thrown again there, so a test of one sees its LL_ERRS only by + // servicing that queue. + void installFatalFunction(); + + // Makes std::terminate, before it aborts, name the test case that was + // running and what the exception in flight said. Catch2's own report of + // the abort that follows names the case outside Windows, but not what was + // thrown. On Windows, where Catch2 handles no SIGABRT and the CRT keeps a + // terminate handler a thread, an abort from any thread names the case and + // replays LOGFAIL's log, where an LL_ERRS wrote what went wrong. + void installTerminateHandler(); +} + +#endif // AL_ALTESTFATAL_H diff --git a/indra/test/altestlogreplay.cpp b/indra/test/altestlogreplay.cpp new file mode 100644 index 0000000000..168ce4bcbd --- /dev/null +++ b/indra/test/altestlogreplay.cpp @@ -0,0 +1,129 @@ +/** + * @file altestlogreplay.cpp + * @brief LOGFAIL: a failed test case's log, replayed. + * + * $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 "altestlogreplay.h" +#include "altestname.h" + +#include +#include +#include + +#include +#include + +namespace ll_test +{ + namespace + { + // Made by install(), and kept for the life of the process: LLError + // holds it as a recorder until then. + std::shared_ptr sReplay; + + class LogReplayListener : public Catch::EventListenerBase + { + public: + using Catch::EventListenerBase::EventListenerBase; + + // Partial: a test case with sections runs once per leaf section, + // and each run fails or passes on its own. + void testCasePartialStarting(Catch::TestCaseInfo const&, uint64_t) override + { + LogReplay::clear(); + } + + void testCasePartialEnded(Catch::TestCaseStats const& stats, uint64_t) override + { + if (stats.totals.assertions.failed > 0) + { + LogReplay::replay(std::cerr, stats.testInfo->name); + } + } + + // A crash -- a signal, or on Windows a structured exception -- ends + // the case without a testCasePartialEnded, at the moment its log + // is wanted most. + void fatalErrorEncountered(Catch::StringRef) override + { + LogReplay::replay(std::cerr, currentTestName(), true); + } + }; + } + + void LogReplay::install(LLError::ELevel level) + { + if (!sReplay) + { + sReplay = std::make_shared(); + LLError::addRecorder(sReplay); + } + LLError::setDefaultLevel(level); + } + + void LogReplay::clear() + { + if (sReplay) + { + std::lock_guard lock(sReplay->mMutex); + sReplay->mLines.clear(); + } + } + + void LogReplay::replay(std::ostream& out, const std::string& test_case, bool after_crash) + { + if (!sReplay) + { + return; + } + std::unique_lock lock(sReplay->mMutex, std::defer_lock); + if (after_crash) + { + if (!lock.try_lock()) + { + out << "---- log of \"" << test_case << "\" not replayed: it was being written ----" + << std::endl; + return; + } + } + else + { + lock.lock(); + } + out << "---- log of \"" << test_case << "\" ----\n"; + for (const std::string& line : sReplay->mLines) + { + out << line << '\n'; + } + out << "---- end of log ----" << std::endl; + } + + void LogReplay::recordMessage(LLError::ELevel, const std::string& message) + { + std::lock_guard lock(mMutex); + mLines.push_back(message); + } +} + +CATCH_REGISTER_LISTENER(ll_test::LogReplayListener) diff --git a/indra/test/altestlogreplay.h b/indra/test/altestlogreplay.h new file mode 100644 index 0000000000..67a788f2f0 --- /dev/null +++ b/indra/test/altestlogreplay.h @@ -0,0 +1,63 @@ +/** + * @file altestlogreplay.h + * @brief LOGFAIL: what LL code logged while a test case ran, kept, and printed only + * if the case failed. + * + * $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$ + */ + +#ifndef AL_ALTESTLOGREPLAY_H +#define AL_ALTESTLOGREPLAY_H + +#include "llerrorcontrol.h" + +#include +#include +#include +#include + +namespace ll_test +{ + // LLError's stderr recorder writes with fprintf, which Catch2's own output + // capture, a swap of std::cout's and std::cerr's buffers, never sees. So + // the log is kept by a recorder of its own: cleared as each test case + // starts, printed to stderr if it failed, and dropped if it passed. + class LogReplay : public LLError::Recorder + { + public: + // From the main, once, when LOGFAIL names a level: everything at or + // above it is kept, and LLError's default level becomes it. + static void install(LLError::ELevel level); + + // The listener's, around each test case. Both do nothing when LOGFAIL + // was not set. After a crash, the thread that crashed may hold the + // log as it writes to it: then a replay says so rather than wait. + static void clear(); + static void replay(std::ostream& out, const std::string& test_case, bool after_crash = false); + + void recordMessage(LLError::ELevel level, const std::string& message) override; + + private: + std::mutex mMutex; + std::vector mLines; + }; +} + +#endif // AL_ALTESTLOGREPLAY_H diff --git a/indra/test/altestmain.cpp b/indra/test/altestmain.cpp new file mode 100644 index 0000000000..a6358e06d3 --- /dev/null +++ b/indra/test/altestmain.cpp @@ -0,0 +1,184 @@ +/** + * @file altestmain.cpp + * @brief The main of every Catch2 test binary: it reads Catch2's command line and + * ours, sets up what LL code expects of a process, runs the tests, and + * ends the binary's process-wide fixtures in order. + * + * $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 "altestfatal.h" +#include "altestlogreplay.h" +#include "alteststateguard.h" +#include "altestteardown.h" + +#include "llapp.h" +#include "llapr.h" +#include "llerrorcontrol.h" +#include "llprocessor.h" +#include "llstring.h" +#include "lltracethreadrecorder.h" + +#include + +#include +#include + +namespace +{ + // Whether the host can run a binary compiled for an x86-64 level. A level + // of 0 is the baseline, which anything the viewer builds for can run. + bool hostRunsIsaLevel(int level) + { + if (level <= 0) + { + return true; + } + LLProcessorInfo cpu; + switch (level) + { + case 2: return cpu.hasSSE42(); + case 3: return cpu.hasAVX2(); + case 4: return cpu.hasAVX512F(); + default: return false; + } + } + + // LOGTEST logs everything at its level to stderr as it happens. Otherwise + // LL code logs at DEBUG to nowhere but the --al-log file and, with LOGFAIL, + // to a buffer replayed for a test case that fails. Either way, LLError is + // set up as LL code expects to find it. + void setUpLogging(const char* executable, bool log_to_file) + { + const char* logtest = std::getenv("LOGTEST"); + if (logtest && *logtest) + { + LLError::initForApplication(".", ".", true /* log to stderr */); + LLError::setDefaultLevel(LLError::decodeLevel(logtest)); + } + else + { + LLError::initForApplication(".", ".", false /* do not log to stderr */); + LLError::setDefaultLevel(LLError::LEVEL_DEBUG); + const char* logfail = std::getenv("LOGFAIL"); + if (logfail && *logfail) + { + ll_test::LogReplay::install(LLError::decodeLevel(logfail)); + } + } + if (log_to_file) + { + // One file to a process: CTest runs a binary's test files side by + // side, each in a process of its own. + LLError::logToFile(llformat("%s.%d.log", executable, LLApp::getPid())); + } + } +} + +int main(int argc, char* argv[]) +{ + // Tracy first, so it allocates before anything else does + // (https://github.com/wolfpld/tracy/issues/196). + LL_PROFILER_FRAME_END; + LL_PROFILER_SET_THREAD_NAME("App"); + + Catch::Session session; + + // Long names only: Catch2 owns every short one. + int isa_level = 0; + bool log_to_file = false; + using namespace Catch::Clara; + session.cli(session.cli() + | Opt(isa_level, "level")["--isa-level"]( + "exit 125, which CTest reads as skipped, unless the host runs this x86-64 " + "level: 2 for SSE4.2, 3 for AVX2, 4 for AVX-512") + | Opt(log_to_file)["--al-log"]( + "also log to ..log, at the level LOGTEST or LOGFAIL " + "names, else DEBUG")); + + // 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; + + if (const int result = session.applyCommandLine(argc, argv); result != 0) + { + return result; + } + + // A listing, help or the like runs no test, and needs nothing set up: CTest + // and tools list a binary that may not be able to run its tests here. + const Catch::ConfigData& config = session.configData(); + if (config.showHelp || config.libIdentify || config.listTests || config.listTags + || config.listReporters || config.listListeners) + { + return session.run(); + } + + if (!hostRunsIsaLevel(isa_level)) + { + std::cout << "Skipped: this host does not run x86-64 level " << isa_level << std::endl; + return 125; + } + + ll_init_apr(); + setUpLogging(argv[0], log_to_file); + ll_test::installFatalFunction(); + ll_test::installTerminateHandler(); + + // LLTrace needs a master recorder before any thread records. Never freed, + // as the TUT runner never freed it: static destructors that record would + // find it gone. + LLTrace::set_master_thread_recorder(new LLTrace::ThreadRecorder()); + + ll_test::StateGuard::addCommonChecks(); + + int result = session.run(); + + const int teardown_failures = ll_test::Teardown::runAll(); + ll_cleanup_apr(); + + // A run that failed no case can still fail: a case left process state + // changed, or a fixture's end found something wrong. Skipped cases ran + // too, as far as they got, so a run of nothing but skips is held to the + // same. + if (result == 0 || result == Catch::AllTestsSkippedExitCode) + { + if (ll_test::StateGuard::changes() > 0) + { + std::cerr << "The state guard found " << ll_test::StateGuard::changes() + << " change(s) a test case left behind; see above." << std::endl; + result = 1; + } + if (teardown_failures > 0) + { + std::cerr << teardown_failures << " teardown step(s) threw; see above." << std::endl; + result = 1; + } + } + // Every test file is a CTest entry of its own, all with 125 as their skip + // code; Catch2 says "all skipped" with 4. + if (result == Catch::AllTestsSkippedExitCode) + { + result = 125; + } + return result; +} diff --git a/indra/test/altestmatchers.cpp b/indra/test/altestmatchers.cpp new file mode 100644 index 0000000000..633a7ce47a --- /dev/null +++ b/indra/test/altestmatchers.cpp @@ -0,0 +1,210 @@ +/** + * @file altestmatchers.cpp + * @brief Catch2 matchers for what LL's TUT helpers compared. + * + * $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 "altestmatchers.h" + +#include "llformat.h" +#include "llsdserialize.h" + +#include +#include + +namespace ll_test +{ + namespace + { + // Where actual and expected first differ: a path from the top, as in + // ["key"][3], and what differs there. Empty "what" means equal. + struct Difference + { + std::string path; + std::string what; + }; + + Difference firstDifference(const LLSD& actual, const LLSD& expected, const std::string& path) + { + if (actual.type() != expected.type()) + { + return { path, "type" }; + } + bool same = true; + switch (actual.type()) + { + case LLSD::TypeUndefined: + break; + case LLSD::TypeBoolean: + same = actual.asBoolean() == expected.asBoolean(); + break; + case LLSD::TypeInteger: + same = actual.asInteger() == expected.asInteger(); + break; + case LLSD::TypeReal: + same = actual.asReal() == expected.asReal(); + break; + case LLSD::TypeString: + same = actual.asString() == expected.asString(); + break; + case LLSD::TypeUUID: + same = actual.asUUID() == expected.asUUID(); + break; + case LLSD::TypeDate: + // Not LLDate's ==, which is made of < and so holds a NaN + // date equal to every date; TUT compared the seconds. + same = actual.asDate().secondsSinceEpoch() == expected.asDate().secondsSinceEpoch(); + break; + case LLSD::TypeURI: + same = actual.asURI().asString() == expected.asURI().asString(); + break; + case LLSD::TypeBinary: + same = actual.asBinary() == expected.asBinary(); + break; + case LLSD::TypeMap: + { + if (actual.size() != expected.size()) + { + return { path, "map size" }; + } + LLSD::map_const_iterator expected_entry = expected.beginMap(); + for (LLSD::map_const_iterator entry = actual.beginMap(); entry != actual.endMap(); + ++entry, ++expected_entry) + { + if (entry->first != expected_entry->first) + { + return { path, "map keys" }; + } + Difference inner = firstDifference(entry->second, expected_entry->second, + path + "[\"" + entry->first + "\"]"); + if (!inner.what.empty()) + { + return inner; + } + } + break; + } + case LLSD::TypeArray: + { + if (actual.size() != expected.size()) + { + return { path, "array size" }; + } + for (LLSD::Integer index = 0; index < actual.size(); ++index) + { + Difference inner = firstDifference(actual[index], expected[index], + path + "[" + std::to_string(index) + "]"); + if (!inner.what.empty()) + { + return inner; + } + } + break; + } + default: + return { path, "type " + std::to_string(actual.type()) + ", which is not an LLSD type" }; + } + return { path, same ? std::string() : std::string("value") }; + } + } + + LLSDEqualsMatcher::LLSDEqualsMatcher(LLSD expected) + : mExpected(std::move(expected)) + { + } + + bool LLSDEqualsMatcher::match(const LLSD& actual) const + { + const Difference difference = firstDifference(actual, mExpected, ""); + mDifference = difference.what.empty() + ? std::string() + : (difference.path.empty() ? std::string("the top") : difference.path) + ": " + difference.what; + return mDifference.empty(); + } + + std::string LLSDEqualsMatcher::describe() const + { + std::ostringstream out; + out << "equals " << LLSDNotationStreamer(mExpected); + if (!mDifference.empty()) + { + out << " (they differ at " << mDifference << ")"; + } + return out.str(); + } + + std::ostream& operator<<(std::ostream& out, const MemoryView& memory) + { + // The first bytes are what tell two buffers apart at a glance. + constexpr size_t SHOWN = 32; + const U8* bytes = static_cast(memory.data); + out << memory.size << " bytes"; + if (bytes && memory.size) + { + out << ":"; + for (size_t i = 0; i < std::min(memory.size, SHOWN); ++i) + { + out << llformat(" %02x", bytes[i]); + } + if (memory.size > SHOWN) + { + out << " ..."; + } + } + return out; + } + + bool MemoryMatchesMatcher::match(const MemoryView& actual) const + { + if (actual.size != mExpected.size) + { + mDifference = "the sizes"; + return false; + } + if (actual.size == 0 || std::memcmp(actual.data, mExpected.data, actual.size) == 0) + { + mDifference.clear(); + return true; + } + const U8* a = static_cast(actual.data); + const U8* e = static_cast(mExpected.data); + size_t at = 0; + while (a[at] == e[at]) + { + ++at; + } + mDifference = llformat("byte %zu", at); + return false; + } + + std::string MemoryMatchesMatcher::describe() const + { + std::ostringstream out; + out << "matches " << mExpected; + if (!mDifference.empty()) + { + out << " (they differ at " << mDifference << ")"; + } + return out.str(); + } +} diff --git a/indra/test/altestmatchers.h b/indra/test/altestmatchers.h new file mode 100644 index 0000000000..74a7970f41 --- /dev/null +++ b/indra/test/altestmatchers.h @@ -0,0 +1,177 @@ +/** + * @file altestmatchers.h + * @brief Catch2 matchers for what LL's TUT helpers compared: floats to a number of + * bits, LLSD deeply, and memory byte for byte. + * + * $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$ + */ + +#ifndef AL_ALTESTMATCHERS_H +#define AL_ALTESTMATCHERS_H + +#include "llmath.h" +#include "llsd.h" + +#include +#include + +#include +#include +#include +#include + +namespace ll_test +{ + // ensure_approximately_equals: equal to frac_bits bits of the fraction, as + // is_approx_equal_fraction() decides, not to an epsilon. + template + class WithinBitsMatcher : public Catch::Matchers::MatcherBase + { + public: + WithinBitsMatcher(F expected, U32 frac_bits) + : mExpected(expected), mFracBits(frac_bits) + { + } + + bool match(const F& actual) const override + { + return is_approx_equal_fraction(actual, mExpected, mFracBits); + } + + std::string describe() const override + { + std::ostringstream out; + out << "is " << mExpected << " to " << mFracBits << " bits of the fraction"; + return out.str(); + } + + private: + F mExpected; + U32 mFracBits; + }; + + inline WithinBitsMatcher WithinBits(F32 expected, U32 frac_bits) + { + return WithinBitsMatcher(expected, frac_bits); + } + + inline WithinBitsMatcher WithinBits(F64 expected, U32 frac_bits) + { + return WithinBitsMatcher(expected, frac_bits); + } + + // ensure_distance: strictly less than distance from expected, worked in + // T as TUT worked it, so a value on the border fails, where Catch2's + // WithinAbs, worked in double, would pass it. + template + class WithinDistanceMatcher : public Catch::Matchers::MatcherBase + { + public: + WithinDistanceMatcher(T expected, T distance) + : mExpected(expected), mDistance(distance) + { + } + + bool match(const T& actual) const override + { + return mExpected - mDistance < actual && actual < mExpected + mDistance; + } + + std::string describe() const override + { + std::ostringstream out; + out << "is strictly between " << Catch::Detail::stringify(T(mExpected - mDistance)) << " and " + << Catch::Detail::stringify(T(mExpected + mDistance)); + return out.str(); + } + + private: + T mExpected; + T mDistance; + }; + + template + WithinDistanceMatcher WithinDistance(T expected, T distance) + { + return WithinDistanceMatcher(expected, distance); + } + + // ensure_equals on LLSD: the same type, and the same value all the way + // down, maps in key order and arrays in element order. A mismatch is + // described by where it is, as in ["key"][3] type, so a deep difference + // reads as more than "the two differ". + class LLSDEqualsMatcher : public Catch::Matchers::MatcherBase + { + public: + explicit LLSDEqualsMatcher(LLSD expected); + + bool match(const LLSD& actual) const override; + std::string describe() const override; + + private: + LLSD mExpected; + // What match() last found, for describe(), which Catch2 asks after. + mutable std::string mDifference; + }; + + inline LLSDEqualsMatcher LLSDEquals(LLSD expected) + { + return LLSDEqualsMatcher(std::move(expected)); + } + + // ensure_memory_matches: so many bytes at an address, compared as bytes. + // The bytes are not copied; they must outlive the assertion. + struct MemoryView + { + const void* data; + size_t size; + }; + + inline MemoryView Memory(const void* data, size_t size) + { + return MemoryView{ data, size }; + } + + std::ostream& operator<<(std::ostream& out, const MemoryView& memory); + + class MemoryMatchesMatcher : public Catch::Matchers::MatcherBase + { + public: + explicit MemoryMatchesMatcher(MemoryView expected) + : mExpected(expected) + { + } + + bool match(const MemoryView& actual) const override; + std::string describe() const override; + + private: + MemoryView mExpected; + // Where match() last found the two to part, for describe(). + mutable std::string mDifference; + }; + + inline MemoryMatchesMatcher MemoryMatches(const void* expected, size_t size) + { + return MemoryMatchesMatcher(MemoryView{ expected, size }); + } +} + +#endif // AL_ALTESTMATCHERS_H diff --git a/indra/test/altestname.cpp b/indra/test/altestname.cpp new file mode 100644 index 0000000000..b45748b0e5 --- /dev/null +++ b/indra/test/altestname.cpp @@ -0,0 +1,62 @@ +/** + * @file altestname.cpp + * @brief The name of the test case running. + * + * $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 "altestname.h" + +#include +#include +#include + +namespace ll_test +{ + namespace + { + std::string sCurrentTestName; + + class TestNameListener : public Catch::EventListenerBase + { + public: + using Catch::EventListenerBase::EventListenerBase; + + void testCaseStarting(Catch::TestCaseInfo const& info) override + { + sCurrentTestName = info.name; + } + + void testCaseEnded(Catch::TestCaseStats const&) override + { + sCurrentTestName.clear(); + } + }; + } + + const std::string& currentTestName() + { + return sCurrentTestName; + } +} + +CATCH_REGISTER_LISTENER(ll_test::TestNameListener) diff --git a/indra/test/altestname.h b/indra/test/altestname.h new file mode 100644 index 0000000000..14b701292b --- /dev/null +++ b/indra/test/altestname.h @@ -0,0 +1,38 @@ +/** + * @file altestname.h + * @brief The name of the test case running, where a TUT test asked for its own. + * + * $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$ + */ + +#ifndef AL_ALTESTNAME_H +#define AL_ALTESTNAME_H + +#include + +namespace ll_test +{ + // The running test case's name, as TUT's get_test_name() gave a test its + // own: to describe what it starts, or to name a file it writes. Empty + // outside a test case. Catch2's own way to ask is deprecated. + const std::string& currentTestName(); +} + +#endif // AL_ALTESTNAME_H diff --git a/indra/test/alteststateguard.cpp b/indra/test/alteststateguard.cpp new file mode 100644 index 0000000000..d2cc19e392 --- /dev/null +++ b/indra/test/alteststateguard.cpp @@ -0,0 +1,286 @@ +/** + * @file alteststateguard.cpp + * @brief Process state every test case must leave as it found it. + * + * $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 "alteststateguard.h" +#include "altestfatal.h" + +#include "llapp.h" +#include "llapr.h" +#include "llerrorcontrol.h" +#include "lltracethreadrecorder.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if LL_WINDOWS +#include "llwin32headers.h" +#include +#elif defined(__x86_64__) || defined(__i386__) +#include +#endif + +#if !LL_WINDOWS +extern char** environ; +#endif + +namespace ll_test +{ + namespace + { + struct Check + { + std::string name; + StateGuard::Describe describe; + StateGuard::Restore restore; + std::string last; + }; + + std::vector& checks() + { + static std::vector sChecks; + return sChecks; + } + + int sChanges = 0; + + std::string pointerText(const void* pointer) + { + std::ostringstream out; + out << pointer; + return out.str(); + } + + // A multi-line description is reported by its lines that went and + // came, not as two walls of text. + void reportDifference(std::ostream& out, const std::string& was, const std::string& now) + { + if (was.find('\n') == std::string::npos && now.find('\n') == std::string::npos) + { + out << " was: " << was << "\n now: " << now << "\n"; + return; + } + auto lines = [](const std::string& text) + { + std::set result; + std::istringstream in(text); + for (std::string line; std::getline(in, line);) + { + result.insert(line); + } + return result; + }; + const std::set before = lines(was); + const std::set after = lines(now); + for (const std::string& line : before) + { + if (!after.count(line)) + { + out << " gone: " << line << "\n"; + } + } + for (const std::string& line : after) + { + if (!before.count(line)) + { + out << " new: " << line << "\n"; + } + } + } + + std::string workingDirectory() + { + std::error_code error; + const std::filesystem::path path = std::filesystem::current_path(error); + return error ? "(unreadable: " + error.message() + ")" : path.string(); + } + + // Sorted, one variable a line, so a change reads as the lines it made. + std::string environment() + { + std::vector variables; +#if LL_WINDOWS + if (wchar_t* block = GetEnvironmentStringsW()) + { + for (const wchar_t* entry = block; *entry; entry += wcslen(entry) + 1) + { + variables.push_back(ll_convert_wide_to_string(entry)); + } + FreeEnvironmentStringsW(block); + } +#else + for (char** entry = environ; entry && *entry; ++entry) + { + variables.emplace_back(*entry); + } +#endif + std::sort(variables.begin(), variables.end()); + std::string text; + for (const std::string& variable : variables) + { + text += variable; + text += '\n'; + } + return text; + } + + // The control state only: the sticky exception flags change with any + // arithmetic and say nothing about what a test left behind. + std::string floatingPointControl() + { + std::ostringstream out; + out << std::hex; +#if LL_WINDOWS + out << "control 0x" << _controlfp(0, 0); +#elif defined(__aarch64__) + std::uint64_t fpcr; + __asm__ volatile("mrs %0, fpcr" : "=r"(fpcr)); + out << "FPCR 0x" << fpcr; +#elif defined(__x86_64__) || defined(__i386__) + out << "MXCSR 0x" << (_mm_getcsr() & ~0x3Fu); +#else + out << "unknown"; +#endif + return out.str(); + } + + std::string cLocale() + { + const char* locale = std::setlocale(LC_ALL, nullptr); + return locale ? locale : "(none)"; + } + + // A function's target, where it is a plain function, as an address; + // otherwise its type, which is what an override is told apart by. + std::string fatalFunction() + { + const LLError::FatalFunction function = LLError::getFatalFunction(); + if (!function) + { + return "(none)"; + } + using Plain = void (*)(const std::string&); + if (const Plain* plain = function.target()) + { + return pointerText(reinterpret_cast(*plain)); + } + return function.target_type().name(); + } + + // Catch2 hands every event to every listener; this one runs the + // guard's checks around each test case. + class StateGuardListener : public Catch::EventListenerBase + { + public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) override + { + StateGuard::snapshot(); + } + + void testCaseEnded(Catch::TestCaseStats const& stats) override + { + StateGuard::check(stats.testInfo->name); + } + }; + } + + void StateGuard::add(std::string name, Describe describe, Restore restore) + { + checks().push_back({ std::move(name), std::move(describe), std::move(restore), {} }); + } + + void StateGuard::addCommonChecks() + { + add("the working directory", workingDirectory); + add("the environment", environment); + add("the floating-point control state", floatingPointControl); + add("the C locale", cLocale); + add("the APR pool", [] { return pointerText(gAPRPoolp); }); + add("the LLTrace master recorder", [] { return pointerText(LLTrace::get_master_thread_recorder()); }); + add("the LLApp", [] { return pointerText(LLApp::instance()); }); + add("LLError's fatal function", fatalFunction); + } + + void StateGuard::snapshot() + { + for (Check& check : checks()) + { + check.last = check.describe(); + } + } + + void StateGuard::accept(const std::string& check_name) + { + for (Check& check : checks()) + { + if (check_name.empty() || check.name == check_name) + { + check.last = check.describe(); + } + } + } + + void StateGuard::check(const std::string& test_case) + { + for (Check& check : checks()) + { + std::string now = check.describe(); + if (now == check.last) + { + continue; + } + ++sChanges; + std::cerr << "State guard: test case \"" << test_case << "\" left " << check.name + << " changed\n"; + reportDifference(std::cerr, check.last, now); + if (check.restore) + { + check.restore(); + now = check.describe(); + } + check.last = std::move(now); + } + std::cerr << std::flush; + } + + int StateGuard::changes() + { + return sChanges; + } +} + +CATCH_REGISTER_LISTENER(ll_test::StateGuardListener) diff --git a/indra/test/alteststateguard.h b/indra/test/alteststateguard.h new file mode 100644 index 0000000000..59dc3a8fdb --- /dev/null +++ b/indra/test/alteststateguard.h @@ -0,0 +1,76 @@ +/** + * @file alteststateguard.h + * @brief Process state every test case must leave as it found it, checked after + * each case, so a leak fails the test that made it. + * + * $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$ + */ + +#ifndef AL_ALTESTSTATEGUARD_H +#define AL_ALTESTSTATEGUARD_H + +#include +#include + +namespace ll_test +{ + // Each check describes one piece of process state as text. The guard takes + // every description when the run starts and again after each test case; + // where one differs, it reports the case that changed it, then takes the + // new description as the one to compare with, so only that case is named. + // A check with a restore puts the state back after reporting, so a known + // leak cannot reach the next case. + // + // Today a binary runs one test file per process, so the guard catches what + // would otherwise only surface once the files share a process: in the + // combined run, as some later test failing for no reason of its own. + class StateGuard + { + public: + using Describe = std::function; + using Restore = std::function; + + static void add(std::string name, Describe describe, Restore restore = {}); + + // What every LL process has: the working directory, the environment, + // the floating-point control state, the C locale, the APR pool, the + // LLTrace master recorder, the LLApp and LLError's fatal function. + static void addCommonChecks(); + + // The listener's: before the first case, and after each. + static void snapshot(); + static void check(const std::string& test_case); + + // A process-wide fixture's, once it is made (see Teardown): what it + // changed on purpose -- a variable it set, the LLApp it made, the + // floating-point mode a GL driver chose -- becomes the state to + // compare with, so the test case that first asked for the fixture is + // not blamed for it. Names one check, as add() named it ("the + // environment"), or every check when empty. Whatever that case had + // itself changed of the same state is accepted with it. + static void accept(const std::string& check_name = {}); + + // How many times a case left something changed. The main fails the run + // if any did, since a listener cannot fail a test case of its own. + static int changes(); + }; +} + +#endif // AL_ALTESTSTATEGUARD_H diff --git a/indra/test/altestteardown.cpp b/indra/test/altestteardown.cpp new file mode 100644 index 0000000000..02f2a30e4e --- /dev/null +++ b/indra/test/altestteardown.cpp @@ -0,0 +1,76 @@ +/** + * @file altestteardown.cpp + * @brief The end of a test binary's process-wide fixtures, in order. + * + * $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 "altestteardown.h" + +#include +#include +#include + +namespace ll_test +{ + namespace + { + // Made on first use, so registering a step from any static + // initializer finds it ready. + std::vector>& steps() + { + static std::vector> sSteps; + return sSteps; + } + } + + void Teardown::add(std::function step) + { + steps().push_back(std::move(step)); + } + + int Teardown::runAll() + { + int failed = 0; + std::vector>& pending = steps(); + while (!pending.empty()) + { + std::function step = std::move(pending.back()); + pending.pop_back(); + try + { + step(); + } + catch (const std::exception& e) + { + ++failed; + std::cerr << "Teardown step threw: " << e.what() << std::endl; + } + catch (...) + { + ++failed; + std::cerr << "Teardown step threw a non-standard exception" << std::endl; + } + } + return failed; + } +} diff --git a/indra/test/altestteardown.h b/indra/test/altestteardown.h new file mode 100644 index 0000000000..00c999e7a8 --- /dev/null +++ b/indra/test/altestteardown.h @@ -0,0 +1,53 @@ +/** + * @file altestteardown.h + * @brief The end of a test binary's process-wide fixtures, in order, before the + * main lets LLError and APR go. + * + * $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$ + */ + +#ifndef AL_ALTESTTEARDOWN_H +#define AL_ALTESTTEARDOWN_H + +#include + +namespace ll_test +{ + // A process-wide fixture -- a GL context, a headless UI, a messaging + // system -- is made by its accessor the first time a test asks for it, + // and registers its end here. The main runs the ends after the last test + // case, newest first, while APR and LLError are still up. Left to static + // destruction, they would run in whatever order the linker chose, after + // the main had already let APR go. A fixture that changes process state + // on purpose as it is made tells the state guard (StateGuard::accept). + class Teardown + { + public: + static void add(std::function step); + + // The main's: runs each step once, newest first. A step that throws + // is reported and the rest still run; the count of those that threw + // is returned, and the main fails the run on any, as a check at + // static destruction ended a TUT process. + static int runAll(); + }; +} + +#endif // AL_ALTESTTEARDOWN_H diff --git a/indra/test/run_and_expect.cmake b/indra/test/run_and_expect.cmake new file mode 100644 index 0000000000..4b77915c08 --- /dev/null +++ b/indra/test/run_and_expect.cmake @@ -0,0 +1,36 @@ +# -*- cmake -*- +# +# Runs a test binary and checks what it did: its exit code, and what its output +# must and must not hold. For the Catch2 harness's own tests, whose exit codes +# and messages are its interface to CTest, which can check one of the two at a +# time. +# +# cmake -DEXE= -DARGS= -DEXPECT_EXIT= +# [-DEXPECT_OUTPUT=] [-DREJECT_OUTPUT=] -P run_and_expect.cmake +# +# ARGS separates its arguments with |, since CTest would split them on ;. +# EXPECT_EXIT nonzero takes any end but a clean exit, a crash included, whose +# code a signal decides on one platform and the system on another. + +string(REPLACE "|" ";" arguments "${ARGS}") +execute_process( + COMMAND ${EXE} ${arguments} + RESULT_VARIABLE exit_code + OUTPUT_VARIABLE output + ERROR_VARIABLE output +) +message("${output}") + +if(EXPECT_EXIT STREQUAL "nonzero") + if(exit_code STREQUAL "0") + message(FATAL_ERROR "Expected the run to fail, and it exited 0") + endif() +elseif(NOT exit_code STREQUAL EXPECT_EXIT) + message(FATAL_ERROR "Expected exit code ${EXPECT_EXIT}, got ${exit_code}") +endif() +if(DEFINED EXPECT_OUTPUT AND NOT output MATCHES "${EXPECT_OUTPUT}") + message(FATAL_ERROR "The output does not hold: ${EXPECT_OUTPUT}") +endif() +if(DEFINED REJECT_OUTPUT AND output MATCHES "${REJECT_OUTPUT}") + message(FATAL_ERROR "The output holds what it must not: ${REJECT_OUTPUT}") +endif() diff --git a/indra/test/tests/altestsupport_test.cpp b/indra/test/tests/altestsupport_test.cpp new file mode 100644 index 0000000000..d8c1283ad7 --- /dev/null +++ b/indra/test/tests/altestsupport_test.cpp @@ -0,0 +1,202 @@ +/** + * @file altestsupport_test.cpp + * @brief Tests of the Catch2 harness itself: its matchers, what an LL_ERRS + * does, the teardown, and the cases that CTest entries in + * test/CMakeLists.txt run to check the guard, the log replay and the exit + * codes from outside. + * + * $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 "altestfatal.h" +#include "altestmatchers.h" +#include "altestname.h" +#include "alteststateguard.h" +#include "altestteardown.h" + +#include "llerror.h" +#include "llsd.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + LLSD nested(F64 leaf) + { + LLSD sd; + sd["a"][2] = leaf; + sd["b"] = "text"; + return sd; + } + + void setLeak(const char* name) + { +#if LL_WINDOWS + _putenv_s(name, "1"); +#else + setenv(name, "1", 1); +#endif + } +} + +TEST_CASE("WithinBits holds to the bits asked for, and no further", "[altestsupport]") +{ + // 2^-20 apart: equal to 16 bits of the fraction, not to 24. (Not "near": + // windows.h defines that, and far, as nothing.) + const F32 one = 1.f; + const F32 just_off = 1.f + 1.f / (1 << 20); + CHECK_THAT(just_off, ll_test::WithinBits(one, 16)); + CHECK_FALSE(ll_test::WithinBits(one, 24).match(just_off)); + CHECK_FALSE(ll_test::WithinBits(one, 16).match(1.01f)); + CHECK_THAT(F64(just_off), ll_test::WithinBits(F64(one), 16)); +} + +TEST_CASE("WithinDistance holds strictly inside the distance, as ensure_distance did", "[altestsupport]") +{ + CHECK_THAT(11, ll_test::WithinDistance(12, 2)); + CHECK_FALSE(ll_test::WithinDistance(12, 2).match(10)); + CHECK_FALSE(ll_test::WithinDistance(12, 2).match(14)); + CHECK_THAT(0.5, ll_test::WithinDistance(1.0, 0.75)); + CHECK_FALSE(ll_test::WithinDistance(1.0, 0.5).match(0.5)); +} + +TEST_CASE("LLSDEquals compares all the way down and says where two differ", "[altestsupport]") +{ + CHECK_THAT(nested(1.5), ll_test::LLSDEquals(nested(1.5))); + + const ll_test::LLSDEqualsMatcher deep = ll_test::LLSDEquals(nested(1.5)); + CHECK_FALSE(deep.match(nested(2.5))); + CHECK_THAT(deep.describe(), Catch::Matchers::ContainsSubstring("[\"a\"][2]: value")); + + const ll_test::LLSDEqualsMatcher top = ll_test::LLSDEquals(LLSD("1")); + CHECK_FALSE(top.match(LLSD(1))); + CHECK_THAT(top.describe(), Catch::Matchers::ContainsSubstring("the top: type")); + + // LLDate's own == holds a NaN date equal to any. + const LLSD nan_date = LLDate(std::numeric_limits::quiet_NaN()); + CHECK_FALSE(ll_test::LLSDEquals(LLDate(0.0)).match(nan_date)); + CHECK_THAT(LLSD(LLDate(1.0)), ll_test::LLSDEquals(LLDate(1.0))); +} + +TEST_CASE("MemoryMatches compares byte for byte and says where two part", "[altestsupport]") +{ + const char same[] = "abcd"; + const char other[] = "abxd"; + CHECK_THAT(ll_test::Memory(same, 4), ll_test::MemoryMatches(same, 4)); + + const ll_test::MemoryMatchesMatcher differs = ll_test::MemoryMatches(other, 4); + CHECK_FALSE(differs.match(ll_test::Memory(same, 4))); + CHECK_THAT(differs.describe(), Catch::Matchers::ContainsSubstring("byte 2")); + + CHECK_FALSE(ll_test::MemoryMatches(same, 3).match(ll_test::Memory(same, 4))); +} + +TEST_CASE("currentTestName is the running test case's", "[altestsupport]") +{ + CHECK(ll_test::currentTestName() == "currentTestName is the running test case's"); +} + +TEST_CASE("An LL_ERRS throws FatalError, which a test can catch", "[altestsupport]") +{ + // Named, not written inside the assertion: Catch2 hands its argument on to + // a macro of its own after expanding it, and LL_ERRS expands to commas. + const auto deliberate = [] { LL_ERRS("altestsupport") << "deliberate" << LL_ENDL; }; + CHECK_THROWS_AS(deliberate(), ll_test::FatalError); + CHECK_THROWS_WITH(deliberate(), Catch::Matchers::ContainsSubstring("deliberate")); +} + +TEST_CASE("Teardown runs each step once, newest first", "[altestsupport]") +{ + std::vector order; + ll_test::Teardown::add([&order] { order.push_back(1); }); + ll_test::Teardown::add([&order] { order.push_back(2); }); + CHECK(ll_test::Teardown::runAll() == 0); + const std::vector newest_first{ 2, 1 }; + CHECK(order == newest_first); + CHECK(ll_test::Teardown::runAll() == 0); + CHECK(order.size() == 2); +} + +// Hidden: the CTest entries in test/CMakeLists.txt run these one at a time +// and check what the harness made of them. + +TEST_CASE("a test case that leaves the environment changed", "[.guard-leak]") +{ + setLeak("AL_TEST_SUPPORT_LEAK"); + SUCCEED(); +} + +TEST_CASE("a failing test case that logged", "[.replay-fail]") +{ + LL_INFOS("altestsupport") << "the line a failing case logged" << LL_ENDL; + FAIL("deliberately"); +} + +TEST_CASE("a passing test case that logged", "[.replay-pass]") +{ + LL_INFOS("altestsupport") << "the line a passing case logged" << LL_ENDL; + SUCCEED(); +} + +TEST_CASE("a test case that skips", "[.skip]") +{ + SKIP("deliberately"); +} + +TEST_CASE("a test case that leaves the environment changed, then skips", "[.skip-leak]") +{ + setLeak("AL_TEST_SUPPORT_SKIP_LEAK"); + SKIP("deliberately"); +} + +TEST_CASE("a test case whose fixture sets a variable and says so", "[.guard-accept]") +{ + setLeak("AL_TEST_SUPPORT_ACCEPTED"); + ll_test::StateGuard::accept("the environment"); + SUCCEED(); +} + +TEST_CASE("a test case whose fixture's end fails", "[.teardown-fails]") +{ + ll_test::Teardown::add([] { LL_ERRS("altestsupport") << "a teardown step that fails" << LL_ENDL; }); + SUCCEED(); +} + +TEST_CASE("a test case that logged, then crashes", "[.crash-replay]") +{ + LL_INFOS("altestsupport") << "the line a crashing case logged" << LL_ENDL; + // Through a volatile pointer, so no compiler can know it is null. + volatile int* volatile nowhere = nullptr; + *nowhere = 1; +} + +TEST_CASE("a test case whose thread hits an LL_ERRS", "[.thread-errs]") +{ + std::thread([] { LL_ERRS("altestsupport") << "an error off the main thread" << LL_ENDL; }).join(); +} diff --git a/indra/vcpkg-configuration.json b/indra/vcpkg-configuration.json index 3c915aec0a..322ea18b46 100644 --- a/indra/vcpkg-configuration.json +++ b/indra/vcpkg-configuration.json @@ -13,6 +13,7 @@ "apr", "boost-context", "boost-fiber", + "catch2", "cef-bin", "collada-dom", "corrosion", diff --git a/indra/vcpkg.json b/indra/vcpkg.json index 2b48f5dee4..c1c492cf93 100644 --- a/indra/vcpkg.json +++ b/indra/vcpkg.json @@ -268,6 +268,17 @@ } ] }, + "tests": { + "description": "Build the unit tests, on Catch2", + "dependencies": [ + { + "name": "catch2", + "features": [ + "thread-safe-assertions" + ] + } + ] + }, "tracy": { "description": "Build with Tracy profiler support", "dependencies": [ diff --git a/scripts/testing/tut2catch.py b/scripts/testing/tut2catch.py new file mode 100644 index 0000000000..49e78fff33 --- /dev/null +++ b/scripts/testing/tut2catch.py @@ -0,0 +1,3388 @@ +#!/usr/bin/env python3 +"""Converts TUT test files to idiomatic Catch2 v3, in place. + +Each file is read through clang's own AST of it, so what a call means is +decided by the types clang resolved, not by its text: + + structure every test of a group becomes + TEST_CASE_METHOD(, ": ", "[]") + with the group's data struct as the fixture, built fresh for + each case as TUT built it for each test; or a plain + TEST_CASE(": ", "[]") where the struct + holds nothing and nothing else names it, and the struct goes. + The name is, first, one --names gives; then the test's + set_test_name literal; then the first sentence of the + comment written above the test, where that is prose; else + #N. " #N" is added to a name another case of the file has + (ignoring case), unless the two are in different branches + of one #if, never compiled together. TUT ran a file's groups + in the byte order of their names, each group's tests by + number; Catch2 runs cases as declared, so a test declared + before one TUT ran ahead of it moves later in the file, + never earlier, to just after it. A move stays in its + namespace (a later block of it is the same scope) and #if + branch; where one cannot, the order is left (each group's + tests still put in numeric order where they can be) and the + file marked. The group's typedefs, its + test_group object and a class derived from test_group only + to name it go; a class derived from it that holds more (a + constructor's body, a destructor, members) stays, with its + object, marked. namespace tut, once nothing names tut:: and + no type production code befriends as tut:: is in it, becomes + an anonymous namespace, taking in one directly inside it. + assertions CHECK, which lets a case go on and so reports every failure + of a run; REQUIRE where going on could only crash or bury the + first failure: a condition that guards what follows (a + pointer, null or not; an optional or a smart pointer, tested + through its operator bool or compared with nullptr; a + position compared with end() or npos; a size, an emptiness, + a presence: has(), has_value(), found() and the like, + answered with a bool or an integer, not a vector's float + length()); a + condition that calls what makes it (a non-const member + function returning bool, or a call returning bool that fills + an argument passed by reference or address), where that + object or argument is a local declared without an + initializer, written by nothing since, that the test reads + after it (LLMatrix4a m; ensure(m.setInverse(n))), so + indeterminate had the call failed; a term of a split && chain whose variable a later + term of it leans on for its safety, dereferencing it or + indexing by it (TUT's ensure stopped the test at the first + term that failed; v.mV[VX] == 0 && v.mV[VY] == 0 stays two + CHECKs); in a loop or a lambda; and in a helper outside a + test's body. A plain compare of values stays CHECK. In a + destructor, where a throw would end the process, CHECK + always. With that macro (X below): + ensure(c) -> X(c); ensure(a && b && ...) -> X(a); X(b); ... + one assertion a term, so that each prints its operands + (whole, where a term after the first changes something, ++, + -- or an assignment, which TUT's && ran only past the terms + before it); + ensure(msg, c) -> UNSCOPED_INFO(msg) then X(c), or, where c + split, { INFO(msg); X(a); X(b); } so the message goes with + each; braced where the call was a lone statement of an if, + else or loop; ensure_not -> X_FALSE; ensure_equals -> X(a == + b), except where both values are LLSD (X_THAT with + ll_test::LLSDEquals, the deep compare TUT's overload made) + or LLURI (their asString()s, as TUT compared them); + ensure_not_equals -> X(a != b); ensure_distance -> + ll_test::WithinDistance, which, as TUT's did, fails a + value on the border and works in T, the type TUT's + template took; ensure_approximately_equals_range -> + WithinAbs, the actual, the expected and the delta narrowed + to F32 where they are not, as TUT's parameters narrowed them + (a NaN, which TUT's let pass, fails it); + ensure_approximately_equals -> ll_test::WithinBits, at the + precision of the overload TUT chose; ensure_contains, + _does_not_contain, _starts_with, _ends_with -> the string + matchers; ensure_memory_matches -> ll_test::MemoryMatches; + fail -> FAIL; skip -> SKIP; throw tut::failure(m) -> FAIL(m); + get_test_name() -> ll_test::currentTestName(); a catch of + tut::failure that rethrows -> ll_test::Failure. A condition + is written so that Catch2 decomposes it and prints its + operands: parentheses the test wrote around the whole go; a + literal compared on the left moves right; x == true is X(x) + and x == false X_FALSE(x); strcmp(a, b) == 0 compares + std::string_views. Operands are parenthesised where a macro + or an operator would split or rebind them; a condition + Catch2 cannot decompose (||, ?:) is parenthesised whole. + None of this reaches into a macro the test used: a && or + parentheses a macro made (WRAP(a && b), a && in a macro's + body) are not split or dropped, and the condition goes as + the test wrote it, macro and all. + includes lltut.h and tut.hpp give way to the Catch2 headers the file + now uses, and altestmatchers.h, altestname.h or + altestfailure.h where it uses them. + +Left as they are: #if guards (a test or call in a branch this host does not +compile is converted from its text where its types do not matter), code +under #if 0 and in comments, #include "*.cpp" lines, and every type the +production code names as a friend. + +What it cannot decide it leaves unconverted, marked // TM-CHECK: (or +/* TM-CHECK: */ inside a macro), and it exits 1 listing every mark; 2 if a +file could not be read at all, or the converter itself failed on one (its +traceback is that file's error, and the other files go on). It never +drops a test or an assertion: --mapping writes, as {"tests": [...], +"files": [...]}, one entry per test, with the TUT-side assertion calls in +its body and the Catch2 assertions now in it, and one per file, with the +sha256 of the TUT source it read (tut_sha256) and of the Catch2 text it +wrote, or would have under --dry-run (catch2_sha256), so that a file edited +since shows, and the TUT assertion calls outside every test's body, in +code not under #if 0 (tut_outside_calls), with the Catch2 assertions they +became (catch2_outside_expected), all of which scripts/testing/tut_parity.py +checks. + +Needs the build tree's compile_commands.json (CMake writes it; the presets +turn it on) and the compiler it names, which must be clang: the AST comes +from clang++ -fsyntax-only -Xclang -ast-dump=json with the file's own flags. + +A test header #included by a test file (llcorehttp's) is read through the +file that includes it. TUT2CATCH_DUMP= writes the file's own AST +declarations there, as JSON, to see what the converter saw. A clang AST of a viewer test runs to gigabytes of +JSON; only the file's own declarations are parsed, but each file still +takes from seconds to a minute, so --jobs helps. + + python3 -I scripts/testing/tut2catch.py indra/llmath/tests/v3math_test.cpp --mapping /tmp/map.json + python3 -I scripts/testing/tut2catch.py --dry-run --jobs 6 indra/*/tests/*_test.cpp +""" +import argparse +import bisect +import concurrent.futures +import hashlib +import json +import os +import re +import shlex +import subprocess +import sys +import threading +import traceback + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +DEFAULT_BUILD = "build-Darwin-ninja-os" +DEFAULT_CONFIG = "RelWithDebInfo" + +# TUT's free assertion functions, and the members of test_object a test calls. +TUT_FUNCTIONS = { + "ensure", "ensure_not", "ensure_equals", "ensure_not_equals", "ensure_distance", + "ensure_approximately_equals", "ensure_approximately_equals_range", "ensure_memory_matches", + "ensure_contains", "ensure_does_not_contain", "ensure_starts_with", "ensure_ends_with", + "ensure_errno", "fail", "skip", +} +TUT_MEMBERS = {"set_test_name", "get_test_name"} +# Whose conversion does not depend on the types of the arguments. +TYPE_FREE = {"ensure", "ensure_not", "fail", "skip"} +# What counts as an assertion on each side, for the parity count. +TUT_ASSERTING = TUT_FUNCTIONS +CATCH_ASSERTION = re.compile( + rb"(?:])(?:REQUIRE|REQUIRE_FALSE|REQUIRE_THAT|CHECK|CHECK_FALSE|CHECK_THAT|FAIL|FAIL_CHECK|SKIP)\s*\(") +TUT_INCLUDE = re.compile(rb'^[ \t]*#[ \t]*include[ \t]*[<"](?:[^>"]*/)?(?:lltut\.h|tut\.hpp)[>"][^\n]*\n?', re.M) + +IMPLICIT = {"ImplicitCastExpr", "MaterializeTemporaryExpr", "CXXBindTemporaryExpr", "ExprWithCleanups", + "ConstantExpr", "SubstNonTypeTemplateParmExpr", "FullExpr"} +BIN_PREC = { + ".*": 14, "->*": 14, "*": 13, "/": 13, "%": 13, "+": 12, "-": 12, "<<": 11, ">>": 11, "<=>": 10, + "<": 9, ">": 9, "<=": 9, ">=": 9, "==": 8, "!=": 8, "&": 7, "^": 6, "|": 5, "&&": 4, "||": 3, + "=": 2, "*=": 2, "/=": 2, "%=": 2, "+=": 2, "-=": 2, "<<=": 2, ">>=": 2, "&=": 2, "^=": 2, "|=": 2, + ",": 1, +} +STATEMENT_PARENTS = {"CompoundStmt", "CaseStmt", "DefaultStmt", "LabelStmt", "AttributedStmt"} +BODY_PARENTS = {"IfStmt", "ForStmt", "WhileStmt", "DoStmt", "CXXForRangeStmt"} +FUNCTION_KINDS = {"FunctionDecl", "CXXMethodDecl", "CXXConstructorDecl", "CXXDestructorDecl", + "CXXConversionDecl", "LambdaExpr"} +LOOP_KINDS = {"ForStmt", "WhileStmt", "DoStmt", "CXXForRangeStmt"} +# What a condition asks of an object when what follows relies on the answer: +# that it holds something, or how much. +GUARD_MEMBERS = {"size", "empty", "length", "count", "isDefined", "isUndefined", "has", "isMap", "isArray", + "notNull", "isNull", "has_value", "found"} +# What a member answering how much or whether answers with. +INTEGRAL = re.compile(r"(?:(?:unsigned|signed|short|long|int|char|bool|wchar_t|char8_t|char16_t|char32_t|" + r"__int128)\s*)+") +# A container's ends, which a position found in it is compared with: at one, +# there is nothing to read. +END_MEMBERS = {"end", "cend", "rend", "crend"} +# The arithmetic types: a constant of one is no state a later term relies on. +ARITHMETIC = re.compile(r"(?:(?:unsigned|signed|short|long|int|char|float|double|bool|wchar_t|char8_t|char16_t|" + r"char32_t|size_t|F32|F64|S8|U8|S16|U16|S32|U32|S64|U64)\s*)+") +LITERAL_KINDS = {"IntegerLiteral", "FloatingLiteral", "CXXBoolLiteralExpr", "CharacterLiteral", "StringLiteral", + "CXXNullPtrLiteralExpr", "GNUNullExpr", "FixedPointLiteral"} +# A comment that names nothing: a separator, or a note to whoever reads on. +NOT_A_NAME = re.compile(r"^(?:[-=*/#~_ ]*|(?:TODO|FIXME|XXX|HACK|NB|NOTE|BUG)\b.*)$", re.I) + + +class ConvertError(Exception): + pass + + +# -------------------------------------------------------------------------- +# The source text +# -------------------------------------------------------------------------- +def mask_source(data): + """The text with comments blanked (newlines kept) and the insides of string + and character literals replaced, so a search of it finds only code. Every + offset means what it meant.""" + out = bytearray(data) + n = len(data) + i = 0 + while i < n: + c = data[i] + if c == 0x2F and i + 1 < n and data[i + 1] == 0x2F: # // + j = i + while j < n and data[j] != 0x0A: + if data[j] == 0x5C and j + 1 < n and data[j + 1] == 0x0A: + j += 2 + continue + j += 1 + for k in range(i, j): + if out[k] != 0x0A: + out[k] = 0x20 + i = j + elif c == 0x2F and i + 1 < n and data[i + 1] == 0x2A: # /* + j = data.find(b"*/", i + 2) + j = n if j < 0 else j + 2 + for k in range(i, j): + if out[k] != 0x0A: + out[k] = 0x20 + i = j + elif c == 0x22: # " + # A raw string: R"delim( ... )delim", after an optional prefix. + k = i - 1 + if k >= 0 and data[k] == ord("R"): + start = k + while start > 0 and chr(data[start - 1]) in "u8UL": + start -= 1 + if start == 0 or not (chr(data[start - 1]).isalnum() or data[start - 1] == 0x5F): + paren = data.find(b"(", i) + delim = data[i + 1:paren] + end = data.find(b")" + delim + b'"', paren) + if paren >= 0 and end >= 0 and len(delim) <= 16: + for m in range(i + 1, end + len(delim) + 1): + if out[m] != 0x0A: + out[m] = 0x5F + i = end + len(delim) + 2 + continue + j = i + 1 + while j < n and data[j] != 0x22 and data[j] != 0x0A: + if data[j] == 0x5C: + j += 1 + j += 1 + for k in range(i + 1, min(j, n)): + out[k] = 0x5F + i = j + 1 + elif c == 0x27: # ' + # A digit separator, inside a number, is no literal. + k = i - 1 + while k >= 0 and (chr(data[k]).isalnum() or data[k] in (0x5F, 0x27, 0x2E)): + k -= 1 + if k + 1 < i and chr(data[k + 1]).isdigit(): + i += 1 + continue + j = i + 1 + while j < n and data[j] != 0x27 and data[j] != 0x0A: + if data[j] == 0x5C: + j += 1 + j += 1 + for k in range(i + 1, min(j, n)): + out[k] = 0x5F + i = j + 1 + else: + i += 1 + return bytes(out) + + +def c_unescape(body): + """The value of the inside of a narrow string literal.""" + out = [] + i = 0 + simple = {"n": "\n", "t": "\t", "r": "\r", "0": "\0", "\\": "\\", '"': '"', "'": "'", "a": "\a", + "b": "\b", "f": "\f", "v": "\v", "?": "?"} + while i < len(body): + ch = body[i] + if ch == "\\" and i + 1 < len(body): + nxt = body[i + 1] + if nxt == "x": + m = re.match(r"[0-9A-Fa-f]+", body[i + 2:]) + out.append(chr(int(m.group(0), 16)) if m else "x") + i += 2 + (len(m.group(0)) if m else 0) + continue + if nxt in "01234567": + m = re.match(r"[0-7]{1,3}", body[i + 1:]) + out.append(chr(int(m.group(0), 8))) + i += 1 + len(m.group(0)) + continue + out.append(simple.get(nxt, nxt)) + i += 2 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def c_escape(text): + return (text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\t", "\\t")) + + +class Source: + def __init__(self, path): + self.path = path + with open(path, "rb") as f: + self.data = f.read() + self.masked = mask_source(self.data) + self.line_starts = [0] + [m.end() for m in re.finditer(rb"\n", self.data)] + self.directives = [] # (start, end, keyword, rest) of each preprocessor line + for m in re.finditer(rb"^[ \t]*#[ \t]*(\w+)", self.masked, re.M): + end = m.end() + while True: + nl = self.masked.find(b"\n", end) + if nl < 0: + nl = len(self.masked) + break + if self.masked[nl - 1:nl] == b"\\": + end = nl + 1 + continue + break + self.directives.append((m.start(), nl, m.group(1).decode(), self.masked[m.end():nl].decode("latin-1"))) + self.directive_starts = [d[0] for d in self.directives] + self.regions = [] # (start, end, "dead" | "inactive") of conditional branches + + def line_of(self, off): + return bisect.bisect_right(self.line_starts, off) + + def line_start(self, off): + return self.line_starts[self.line_of(off) - 1] + + def line_end(self, off): + nl = self.data.find(b"\n", off) + return len(self.data) if nl < 0 else nl + + def indent_at(self, off): + start = self.line_start(off) + m = re.match(rb"[ \t]*", self.data[start:]) + return self.data[start:start + m.end()] + + def starts_line(self, off): + return self.data[self.line_start(off):off].strip() == b"" + + def directive_at(self, off): + i = bisect.bisect_right(self.directive_starts, off) - 1 + if i >= 0 and self.directives[i][0] <= off < self.directives[i][1]: + return self.directives[i] + return None + + def skip_space(self, off): + while off < len(self.masked) and self.masked[off] in b" \t\r\n": + off += 1 + return off + + def skip_space_back(self, off): + while off > 0 and self.masked[off - 1] in b" \t\r\n": + off -= 1 + return off + + def matching(self, off): + """The offset of the bracket that closes the one at off.""" + pairs = {ord("("): ord(")"), ord("["): ord("]"), ord("{"): ord("}")} + stack = [] + i = off + while i < len(self.masked): + c = self.masked[i] + if c in pairs: + stack.append(pairs[c]) + elif c in (0x29, 0x5D, 0x7D): + if not stack or stack.pop() != c: + return -1 + if not stack: + return i + i += 1 + return -1 + + def conditionals_at(self, off): + """The #if chains around off, outermost first: each as (where its #if + is, which of its branches holds off, counting from 0).""" + stack = [] + for start, end, kw, _ in self.directives: + if start > off: + break + if kw in ("if", "ifdef", "ifndef"): + stack.append((start, 0)) + elif kw in ("elif", "else", "elifdef", "elifndef") and stack: + stack[-1] = (stack[-1][0], stack[-1][1] + 1) + elif kw == "endif" and stack: + stack.pop() + return tuple(stack) + + def state(self, off): + """dead (under #if 0), inactive (a branch this host does not compile) or active.""" + result = "active" + for start, end, what in self.regions: + if start <= off < end: + if what == "dead": + return "dead" + result = "inactive" + return result + + def find_conditionals(self, active_lines): + """Each #if branch's extent, and whether it is dead or not compiled here.""" + stack = [] + branches = [] # (start, end, keyword, condition, nested ranges) + for start, end, kw, rest in self.directives: + if kw in ("if", "ifdef", "ifndef"): + stack.append({"branches": [], "open": (end, kw, rest.strip()), "dead_before": False}) + elif kw in ("elif", "elifdef", "elifndef", "else", "endif") and stack: + top = stack[-1] + bstart, bkw, brest = top["open"] + top["branches"].append((bstart, start, bkw, brest)) + if kw == "endif": + stack.pop() + branches.extend(top["branches"]) + else: + top["open"] = (end, kw, rest.strip()) + for bstart, bend, kw, cond in branches: + if kw in ("if", "elif") and re.fullmatch(r"\(?\s*(0|false)\s*\)?", cond): + self.regions.append((bstart, bend, "dead")) + continue + # A branch is compiled if a line of its own code reached the + # preprocessor's output. Lines of nested branches are theirs. + nested = [(s, e) for s, e, _, _ in branches if bstart < s and e <= bend] + has_code = False + compiled = False + for line in range(self.line_of(bstart) + 1, self.line_of(max(bstart, bend - 1)) + 1): + ls = self.line_starts[line - 1] + le = self.line_end(ls) + if any(s <= ls < e for s, e in nested) or self.directive_at(ls): + continue + if self.masked[ls:le].strip(): + has_code = True + if line in active_lines: + compiled = True + break + if has_code and not compiled: + self.regions.append((bstart, bend, "inactive")) + + +# -------------------------------------------------------------------------- +# The compile database and clang +# -------------------------------------------------------------------------- +class CompileDatabase: + """Each file's compile command in one configuration. A file compiled by + more than one target -- a TUT test and, mid-migration, its Catch2 + conversion -- is read as its TUT target compiles it, the one with TUT's + headers on its path.""" + + def __init__(self, build_dir, config): + path = os.path.join(build_dir, "compile_commands.json") + if not os.path.exists(path): + raise ConvertError(f"no {path}: configure the build tree first") + with open(path) as f: + entries = json.load(f) + self.entries = {} + tut = os.path.join("externals", "tut") + " " + for e in entries: + key = os.path.normpath(os.path.join(e["directory"], e["file"])) + command = e.get("command") or shlex.join(e["arguments"]) + # Ranked on the raw text: splitting thousands of commands costs + # seconds, and one is all a file needs. + rank = (f'-DCMAKE_INTDIR=\\"{config}\\"' in command or f'-DCMAKE_INTDIR="{config}"' in command, + tut in command or command.rstrip().endswith(tut.strip())) + have = self.entries.get(key) + if have is None or rank > have[2]: + self.entries[key] = (e["directory"], command, rank) + + def command(self, path): + """(directory, clang arguments, the file compiled): a header of tests, + such as llcorehttp's, is read through the file that includes it.""" + path = os.path.normpath(path) + entry = self.entries.get(path) + compiled = path + if entry is None: + folder = os.path.dirname(path) + include = re.compile(r'^\s*#\s*include\s*"(?:[^"]*/)?' + re.escape(os.path.basename(path)) + '"', re.M) + for candidate in sorted(self.entries): + if os.path.dirname(candidate) != folder or candidate == path: + continue + try: + with open(candidate, encoding="utf-8", errors="replace") as f: + if include.search(f.read()): + entry, compiled = self.entries[candidate], candidate + break + except OSError: + continue + if entry is None: + raise ConvertError(f"{path} is in no compile command: is it built by any target?") + directory, args = entry[0], shlex.split(entry[1]) + out = ["clang++"] + skip = False + for a in args[1:]: + if skip: + skip = False + continue + if a in ("-o", "-c", "-MF", "-MT", "-MQ"): + skip = True + continue + if a in ("-MD", "-MMD", "-fcolor-diagnostics") or a.startswith("-fdiagnostics-color"): + continue + if os.path.normpath(os.path.join(directory, a)) == compiled: + continue + out.append(a) + return directory, out, compiled + + +def split_ast_items(stream): + """The AST's top-level declarations, as JSON bytes, without parsing the + whole dump: they are the items of the translation unit's "inner" array, + and clang writes each between lines indented four spaces.""" + sep = b"\n },\n {\n" + buf = b"" + started = False + while True: + chunk = stream.read(1 << 24) + if not chunk: + break + buf += chunk + if not started: + k = buf.find(b' "inner": [\n {\n') + if k < 0: + continue + buf = buf[k + len(b' "inner": [\n'):] + started = True + pos = 0 + while True: + k = buf.find(sep, pos) + if k < 0: + break + yield buf[pos:k + 6] + pos = k + 7 + buf = buf[pos:] + if started: + k = buf.rfind(b"\n }\n") + if k >= 0: + yield buf[:k + 6] + + +FILE_ATTR = re.compile(rb'"file": "((?:[^"\\]|\\.)*)"') + + +def last_file(text, current): + """The file of the last location written in text: clang writes a + location's file only when it changes, so this is the file in force after + it. An includedFrom's file is not a location's.""" + end = len(text) + while True: + i = text.rfind(b'"file": "', 0, end) + if i < 0: + return current + j = i + while j > 0 and text[j - 1] in b" \t\r\n": + j -= 1 + if text.endswith(b'"includedFrom": {', 0, j): + end = i + continue + return json.loads(b'"' + FILE_ATTR.match(text, i).group(1) + b'"') + + +def item_files(text, current): + """The files of a top-level item's own location: spelling and expansion.""" + i = text.find(b'"loc": {') + if i < 0: + return set() + j = text.find(b'"range": ', i) + head = text[i + len(b'"loc": '):j if j > 0 else i + 4096].decode("utf-8", "replace") + loc, _ = json.JSONDecoder().raw_decode(head) + files = set() + state = [current] + + def visit(obj): + if "offset" in obj: + state[0] = obj.get("file", state[0]) + files.add(state[0]) + for key in ("spellingLoc", "expansionLoc"): + if key in obj: + visit(obj[key]) + visit(loc) + return files + + +def annotate(obj, state): + """Gives every location in obj its file, as "_f", walking in the order + clang wrote them.""" + if isinstance(obj, dict): + if "offset" in obj: + state[0] = obj.get("file", state[0]) + obj["_f"] = os.path.normpath(state[0]) if state[0] else None + for key, value in obj.items(): + if key == "includedFrom": + continue + if isinstance(value, (dict, list)): + annotate(value, state) + elif isinstance(obj, list): + for value in obj: + annotate(value, state) + + +TUT_DECL = re.compile( + rb'"id": "(0x[0-9a-f]+)",\s*"kind": "(?:FunctionDecl|CXXMethodDecl|FunctionTemplateDecl)",' + rb'(?:(?!"kind": ).)*?"name": "(\w+)"', re.S) + + +def is_tut_header(path): + path = os.path.normpath(path) + return path.endswith(os.path.join("test", "lltut.h")) or (os.sep + os.path.join("tut", "tut")) in path + + +def run_clang(db, path): + """The main file's top-level declarations, parsed, the ids of TUT's own + functions, and the lines of the main file the preprocessor kept.""" + directory, args, compiled = db.command(path) + main = os.path.normpath(path) + preprocess = subprocess.Popen(args + ["-E", compiled], cwd=directory, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + dump = subprocess.Popen(args + ["-fsyntax-only", "-Xclang", "-ast-dump=json", compiled], cwd=directory, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + errors = [] + reader = threading.Thread(target=lambda: errors.append(dump.stderr.read())) + reader.start() + items = [] + tut_ids = {} + current = None + for text in split_ast_items(dump.stdout): + files = {os.path.normpath(f) for f in item_files(text, current) if f} + if main in files: + items.append((current, json.loads(text))) + elif any(is_tut_header(f) for f in files): + for m in TUT_DECL.finditer(text): + name = m.group(2).decode() + if name in TUT_FUNCTIONS or name in TUT_MEMBERS: + tut_ids[m.group(1).decode()] = name + current = last_file(text, current) + dump.wait() + reader.join() + if dump.returncode != 0: + stderr = b"".join(errors).decode("utf-8", "replace") + preprocess.kill() + preprocess.wait() + raise ConvertError(f"clang could not parse {compiled}:\n{stderr[-4000:]}") + active_lines = set() + marker = re.compile(rb'^# (\d+) "((?:[^"\\]|\\.)*)"') + line = 0 + in_main = False + for raw in preprocess.stdout.read().split(b"\n"): + m = marker.match(raw) + if m: + line = int(m.group(1)) + in_main = os.path.normpath(os.path.join(directory, m.group(2).decode("utf-8", "replace"))) == main + continue + if in_main and raw.strip(): + active_lines.add(line) + line += 1 + preprocess.wait() + for current_file, item in items: + annotate(item, [current_file]) + return [item for _, item in items], tut_ids, active_lines + + +def demangle(names): + if not names: + return {} + for tool in (["xcrun", "c++filt"], ["c++filt"], ["llvm-cxxfilt"]): + try: + out = subprocess.run(tool, input="\n".join(names), capture_output=True, text=True, check=True) + return dict(zip(names, out.stdout.splitlines())) + except (OSError, subprocess.CalledProcessError): + continue + raise ConvertError("no c++filt to demangle with") + + +# -------------------------------------------------------------------------- +# Reading the AST +# -------------------------------------------------------------------------- +def skip_implicit(node): + while True: + kind = node.get("kind") + inner = [c for c in node.get("inner", []) if isinstance(c, dict) and "kind" in c] + if kind in IMPLICIT and inner: + node = inner[0] + continue + if kind in ("CXXConstructExpr", "CXXMemberCallExpr") and len(inner) == 1 and \ + node.get("range") and inner[0].get("range") and \ + loc_key(node["range"]["begin"]) == loc_key(inner[0]["range"]["begin"]) and \ + loc_key(node["range"]["end"]) == loc_key(inner[0]["range"]["end"]): + node = inner[0] + continue + return node + + +def loc_key(loc): + if "spellingLoc" in loc: + loc = loc["spellingLoc"] + return (loc.get("_f"), loc.get("offset")) + + +def value_type(node): + t = node.get("type", {}) + text = t.get("desugaredQualType") or t.get("qualType") or "" + text = re.sub(r"\b(const|volatile)\b", "", text).replace("&", "") + return re.sub(r"\s+", " ", text).strip() + + +def precedence(node): + node = skip_implicit(node) + kind = node.get("kind") + if kind in ("BinaryOperator", "CompoundAssignOperator"): + return BIN_PREC.get(node.get("opcode"), 16) + if kind in ("ConditionalOperator", "BinaryConditionalOperator", "CXXThrowExpr"): + return 2 + if kind == "CXXRewrittenBinaryOperator": + return 8 + if kind == "CXXOperatorCallExpr": + inner = [c for c in node.get("inner", []) if "kind" in c] + callee = skip_implicit(inner[0]) if inner else {} + name = callee.get("referencedDecl", {}).get("name", "") + op = name[len("operator"):] if name.startswith("operator") else "" + if op in ("()", "[]", "->", "++", "--"): + return 16 + if len(inner) == 2: + return 15 + return BIN_PREC.get(op, 16) + if kind in ("UnaryOperator", "CStyleCastExpr", "CXXNewExpr", "CXXDeleteExpr", "UnaryExprOrTypeTraitExpr"): + return 15 + return 16 + + +def headers_for(name, kind): + """What a converted call needs included, beyond the test macros.""" + if kind == "equals-llsd" or name in ("ensure_approximately_equals", "ensure_memory_matches", "ensure_distance"): + return {'"altestmatchers.h"'} + if name == "ensure_approximately_equals_range": + return {""} + if name in ("ensure_contains", "ensure_does_not_contain", "ensure_starts_with", "ensure_ends_with"): + return {""} + return set() + + +def decomposable(node): + """Whether REQUIRE(node) compiles as Catch2 decomposes it: not &&, ||, + ?:, an assignment or a comma at the top, and one comparison at most.""" + node = skip_implicit(node) + p = precedence(node) + if p <= 4: + return False + if 5 <= p <= 9: + inner = [c for c in node.get("inner", []) if "kind" in c] + if node.get("kind") == "CXXOperatorCallExpr": + inner = inner[1:] + if inner and precedence(inner[0]) <= 9: + return False + return True + + +def unparenthesised(node): + """node, without the parentheses a test wrote around a whole Catch2 can + decompose, and without the implicit nodes around that.""" + node = skip_implicit(node) + while node.get("kind") == "ParenExpr": + inner = [c for c in node.get("inner", []) if "kind" in c] + if not inner or not decomposable(inner[0]): + break + node = skip_implicit(inner[0]) + return node + + +def operands(node): + """A comparison's two operands, implicit nodes and all, or None.""" + node = skip_implicit(node) + inner = [c for c in node.get("inner", []) if "kind" in c] + if node.get("kind") == "BinaryOperator" and len(inner) == 2: + return inner + return None + + +def is_literal(node): + node = skip_implicit(node) + if node.get("kind") == "UnaryOperator" and node.get("opcode") in ("-", "+"): + inner = [c for c in node.get("inner", []) if "kind" in c] + return bool(inner) and is_literal(inner[0]) + if node.get("kind") == "DeclRefExpr" and node.get("referencedDecl", {}).get("kind") == "EnumConstantDecl": + return True + return node.get("kind") in LITERAL_KINDS + + +def strcmp_args(node): + """The two arguments of a strcmp() call, or None.""" + node = skip_implicit(node) + if node.get("kind") != "CallExpr": + return None + inner = [c for c in node.get("inner", []) if "kind" in c] + if len(inner) != 3: + return None + callee = inner[0] + while callee.get("kind") in ("ImplicitCastExpr", "ParenExpr") and callee.get("inner"): + callee = callee["inner"][0] + if callee.get("kind") != "DeclRefExpr" or callee.get("referencedDecl", {}).get("name") not in \ + ("strcmp", "__builtin_strcmp"): + return None + return inner[1:] + + +def value_args(name, args): + """The arguments a TUT call asserts about, without its message.""" + if name in ("ensure", "ensure_not"): + return args[-1:] + if name in ("ensure_equals", "ensure_not_equals"): + return args[-2:] + if name in ("ensure_distance", "ensure_approximately_equals", "ensure_approximately_equals_range"): + return args[-3:] + if name in ("ensure_contains", "ensure_does_not_contain", "ensure_starts_with", "ensure_ends_with"): + return args[1:] + if name == "ensure_memory_matches": + return args[-4:] + return [] + + +def name_from_comment(text): + """A case's name from the comment above its test: the first sentence of + the comment's first paragraph, or None where the comment is not prose.""" + lines = [] + for raw in text.splitlines(): + s = raw.strip() + s = re.sub(r"^(?://+!?|/\*+!?|\*+(?!/))\s?", "", s) + s = re.sub(r"\*+/\s*$", "", s).strip() + s = re.sub(r"^@(?:brief|test|note)\s+", "", s) + lines.append(s) + paragraph = [] + for s in lines: + if not s: + if paragraph: + break + continue + paragraph.append(s) + text = re.sub(r"\s+", " ", " ".join(paragraph)).strip() + if not text or NOT_A_NAME.match(text): + return None + # Commented-out code, or a test's number, names nothing. + if re.search(r";\s*$|[{}]|==|->|::\w+\(|^\w+\s*\(.*\)\s*;?$|\btest\s*<\s*\d+\s*>|^#", text): + return None + if len(re.findall(r"[A-Za-z]{2,}", text)) < 2: + return None + # The first sentence: a full stop before a space or the end, not the one + # in "e.g." or after an initial. + sentence = text + for m in re.finditer(r"[.!?](?=\s|$)", text): + if re.search(r"(?:\be\.g|\bi\.e|\betc|\bvs|\bcf|\b[A-Za-z])$", text[:m.start()]): + continue + sentence = text[:m.start() + (1 if text[m.start()] in "!?" else 0)] + break + sentence = sentence.rstrip(" .:;,").strip() + if len(sentence) > 100: + cut = sentence[:100] + for sep in ("; ", ": ", " -- ", ", "): + i = cut.rfind(sep) + if i >= 40: + sentence = cut[:i] + break + else: + sentence = cut.rsplit(" ", 1)[0] + sentence = sentence.rstrip(" .:;,") + return sentence or None + + +def is_null_message(node): + node = skip_implicit(node) + if node.get("kind") in ("GNUNullExpr", "CXXNullPtrLiteralExpr"): + return True + if node.get("kind") == "IntegerLiteral" and node.get("value") == "0": + return True + if node.get("kind") == "StringLiteral" and node.get("value") == '""': + return True + return False + + +class Location: + """Where a node's text is in the main file, if it is. how says what the + text is: file (plain), macro-arg (written in a macro's argument, so in + place), macro-body (written in a #define of the file) or foreign (made + by a macro defined elsewhere: only where it was used is known).""" + + def __init__(self, source, loc, main): + self.offset = None + self.length = 0 + self.how = None + self.expansion = None # (offset, length) where a macro was used + if not loc: + return + if "spellingLoc" in loc: + spelling, expansion = loc["spellingLoc"], loc["expansionLoc"] + if expansion.get("_f") == main and "offset" in expansion: + self.expansion = (expansion["offset"], expansion.get("tokLen", 0)) + if spelling.get("_f") == main and "offset" in spelling: + self.offset, self.length = spelling["offset"], spelling.get("tokLen", 0) + self.how = "macro-body" if source.directive_at(self.offset) else "macro-arg" + elif self.expansion: + self.offset, self.length = self.expansion + self.how = "foreign" + return + if loc.get("_f") == main and "offset" in loc: + self.offset, self.length = loc["offset"], loc.get("tokLen", 0) + self.how = "file" + + def in_text(self): + """Where the node starts in the text as written: a macro used in the + file counts from where it was used.""" + if self.how == "macro-body" and self.expansion: + return self.expansion[0] + return self.offset + + +# -------------------------------------------------------------------------- +# What the file holds +# -------------------------------------------------------------------------- +class Test: + def __init__(self): + self.n = None + self.data = None # the fixture, as clang names it + self.fixture = None # as the converted file spells it + self.group = None + self.start = None # template<> ... + self.header_end = None # after test() + self.body_start = None # { + self.body_end = None # after } + self.namespaces = () + self.names = [] # set_test_name calls: (call, literal text or None, top-level?) + self.inactive = False + self.macro = False + self.node = None + self.catch2_name = None + self.tut_name = "" + + +class Group: + def __init__(self, name, data, limit): + self.name = name + self.data = data + self.limit = limit + self.tests = [] + + +class Plan: + """A piece of the file replaced: [start, end) by what text() renders.""" + + def __init__(self, start, end, render): + self.start = start + self.end = end + self.render = render + + def text(self, conv): + return self.render(conv, self) + + +class Converter: + def __init__(self, source, items, tut_ids, active_lines, demangled): + self.src = source + self.main = os.path.normpath(source.path) + self.items = items + self.tut_ids = tut_ids + self.demangled = demangled + self.plans = [] + self.checks = [] # (offset, why) + self.tests = [] + self.groups = {} # data -> Group + self.typedefs = {} # name -> data type of a test_object / test_group typedef + self.removed_typedef_ids = set() + self.removals = [] # (start, end) + self.calls = {} # offset -> [(kind info)] + self.handled_nodes = set() + self.headers = {""} + self.notes = [] + self.counted = {} # offset of a TUT assertion call -> True + self.derived_groups = {} # class derived from test_group -> (data, limit, name) + self.removed_spans = [] + self.slot_at = {} # chunk start -> the plan that puts tests there + self.emitted = {} # offset of a converted call -> the Catch2 assertions it became + self.records = {} # qualified name of a class -> its CXXRecordDecl + self.name_overrides = {} # (group, n) -> a case's name, from --names + self.friend_names = set() # tut:: types production code befriends + self.tut_namespaces = [] # NamespaceDecl nodes named tut, with their parents + self.body_reads = {} # id of a test's body -> (offset, declaration) of what it reads + self.body_bare = {} # id of a test's body -> the variables it declares without an initializer + self.body_writes = {} # id of a test's body -> (offset, variable) of what it writes + self.live = None # the masked text with code under #if 0 blanked too + source.find_conditionals(active_lines) + + # ---- helpers ------------------------------------------------------- + def check(self, offset, why): + self.checks.append((offset, why)) + + def text(self, start, end): + return self.src.data[start:end].decode("utf-8", "surrogateescape") + + def loc(self, loc): + return Location(self.src, loc, self.main) + + def node_range(self, node): + """[start, end) of a node in the main file, or None.""" + rng = node.get("range") + if not rng: + return None + begin, end = self.loc(rng.get("begin")), self.loc(rng.get("end")) + if begin.offset is None or end.offset is None: + return None + return begin.offset, end.offset + end.length, begin.how, end.how + + # ---- the walk ------------------------------------------------------ + def analyse(self): + for item in self.items: + self.walk(item, []) + + def walk(self, node, parents): + kind = node.get("kind") + if kind == "CXXMethodDecl" and node.get("name") == "test" and "test_object" in node.get("mangledName", ""): + self.on_test(node, parents) + elif kind in ("TypedefDecl", "TypeAliasDecl"): + self.on_typedef(node, parents) + elif kind == "VarDecl": + self.on_var(node, parents) + elif kind == "CXXRecordDecl": + if node.get("completeDefinition") and node.get("name"): + spaces = [p.get("name") or "(anonymous namespace)" for p in parents if p.get("kind") == "NamespaceDecl"] + self.records.setdefault("::".join(spaces + [node["name"]]), node) + self.on_record(node, parents) + elif kind == "NamespaceDecl" and node.get("name") == "tut": + self.tut_namespaces.append((node, parents)) + elif kind in ("CallExpr", "CXXMemberCallExpr"): + self.on_call(node, parents) + elif kind == "CXXThrowExpr": + self.on_throw(node, parents) + elif kind == "CXXCatchStmt": + self.on_catch(node, parents) + elif kind in ("DeclRefExpr", "MemberExpr", "UnresolvedLookupExpr") and id(node) not in self.handled_nodes: + self.on_reference(node, parents) + for child in node.get("inner", []): + if isinstance(child, dict): + self.walk(child, parents + [node]) + + def namespaces_of(self, parents): + return tuple(p.get("name", "") for p in parents if p.get("kind") == "NamespaceDecl") + + # ---- tests --------------------------------------------------------- + def on_test(self, node, parents): + body = next((c for c in node.get("inner", []) if c.get("kind") == "CompoundStmt"), None) + if body is None: + return + n = next((c.get("value") for c in node.get("inner", []) if c.get("kind") == "TemplateArgument"), None) + test = Test() + test.node = node + test.n = int(n) if n is not None else None + test.mangled = node.get("mangledName") + rng = self.node_range(node) + brng = self.node_range(body) + if rng is None or brng is None or rng[2] != "file" or brng[2] != "file": + test.macro = True + where = self.loc(node.get("range", {}).get("begin")).offset or self.loc(node.get("loc")).offset or 0 + test.start = where + self.check(where, f"test<{test.n}> is made by a macro: convert it by hand") + self.tests.append(test) + return + test.start, test.body_start, test.body_end = rng[0], brng[0], brng[1] + test.namespaces = self.namespaces_of(parents) + m = re.compile(rb"test\s*<\s*\d+\s*>\s*\(\s*\)").search(self.src.masked, test.start, test.body_start) + if not m: + raise ConvertError(f"{self.src.path}: cannot find test<{test.n}>() in its definition") + test.header_end = m.end() + self.tests.append(test) + + def on_typedef(self, node, parents): + t = node.get("type", {}) + full = t.get("desugaredQualType") or t.get("qualType") or "" + m = re.match(r"^(?:tut::)?test_(object|group)<(.*)>$", full) + if not m: + return + args = split_template_args(m.group(2)) + self.typedefs[node.get("name")] = args[0] + self.removed_typedef_ids.add(node.get("id")) + self.remove_decl(node) + + def on_var(self, node, parents): + t = node.get("type", {}) + full = t.get("desugaredQualType") or t.get("qualType") or "" + m = re.match(r"^(?:tut::)?test_group<(.*)>$", full) + derived = self.derived_groups.get(full) or self.derived_groups.get(full.rpartition("::")[2]) + if not m and derived is None: + return + plain = True + if m: + args = split_template_args(m.group(1)) + data, limit = args[0], int(args[1]) if len(args) > 1 else 50 + literal = find_kind(node, "StringLiteral") + name = literal.get("value") if literal else None + else: + data, limit, name, plain = derived + where = self.node_range(node)[0] if self.node_range(node) else 0 + if not name or not name.startswith('"'): + self.check(where, "a test_group whose name is not a string literal") + return + name = c_unescape(name[1:-1]) + if data in self.groups: + self.check(where, f"two groups run the tests of {data}") + self.groups[data] = Group(name, data, limit) + if plain: + self.remove_decl(node) + + def on_record(self, node, parents): + """A class derived from test_group: one that only names the group in + its constructor, as llvocache's does, goes with the group; one that + holds more (a constructor's body, a destructor, members), which TUT + ran around the group's tests, stays, and its object with it, marked + for converting by hand.""" + for base in node.get("bases", []): + t = base.get("type", {}) + full = t.get("desugaredQualType") or t.get("qualType") or "" + m = re.match(r"^(?:tut::)?test_group<(.*)>$", full) + if not m: + continue + args = split_template_args(m.group(1)) + data = args[0] if "::" in args[0] else "tut::" + args[0] + literal = find_kind(node, "StringLiteral") + name = literal.get("value") if literal else None + plain = names_only(node) + qualified = "::".join([n for n in self.namespaces_of(parents) if n] + [node.get("name", "")]) + for key in (qualified, node.get("name", "")): + self.derived_groups[key] = (data, int(args[1]) if len(args) > 1 else 50, name, plain) + if plain: + self.remove_decl(node) + else: + rng = self.node_range(node) + self.check(rng[0] if rng else 0, + f"{node.get('name')} derives from test_group and holds more than the group's name, " + f"which TUT ran around the group's tests: convert it by hand") + + def remove_decl(self, node): + rng = self.node_range(node) + if rng is None: + return + start, end = rng[0], rng[1] + after = self.src.skip_space(end) + if self.src.masked[after:after + 1] == b";": + end = after + 1 + self.removals.append((start, end, "decl")) + self.handled_nodes.add(id(node)) + for child in iter_nodes(node): + self.handled_nodes.add(id(child)) + + # ---- calls --------------------------------------------------------- + def callee_of(self, node): + inner = [c for c in node.get("inner", []) if isinstance(c, dict) and "kind" in c] + if not inner: + return None, None, [] + callee = inner[0] + stripped = callee + while stripped.get("kind") in ("ImplicitCastExpr", "ParenExpr") and stripped.get("inner"): + stripped = stripped["inner"][0] + args = [a for a in inner[1:] if a.get("kind") != "CXXDefaultArgExpr"] + if stripped.get("kind") == "DeclRefExpr": + ref = stripped.get("referencedDecl", {}) + if ref.get("id") in self.tut_ids and ref.get("name") in TUT_FUNCTIONS: + return ref.get("name"), stripped, args + elif stripped.get("kind") == "MemberExpr": + if stripped.get("referencedMemberDecl") in self.tut_ids and stripped.get("name") in TUT_MEMBERS: + return stripped.get("name"), stripped, args + elif stripped.get("kind") == "UnresolvedLookupExpr": + lookups = stripped.get("lookups", []) + if any(l.get("id") in self.tut_ids for l in lookups) and stripped.get("name") in TUT_FUNCTIONS: + return stripped.get("name"), stripped, args + elif stripped.get("kind") in ("CXXDependentScopeMemberExpr", "UnresolvedMemberExpr"): + if stripped.get("member", stripped.get("name")) in TUT_MEMBERS: + return stripped.get("member", stripped.get("name")), stripped, args + return None, None, [] + + def on_call(self, node, parents): + name, callee, args = self.callee_of(node) + if name is None: + return + self.handled_nodes.add(id(callee)) + for sub in iter_nodes(callee): + self.handled_nodes.add(id(sub)) + rng = self.node_range(node) + begin = self.loc(node.get("range", {}).get("begin")) + if rng is None or begin.how is None: + where = begin.offset if begin.offset is not None else 0 + self.check(where, f"{name}() comes from a macro defined elsewhere") + return + record = {"node": node, "name": name, "args": args, "parents": parents, "range": rng, + "how": begin.how, "callee": callee} + self.calls.setdefault(rng[0], []).append(record) + + def on_throw(self, node, parents): + inner = [c for c in node.get("inner", []) if "kind" in c] + if not inner: + return + value = skip_implicit(inner[0]) + if value_type(value) not in ("tut::failure", "failure"): + return + rng = self.node_range(node) + if rng is None: + return + for sub in iter_nodes(node): + self.handled_nodes.add(id(sub)) + if value.get("kind") not in ("CXXConstructExpr", "CXXFunctionalCastExpr", "CXXTemporaryObjectExpr"): + self.calls.setdefault(rng[0], []).append({"node": node, "name": "throw-held", "args": [], + "parents": parents, "range": rng, "how": "file", + "callee": None}) + return + # The message: the first node under the construction that starts + # after its opening parenthesis. + open_paren = self.src.masked.find(b"(", rng[0], rng[1]) + message = None + for sub in iter_nodes(value): + loc = self.loc(sub.get("range", {}).get("begin")) + if sub is not value and loc.offset is not None and loc.offset > open_paren: + message = sub + break + self.calls.setdefault(rng[0], []).append({"node": node, "name": "throw", + "args": [message] if message else [], + "parents": parents, "range": rng, "how": "file", + "callee": None}) + + def on_catch(self, node, parents): + inner = [c for c in node.get("inner", []) if "kind" in c] + if not inner or inner[0].get("kind") != "VarDecl": + return + decl = inner[0] + caught = re.sub(r"^tut::", "", value_type(decl)) + if caught not in ("failure", "tut_error", "skip_failure"): + return + rng = self.node_range(decl) + if rng is None: + return + body = inner[1] if len(inner) > 1 else {} + rethrows = any(n.get("kind") == "CXXThrowExpr" and not [c for c in n.get("inner", []) if "kind" in c] + for n in iter_nodes(body)) + for sub in iter_nodes(decl): + self.handled_nodes.add(id(sub)) + if caught != "failure": + self.check(rng[0], f"catches tut::{caught}, which Catch2 has no counterpart of") + return + if decl.get("name") and (decl.get("isReferenced") or decl.get("isUsed")): + self.check(rng[0], "uses the tut::failure it catches; Catch2's failure carries no message") + return + if not rethrows: + self.check(rng[0], "catches a tut::failure without rethrowing it; Catch2 has recorded the failure already") + return + m = re.compile(rb"(?:\btut\s*::\s*)?\bfailure\b").search(self.src.masked, rng[0], rng[1]) + if not m: + self.check(rng[0], "cannot find tut::failure in the catch") + return + self.headers.add('"altestfailure.h"') + self.plans.append(Plan(m.start(), m.end(), lambda conv, plan: "ll_test::Failure")) + + def on_reference(self, node, parents): + kind = node.get("kind") + ref = None + if kind == "DeclRefExpr": + ref = node.get("referencedDecl", {}).get("id") + elif kind == "MemberExpr": + ref = node.get("referencedMemberDecl") + elif kind == "UnresolvedLookupExpr": + ids = [l.get("id") for l in node.get("lookups", [])] + ref = next((i for i in ids if i in self.tut_ids), None) + if ref in self.tut_ids: + rng = self.node_range(node) + if rng: + self.check(rng[0], f"{self.tut_ids[ref]} is used other than by a call") + + # ---- deciding each call ------------------------------------------ + def resolve_calls(self): + for offset, records in sorted(self.calls.items()): + first = records[0] + name = first["name"] + if name in TUT_ASSERTING or name == "throw": + self.counted[offset] = True + how = first["how"] + if how == "macro-body": + self.convert_in_macro(offset, records) + continue + if how == "foreign": + self.check(offset, f"{name}() comes from a macro defined elsewhere") + continue + kinds = {self.conversion_kind(r) for r in records} + if len(kinds) > 1 and "dependent" in kinds: + # a template's own definition beside its instantiations + kinds.discard("dependent") + if len(kinds) > 1: + self.check(offset, f"{name}() means different things in different instantiations: " + + ", ".join(sorted(kinds))) + continue + kind = kinds.pop() + # an instantiation's record, where the types are known + first = next((r for r in records if self.conversion_kind(r) == kind), first) + self.convert_call(first, kind) + + def conversion_kind(self, record): + """How a call converts, from the types clang gave its arguments.""" + name, args = record["name"], record["args"] + callee = record["callee"] or {} + if callee.get("kind") in ("UnresolvedLookupExpr", "CXXDependentScopeMemberExpr", "UnresolvedMemberExpr") \ + and name not in TYPE_FREE and name not in TUT_MEMBERS: + return "dependent" + if name == "ensure_equals" and len(args) in (2, 3): + types = [value_type(a) for a in args[-2:]] + if types[0] == types[1] == "LLSD": + return "equals-llsd" + if types[0] == types[1] == "LLURI": + return "equals-uri" + return "equals" + if name == "ensure_approximately_equals": + return "bits-" + (self.parameter_float(record) or "?") + if name == "ensure_distance": + return "distance-" + (self.distance_type(record) or "?") + return name + + def distance_type(self, record): + """The type TUT's ensure_distance worked in, as clang resolved it: + that of its last parameter, const T&.""" + callee = record["callee"] or {} + qual = callee.get("referencedDecl", {}).get("type", {}).get("qualType", "") + m = re.match(r"^void \((.*)\)$", qual) + if not m: + return None + last = split_template_args(m.group(1))[-1] + t = re.sub(r"\s*&$", "", last.strip()) + t = re.sub(r"^const\s+|\s+const$", "", t).replace("(anonymous namespace)::", "").strip() + # A comma would split the macro's arguments. + return t if t and "," not in t else None + + def parameter_float(self, record): + callee = record["callee"] or {} + qual = callee.get("referencedDecl", {}).get("type", {}).get("qualType", "") + m = re.match(r"^void \((.*)\)$", qual) + if not m: + return None + params = [p.strip() for p in split_template_args(m.group(1))] + expected = params[-2] if len(params) >= 3 else "" + return {"F32": "F32", "float": "F32", "F64": "F64", "double": "F64"}.get(expected) + + def context(self, record): + """block (a statement among others), body (the lone statement of an + if, else or loop) or expr, with the parent that decided it.""" + child = record["node"] + for parent in reversed(record["parents"]): + kind = parent.get("kind") + if kind in IMPLICIT: + child = parent + continue + if kind in STATEMENT_PARENTS: + return "block", parent + if kind in BODY_PARENTS: + inner = [c for c in parent.get("inner", []) if "kind" in c] + if kind == "IfStmt" and inner and child is not inner[0]: + return "body", parent + if kind == "DoStmt" and inner and child is inner[0]: + return "body", parent + if kind not in ("IfStmt", "DoStmt") and inner and child is inner[-1]: + return "body", parent + return "expr", parent + return "expr", None + + def in_destructor(self, record): + for parent in reversed(record["parents"]): + if parent.get("kind") == "LambdaExpr": + return False + if parent.get("kind") in FUNCTION_KINDS: + return parent.get("kind") == "CXXDestructorDecl" + return False + + def strength(self, record, nodes): + """CHECK, which lets the case go on past a failure and so reports every + failure of a run, or REQUIRE, which ends it, where going on could + only crash or bury the first failure under its consequences: + - in a destructor, CHECK, since a throw would end the process; + - outside a test's own body (a helper, which any caller may call + in a loop) and in a loop or lambda of one, REQUIRE, so that the + first element that fails is the one reported; + - where the condition guards what follows (see guards()), or + calls what makes it (see prepares()), REQUIRE; + - otherwise CHECK, a plain compare of values among it.""" + if self.in_destructor(record): + return "CHECK" + test = self.test_containing(record["range"][0]) + body = test_body(test) if test else None + if body is None: + return "REQUIRE" + for parent in reversed(record["parents"]): + if parent is body: + break + if parent.get("kind") in LOOP_KINDS or parent.get("kind") == "LambdaExpr": + return "REQUIRE" + if any(self.guards(n) or self.prepares(n, record) for n in nodes if n): + return "REQUIRE" + return "CHECK" + + @staticmethod + def guards(node): + """Whether a condition or value tells what the code after it relies + on: a pointer, compared with null or tested as a bool; an object + tested through its operator bool (an optional, a smart pointer, a + stream; not an atomic, whose bool is its value) or compared with + nullptr through an operator of its own; a position + compared with its container's end() or with npos; or a member that + says how much an object holds or whether it holds anything + (has_value() and found() among them), answering with a bool or an + integer: a float length() is a magnitude, not a size. Default arguments are not + looked in: a NULL a callee defaults to is nothing the test + compared.""" + if value_type(unparenthesised(node)).endswith("*"): + return True + stack = [node] + while stack: + sub = stack.pop() + kind = sub.get("kind") + if kind == "CXXDefaultArgExpr": + continue + if kind == "ImplicitCastExpr" and sub.get("castKind") == "PointerToBoolean": + return True + if kind == "MemberExpr" and sub.get("name") == "operator bool": + # An atomic's bool is its value, not whether it holds one. + base = [c for c in sub.get("inner", []) if "kind" in c] + if not (base and re.search(r"atomic", base[0].get("type", {}).get("qualType", ""), re.I)): + return True + if any(is_end(c) for c in compared(sub)): + return True + # A member, called or read, answering how much or whether: with + # a bool or an integer, as a size or a count does; a float + # length() is a magnitude. + member = None + if kind == "CXXMemberCallExpr": + member = next((c for c in sub.get("inner", []) if "kind" in c), {}) + elif kind == "MemberExpr" and sub.get("type", {}).get("qualType") != "": + member = sub + if member and member.get("kind") == "MemberExpr" and member.get("name") in GUARD_MEMBERS and \ + INTEGRAL.fullmatch(value_type(sub)): + # isNull() and notNull() guard only on what points: on a + # vector or a UUID they are values like any other. + if member.get("name") not in ("isNull", "notNull"): + return True + base = [c for c in member.get("inner", []) if "kind" in c] + if base and re.search(r"Pointer|ptr\b|Handle|\*", value_type(skip_implicit(base[0]))): + return True + stack.extend(c for c in sub.get("inner", []) if isinstance(c, dict) and "kind" in c) + return False + + def prepares(self, node, record): + """Whether a condition calls what makes state the test reads after + it, state that is indeterminate where the call failed: a call + returning bool, to a non-const member function of an object, or + with an argument it may fill (one passed by non-const reference, or + by address), where that object or argument is a local of the test + declared without an initializer and written by nothing since, and + its body reads it again after the assertion, as with LLMatrix4a + inverse; ensure(inverse.setInverse(m)), or S32 value; + ensure(convertToS32(s, value)). A local initialised or written + before (LLMatrix4a m; m.setIdentity()), a member or a parameter + holds a value whatever the call did.""" + test = self.test_containing(record["range"][0]) + body = test_body(test) if test else None + if body is None: + return False + bare = self.bare_locals(body) + made = set() + stack = [node] + while stack: + sub = stack.pop() + kind = sub.get("kind") + if kind in ("CXXDefaultArgExpr", "LambdaExpr"): + continue + inner = [c for c in sub.get("inner", []) if isinstance(c, dict) and "kind" in c] + if kind in ("CallExpr", "CXXMemberCallExpr") and inner and value_type(sub) == "bool": + callee = inner[0] + if kind == "CXXMemberCallExpr" and callee.get("kind") == "MemberExpr" and \ + callee.get("name") != "operator bool": + base = [c for c in callee.get("inner", []) if "kind" in c] + if base and not const_object(base[0], callee.get("isArrow")): + made.add(written_root(base[0])) + for arg in inner[1:]: + if fills(arg): + made.add(written_root(arg)) + stack.extend(inner) + # Bare, and not written between its declaration and this call. + at = record["range"][0] + made = {d for d in made if d in bare and + not any(bare[d] < where < at and decl == d for where, decl in self.writes(body))} + if not made: + return False + after = record["range"][1] + return any(at >= after and decl in made for at, decl in self.reads(body)) + + def bare_locals(self, body): + """The variables a test's body declares without an initializer, each + with where its declaration ends, once per body.""" + cache = self.body_bare + if id(body) not in cache: + found = {} + for n in iter_nodes(body): + if n.get("kind") == "VarDecl" and declared_bare(n): + rng = self.node_range(n) + if rng: + found[n.get("id")] = rng[1] + cache[id(body)] = found + return cache[id(body)] + + def writes(self, body): + """(offset, variable) of every write a test's body makes to a + variable of its own, as written_root() finds it: an assignment, an + increment, a non-const member call on it, a call it is filled by. + Once per body.""" + cache = self.body_writes + if id(body) not in cache: + found = [] + for n in iter_nodes(body): + kind = n.get("kind") + inner = [c for c in n.get("inner", []) if isinstance(c, dict) and "kind" in c] + targets = [] + if kind in ("BinaryOperator", "CompoundAssignOperator") and n.get("opcode", "").endswith("=") \ + and n.get("opcode") not in ("==", "!=", "<=", ">=") and inner: + targets.append(inner[0]) + elif kind == "UnaryOperator" and n.get("opcode") in ("++", "--") and inner: + targets.append(inner[0]) + elif kind == "CXXOperatorCallExpr" and len(inner) >= 2: + name = skip_implicit(inner[0]).get("referencedDecl", {}).get("name", "") + if re.fullmatch(r"operator(?:[-+*/%^&|]|<<|>>)?=|operator(?:\+\+|--)", name): + targets.append(inner[1]) + elif kind == "CXXMemberCallExpr" and inner and inner[0].get("kind") == "MemberExpr": + base = [c for c in inner[0].get("inner", []) if "kind" in c] + if base and not const_object(base[0], inner[0].get("isArrow")): + targets.append(base[0]) + if kind in ("CallExpr", "CXXMemberCallExpr", "CXXConstructExpr"): + targets += [a for a in (inner if kind == "CXXConstructExpr" else inner[1:]) if fills(a)] + for target in targets: + decl = written_root(target) + where = self.loc(n.get("range", {}).get("begin")).in_text() + if decl and where is not None: + found.append((where, decl)) + cache[id(body)] = found + return cache[id(body)] + + def reads(self, body): + """(offset, declaration) of every variable and field a test's body + names, once per body.""" + cache = self.body_reads + if id(body) not in cache: + found = [] + for sub in iter_nodes(body): + decl = variable_of(sub) + if decl: + at = self.loc(sub.get("range", {}).get("begin")).in_text() + if at is not None: + found.append((at, decl)) + cache[id(body)] = found + return cache[id(body)] + + def plain_span(self, node): + """[start, end) of a node written in this file's text, or None.""" + rng = self.node_range(node) + if rng is None or rng[2] not in ("file", "macro-arg") or rng[3] not in ("file", "macro-arg"): + return None + return rng[0], rng[1] + + @staticmethod + def expansion(loc): + """Where the macro whose expansion holds loc was used, or None for a + place in the file's own text.""" + if loc and "expansionLoc" in loc: + return loc["expansionLoc"].get("_f"), loc["expansionLoc"].get("offset") + return None + + def call_span(self, node, record): + """[start, end) of a node written in its call's own text, or None: + where the call is in the file, no end of the node is a macro's; + where the call is in a macro's argument, the node is in it too. A + node one end of which a macro made, as the ( of WRAP(a && b) or + the 2 of ID(2), has no text of its own to rewrite.""" + span = self.plain_span(node) + rng = node.get("range") or {} + if span is None: + return None + own = self.expansion(record["node"].get("range", {}).get("begin")) + if self.expansion(rng.get("begin")) != own or self.expansion(rng.get("end")) != own: + return None + return span + + def written_parens(self, node, record): + """Whether a ParenExpr's parentheses are the call's own text, not a + macro's.""" + span = self.call_span(node, record) + return span is not None and self.src.masked[span[0]:span[0] + 1] == b"(" and \ + self.src.masked[span[1] - 1:span[1]] == b")" + + def bare(self, node, record): + """node, without the parentheses the test wrote around a whole Catch2 + can decompose (a macro's stay), and without the implicit nodes + around that.""" + node = skip_implicit(node) + while node.get("kind") == "ParenExpr" and self.written_parens(node, record): + inner = [c for c in node.get("inner", []) if "kind" in c] + if not inner or not decomposable(inner[0]): + break + node = skip_implicit(inner[0]) + return node + + def and_terms(self, node, record): + """The terms of a && chain, each as the test wrote it, or [node]. + Only a && and parentheses written in the call's own text are split + or seen through: one a macro made (WRAP(a && b), or the && of a + macro's body) would take the macro with it.""" + node = skip_implicit(node) + while node.get("kind") == "ParenExpr" and self.written_parens(node, record): + inner = [c for c in node.get("inner", []) if "kind" in c] + if not inner: + break + inside = skip_implicit(inner[0]) + if inside.get("kind") != "BinaryOperator" or inside.get("opcode") != "&&": + break + node = inside + if node.get("kind") == "BinaryOperator" and node.get("opcode") == "&&": + inner = [c for c in node.get("inner", []) if "kind" in c] + spans = [self.call_span(c, record) for c in inner] + if len(inner) == 2 and None not in spans and \ + self.src.masked[spans[0][1]:spans[1][0]].strip() in (b"&&", b"and"): + return self.and_terms(inner[0], record) + self.and_terms(inner[1], record) + return [node] + + def assertion(self, node, span, negate, record): + """(suffix, text) of the one assertion a condition is: "" or "_FALSE" + after CHECK or REQUIRE, and what goes between its parentheses. + Parentheses the test wrote around a whole Catch2 decomposes go, so + that a failure prints its operands; a literal compared on the left + moves right; a bool compared with true or false is asserted as + itself; and strcmp() is compared as std::string_view, which prints.""" + suffix = "_FALSE" if negate else "" + whole = self.render(*span) + node = self.bare(node, record) + nspan = self.call_span(node, record) + if nspan is None: + return suffix, whole if decomposable(node) else "(" + whole + ")" + pair = operands(node) + op = node.get("opcode") if pair else None + if op in ("==", "!="): + left, right = (skip_implicit(p) for p in pair) + spans = [self.call_span(p, record) for p in pair] + if None not in spans: + for lit, other, other_span in ((left, right, spans[1]), (right, left, spans[0])): + if lit.get("kind") == "CXXBoolLiteralExpr" and value_type(other) == "bool": + truth = (lit.get("value") is True) == (op == "==") + other_node = self.bare(other, record) + inner = self.render(*(self.call_span(other_node, record) or other_span)) + if not decomposable(other_node) and not wrapped(inner): + inner = "(" + inner + ")" + return ("" if truth != negate else "_FALSE"), inner + for zero, call in ((left, right), (right, left)): + args = strcmp_args(call) + if args and zero.get("kind") == "IntegerLiteral" and zero.get("value") == "0": + text = self.string_views(args, op, record) + if text: + return suffix, text + if is_literal(left) and not is_literal(right): + return suffix, f"{self.render(*spans[1])} {op} {self.render(*spans[0])}" + if node.get("kind") == "UnaryOperator" and node.get("opcode") == "!": + inner = [c for c in node.get("inner", []) if "kind" in c] + args = strcmp_args(inner[0]) if inner else None + text = self.string_views(args, "==", record) if args else None + if text: + return suffix, text + # !x is x, asserted false, so that x prints. + operand = self.bare(inner[0], record) if inner else None + ospan = self.call_span(operand, record) if operand else None + if ospan: + text = self.render(*ospan) + return ("" if negate else "_FALSE"), text if decomposable(operand) else "(" + text + ")" + args = strcmp_args(node) + text = self.string_views(args, "!=", record) if args else None + if text: + return suffix, text + text = self.render(*nspan) + return suffix, text if decomposable(node) else "(" + text + ")" + + def string_views(self, args, op, record): + spans = [self.call_span(a, record) for a in args] + if None in spans: + return None + texts = [] + for arg, span in zip(args, spans): + text = self.render(*span) + texts.append(text if skip_implicit(arg).get("kind") == "StringLiteral" else f"std::string_view({text})") + # Two literals compared as they are would be two arrays' addresses. + if not any(t.startswith("std::string_view(") for t in texts): + texts[0] = f"std::string_view({texts[0]})" + if not texts[0].startswith("std::string_view("): + texts.reverse() + self.headers.add("") + return f"{texts[0]} {op} {texts[1]}" + + def arg_ranges(self, record): + """[start, end) of each argument.""" + start, end = record["range"][0], record["range"][1] + if self.src.data[end - 1:end] != b")": + return None + if record["name"] == "throw": + open_paren = self.src.masked.find(b"(", start, end) + return [(self.src.skip_space(open_paren + 1), self.src.skip_space_back(end - 1))] + begins = [] + own = self.expansion(record["node"].get("range", {}).get("begin")) + for arg in record["args"]: + loc = arg.get("range", {}).get("begin") + where = self.loc(loc) + begin = where.in_text() + # An argument a macro begins, as ID(x) does with x, begins where + # the macro was used, not at the x inside it. + if where.how == "macro-arg" and where.expansion and self.expansion(loc) != own: + begin = where.expansion[0] + if begin is None or not (start < begin < end): + return None + begins.append(begin) + if begins != sorted(begins): + return None + ranges = [] + for i, b in enumerate(begins): + if i + 1 < len(begins): + stop = self.src.skip_space_back(begins[i + 1]) + if self.src.masked[stop - 1:stop] != b",": + return None + stop = self.src.skip_space_back(stop - 1) + else: + stop = self.src.skip_space_back(end - 1) + if stop <= b: + return None + ranges.append((b, stop)) + return ranges + + def separator(self, ranges, i): + """What stood between argument i and i + 1, without its comma: the line + break and comments, or a space.""" + text = self.text(ranges[i][1], ranges[i + 1][0]) + comma = self.src.masked.find(b",", ranges[i][1], ranges[i + 1][0]) - ranges[i][1] + text = text[:comma] + text[comma + 1:] + if text.strip() or "\n" in text: + return text if text[:1].isspace() else " " + text + return " " + + def operand(self, ranges, i, node, wrap_at, guard=False): + """Argument i's text, parenthesised if its operator binds no tighter + than wrap_at, or, with guard, if a macro taking it would split it.""" + text = self.render(ranges[i][0], ranges[i][1]) + if precedence(node) <= wrap_at or (guard and needs_macro_guard(self.src.masked[ranges[i][0]:ranges[i][1]])): + return "(" + text + ")" + return text + + def condition(self, span, node): + """A condition's text for REQUIRE, parenthesised where Catch2 cannot + decompose it (&&, ||, ?:, an assignment, a comparison of a + comparison). Parentheses the test wrote stay: what they hold is + evaluated as TUT evaluated it, and its operands are not printed, so a + type with no operator<< linked still links.""" + text = self.render(*span) + return text if decomposable(node) else "(" + text + ")" + + def convert_call(self, record, kind): + name = record["name"] + start, end = record["range"][0], record["range"][1] + args = record["args"] + if kind == "dependent": + self.check(start, f"{name}() in a template, and no instantiation shows its types") + return + if name == "get_test_name": + self.headers.add('"altestname.h"') + self.plans.append(Plan(start, end, lambda conv, plan: "ll_test::currentTestName()")) + return + if name == "set_test_name": + return # folded into the case's name by name_tests + if name == "ensure_errno": + self.check(start, "ensure_errno has no Catch2 counterpart") + return + if name == "throw-held": + self.check(start, "throws a tut::failure it holds, which has no Catch2 counterpart") + return + context, _ = self.context(record) + if context == "expr": + self.check(start, f"{name}() is part of an expression, where a Catch2 assertion, a statement, cannot go") + return + ranges = self.arg_ranges(record) if args or name == "throw" else [] + if ranges is None or (name == "throw" and not args): + self.check(start, f"cannot place the arguments of {name}(); a macro around them?") + return + check_form = self.in_destructor(record) + if check_form and name in ("fail", "skip", "throw"): + self.check(start, f"{name}() in a destructor would throw out of it") + return + if kind == "bits-?": + self.check(start, "cannot tell which ensure_approximately_equals overload was called") + return + if kind == "distance-?": + self.check(start, "cannot tell, or cannot write in a macro's argument, the type ensure_distance worked in") + return + build = self.builder(name, kind, record, ranges, self.strength(record, value_args(name, args))) + if build is not None: + self.headers.update(headers_for(name, kind)) + if any(strcmp_args(sub) for a in args for sub in iter_nodes(a)): + self.headers.add("") + if build is None: + self.check(start, f"{name}() with {len(args)} arguments is not a form this converter knows") + return + block = getattr(build, "block", False) + stmt_end = end + if context == "body" or block: + after = self.src.skip_space(end) + if self.src.masked[after:after + 1] != b";": + self.check(start, f"{name}() as a statement is not followed by its semicolon") + return + stmt_end = after + 1 + + def render(conv, plan, build=build, context=context, start=start, block=block): + statements = build() + flat = [s for st in statements for s in (st[1] if isinstance(st, tuple) else [st])] + conv.emitted[start] = sum(1 for s in flat if CATCH_ASSERTION.match(s.encode())) + if context == "body": + if len(flat) > 1: + return "{ " + "; ".join(flat) + "; }" + return flat[0] + ";" + indent = conv.src.indent_at(start).decode() + if block: + unit = "\t" if indent.startswith("\t") else " " + return "{\n" + "".join(f"{indent}{unit}{s};\n" for s in flat) + indent + "}" + if conv.src.starts_line(start): + return (";\n" + indent).join(flat) + return "; ".join(flat) + self.plans.append(Plan(start, stmt_end, render)) + + def builder(self, name, kind, record, ranges, req): + """A function giving the statements a call becomes, or None.""" + args = record["args"] + n = len(args) + + def message(i): + if is_null_message(args[i]): + return [] + text = self.render(*ranges[i]) + if precedence(args[i]) <= 11 or needs_macro_guard(self.src.masked[ranges[i][0]:ranges[i][1]]): + text = "(" + text + ")" + return [f"UNSCOPED_INFO({text})"] + + def streamed(i): + text = self.render(*ranges[i]) + return "(" + text + ")" if precedence(args[i]) <= 11 else text + + if name in ("ensure", "ensure_not"): + if n not in (1, 2): + return None + condition = args[n - 1] + # A && chain is as many assertions, each printing its operands, + # each as strong as its own term needs. + terms = self.and_terms(condition, record) if name == "ensure" else [condition] + # A later term that changes something ran only where the ones + # before it held: split, it would run regardless. + if len(terms) > 1 and (any(self.call_span(t, record) is None for t in terms) or + any(side_effect(t) for t in terms[1:])): + terms = [condition] + + # TUT's ensure stopped the test at the first term that failed; + # a term ends the case where a later one leans on what it read + # for its own safety (dereferences it, indexes by it), so that + # going on past its failure could crash or read garbage. + chained = [False] * len(terms) + if len(terms) > 1 and not self.in_destructor(record): + later = set() + for i in range(len(terms) - 1, -1, -1): + chained[i] = bool(variables(terms[i], constants=False) & later) + later |= leaned_on(terms[i]) + + def build(): + out = [] + for i, term in enumerate(terms): + span = ranges[n - 1] if len(terms) == 1 else self.call_span(term, record) + suffix, text = self.assertion(term, span, name == "ensure_not", record) + if len(terms) == 1: + strength = req + else: + strength = "REQUIRE" if chained[i] else self.strength(record, [term]) + out.append(f"{strength}{suffix}({text})") + msg = message(0) if n == 2 else [] + if msg and len(out) > 1: + # The message is about all of them: INFO, scoped to them. + return [("block", ["INFO" + msg[0][len("UNSCOPED_INFO"):]] + out)] + return msg + out + build.block = n == 2 and not is_null_message(args[0]) and len(terms) > 1 + return build + if name in ("fail", "skip", "throw"): + if n != 1: + return None + macro = "SKIP" if name == "skip" else "FAIL" + return lambda: [f"{macro}({streamed(0)})"] + if name in ("ensure_equals", "ensure_not_equals"): + if n not in (2, 3): + return None + a, e = n - 2, n - 1 + + def build(): + msg = message(0) if n == 3 else [] + sep = self.separator(ranges, a) + if kind == "equals-llsd": + actual = self.operand(ranges, a, args[a], 1, guard=True) + expected = self.operand(ranges, e, args[e], 1) + return msg + [f"{req}_THAT({actual},{sep}ll_test::LLSDEquals({expected}))"] + if kind == "equals-uri": + actual = self.operand(ranges, a, args[a], 15) + expected = self.operand(ranges, e, args[e], 15) + return msg + [f"{req}({actual}.asString() =={sep}{expected}.asString())"] + op = "==" if name == "ensure_equals" else "!=" + actual = self.operand(ranges, a, args[a], 9) + expected = self.operand(ranges, e, args[e], 9) + return msg + [f"{req}({actual} {op}{sep}{expected})"] + return build + if name in ("ensure_distance", "ensure_approximately_equals_range"): + if n not in ((4,) if name == "ensure_approximately_equals_range" else (3, 4)): + return None + a = n - 3 + + def build(): + msg = message(0) if n == 4 else [] + actual = self.operand(ranges, a, args[a], 1, guard=True) + expected = self.operand(ranges, a + 1, args[a + 1], 1) + margin = self.operand(ranges, a + 2, args[a + 2], 1) + if name == "ensure_distance": + # TUT's: strictly inside, worked in its own type. + matcher = f"ll_test::WithinDistance<{kind.split('-', 1)[1]}>(" + else: + # TUT's parameters were F32: all three were narrowed. + narrow = [value_type(skip_implicit_casts(args[a + i])) not in ("float", "F32") for i in range(3)] + actual, expected, margin = (f"F32({t})" if cast else t + for t, cast in zip((actual, expected, margin), narrow)) + matcher = "Catch::Matchers::WithinAbs(" + return msg + [f"{req}_THAT({actual},{self.separator(ranges, a)}{matcher}" + f"{expected},{self.separator(ranges, a + 1)}{margin}))"] + return build + if name == "ensure_approximately_equals": + if n not in (3, 4): + return None + a = n - 3 + ftype = kind.split("-", 1)[1] + + def build(): + msg = message(0) if n == 4 else [] + actual = self.operand(ranges, a, args[a], 1, guard=True) + expected = self.operand(ranges, a + 1, args[a + 1], 1) + given = value_type(skip_implicit_casts(args[a + 1])) + if {"float": "F32", "double": "F64"}.get(given) != ftype: + expected = f"{ftype}({expected})" + bits = self.operand(ranges, a + 2, args[a + 2], 1) + return msg + [f"{req}_THAT({actual},{self.separator(ranges, a)}ll_test::WithinBits(" + f"{expected},{self.separator(ranges, a + 1)}{bits}))"] + return build + if name in ("ensure_contains", "ensure_does_not_contain", "ensure_starts_with", "ensure_ends_with"): + if n != 3: + return None + matcher = {"ensure_contains": "Catch::Matchers::ContainsSubstring", + "ensure_does_not_contain": "!Catch::Matchers::ContainsSubstring", + "ensure_starts_with": "Catch::Matchers::StartsWith", + "ensure_ends_with": "Catch::Matchers::EndsWith"}[name] + + def build(): + actual = self.operand(ranges, 1, args[1], 1, guard=True) + part = self.operand(ranges, 2, args[2], 1) + return message(0) + [f"{req}_THAT({actual},{self.separator(ranges, 1)}{matcher}({part}))"] + return build + if name == "ensure_memory_matches": + if n not in (4, 5): + return None + a = n - 4 + + def build(): + msg = message(0) if n == 5 else [] + parts = [self.operand(ranges, a + i, args[a + i], 1) for i in range(4)] + return msg + [f"{req}_THAT(ll_test::Memory({parts[0]}, {parts[1]}),{self.separator(ranges, a + 1)}" + f"ll_test::MemoryMatches({parts[2]}, {parts[3]}))"] + return build + return None + + def convert_in_macro(self, offset, records): + """A call written in a #define of this file: its arguments are the + macro's text, so they are split as text, and every expansion must + agree on what the call means and use it as a statement among others.""" + name = records[0]["name"] + kinds = {self.conversion_kind(r) for r in records} + if len(kinds) > 1 and "dependent" in kinds: + kinds.discard("dependent") + contexts = {self.context(r)[0] for r in records} + if len(kinds) != 1 or contexts != {"block"}: + self.check(offset, f"{name}() in a macro whose uses differ, or are not statements in a block") + return + kind = kinds.pop() + if kind in ("dependent", "bits-?", "distance-?") or name in TUT_MEMBERS or name == "ensure_errno": + self.check(offset, f"{name}() in a macro, as {kind}") + return + parts = self.text_call(offset) + if parts is None: + self.check(offset, f"cannot split the arguments of {name}() in its macro") + return + end, texts = parts + statements = text_statements(name, texts, macro=True, kind=kind) + if statements is None: + self.check(offset, f"{name}() with {len(texts)} arguments in a macro") + return + if any(self.in_destructor(r) for r in records): + statements = [re.sub(r"^REQUIRE", "CHECK", st) for st in statements] + self.headers.update(headers_for(name, kind)) + self.plans.append(Plan(offset, end, lambda conv, plan, s=statements: "; ".join(s))) + + def text_call(self, offset): + """(end, [argument texts]) of a call, read from its text, or None + where the text alone cannot tell its arguments apart.""" + open_paren = self.src.masked.find(b"(", offset) + close = self.src.matching(open_paren) if open_paren >= 0 else -1 + if close < 0: + return None + inside = self.src.masked[open_paren + 1:close] + depth = 0 + segments = [] + start = 0 + for i, c in enumerate(inside): + if c in b"([{": + depth += 1 + elif c in b")]}": + depth -= 1 + elif c == 0x2C and depth == 0: + segments.append((start, i)) + start = i + 1 + segments.append((start, len(inside))) + # A comma between template arguments looks like one between call + # arguments. + if len(segments) > 1 and b"<" in inside and b">" in inside: + return None + texts = [self.text(open_paren + 1 + s, open_paren + 1 + e).strip() for s, e in segments] + if texts == [""]: + texts = [] + return close + 1, texts + + # ---- what the AST does not hold ---------------------------------- + def convert_uncompiled(self): + """Tests and calls in branches this host does not compile: by their + text, where their types do not matter.""" + header = re.compile(rb"template\s*<\s*>\s*template\s*<\s*>\s*void\s+((?:::)?\w+(?:\s*::\s*\w+)*)" + rb"\s*::\s*test\s*<\s*(\d+)\s*>\s*\(\s*\)") + known = {t.start for t in self.tests} + for m in header.finditer(self.src.masked): + if m.start() in known or self.src.state(m.start()) != "inactive": + continue + parts = [p.decode() for p in re.split(rb"\s*::\s*", m.group(1)) if p] + cls = parts[-1] + if cls == "object" and len(parts) > 1: + cls = parts[-2] # test_group::object, by the group's typedef + data = self.typedefs.get(cls) or self.text_typedefs().get(cls) or self.text_groups().get(cls) + open_brace = self.src.skip_space(m.end()) + close = self.src.matching(open_brace) if self.src.masked[open_brace:open_brace + 1] == b"{" else -1 + test = Test() + test.n = int(m.group(2)) + test.inactive = True + test.start = m.start() + test.header_end = m.end() + test.body_start = open_brace + test.body_end = close + 1 if close >= 0 else None + test.data = data + test.namespaces = self.namespaces_at(m.start()) + if data is None or close < 0: + self.check(m.start(), f"test<{test.n}>, not compiled here, belongs to no group this converter found") + test.macro = True + self.tests.append(test) + names = "|".join(sorted(TUT_FUNCTIONS | TUT_MEMBERS, key=len, reverse=True)) + call = re.compile(rb"(?:])(?:tut\s*::\s*)?(" + names.encode() + rb")\s*\(") + throw = re.compile(rb"(?\s*(\w+)\s*;", masked): + remove(m) + if self.src.state(m.start()) == "inactive": + groups.add(m.group(1)) + for m in re.finditer(rb"typedef\s+(?:tut\s*::\s*)?(\w+)\s*::\s*object\s+\w+\s*;", masked): + if m.group(1) in groups: + remove(m) + for m in re.finditer(rb"(?:tut\s*::\s*)?(\w+)\s+\w+\s*\(\s*\"[^\"\n]*\"\s*\)\s*;", masked): + if m.group(1) in groups: + remove(m) + for m in re.finditer(rb"(?:tut\s*::\s*)?test_group\s*<[^;{}]*>\s*\w+\s*\(\s*\"[^\"\n]*\"\s*\)\s*;", masked): + remove(m) + + def convert_text_call(self, offset, name): + if name == "set_test_name": + return # folded into the case's name by name_tests + if name == "get_test_name": + end = self.src.matching(self.src.masked.find(b"(", offset)) + self.headers.add('"altestname.h"') + self.plans.append(Plan(offset, end + 1, lambda conv, plan: "ll_test::currentTestName()")) + return + if name in TUT_ASSERTING or name == "throw": + self.counted[offset] = True + if name not in TYPE_FREE and name != "throw": + self.check(offset, f"{name}(), in code not compiled here, converts by its arguments' types") + return + parts = self.text_call(offset) + if parts is None: + self.check(offset, f"cannot split the arguments of {name}(), in code not compiled here") + return + end, texts = parts + after = self.src.skip_space(end) + if self.src.masked[after:after + 1] != b";": + self.check(offset, f"{name}(), in code not compiled here, is not a statement") + return + statements = text_statements(name, texts, macro=False) + if statements is None: + self.check(offset, f"{name}() with {len(texts)} arguments, in code not compiled here") + return + # The lone statement of an if, else or loop, by what precedes it. + lead = self.src.masked[max(0, offset - 400):offset].decode("latin-1").rstrip() + lone = re.search(r"(\)|\belse|\bdo)$", lead) is not None + if len(statements) > 1 and lone: + text = "{ " + "; ".join(statements) + "; }" + self.plans.append(Plan(offset, after + 1, lambda conv, plan, t=text: t)) + else: + joiner = ";\n" + self.src.indent_at(offset).decode() if self.src.starts_line(offset) else "; " + text = joiner.join(statements) + self.plans.append(Plan(offset, end, lambda conv, plan, t=text: t)) + + def text_groups(self): + """typedef test_group G: G -> X, from the text.""" + found = {} + for m in re.finditer(rb"typedef\s+(?:tut\s*::\s*)?test_group\s*<\s*([\w:]+)\s*(?:,\s*\d+\s*)?>\s*(\w+)\s*;", + self.src.masked): + data = m.group(1).decode() + found[m.group(2).decode()] = data if "::" in data else "tut::" + data + return found + + def text_typedefs(self): + if hasattr(self, "_text_typedefs"): + return self._text_typedefs + found = {} + groups = {} + for m in re.finditer(rb"typedef\s+(?:tut\s*::\s*)?test_group\s*<\s*([\w:]+)\s*(?:,\s*\d+\s*)?>\s*(\w+)\s*;", + self.src.masked): + groups[m.group(2).decode()] = m.group(1).decode() + for m in re.finditer(rb"typedef\s+(?:tut\s*::\s*)?(\w+)\s*::\s*object\s+(\w+)\s*;", self.src.masked): + if m.group(1).decode() in groups: + found[m.group(2).decode()] = "tut::" + groups[m.group(1).decode()] + self._text_typedefs = found + return found + + def namespaces_at(self, offset): + stack = [] + for m in re.finditer(rb"\bnamespace\s*(\w*)\s*\{|[{}]", self.src.masked[:offset]): + if m.group(0).startswith(b"namespace"): + stack.append(m.group(1).decode()) + elif m.group(0) == b"{": + stack.append(None) + elif stack: + stack.pop() + return tuple(s for s in stack if s is not None) + + # ---- tests, named and placed --------------------------------------- + def resolve_tests(self): + demangled = self.demangled + for test in self.tests: + if test.data is None and getattr(test, "mangled", None): + full = demangled.get(test.mangled, "") + m = re.match(r"^void tut::test_object<(.*)>::test<\d+>\(\)$", full) + if m: + test.data = m.group(1) + group = self.find_group(test.data) if test.data else None + if group is None and test.data: + # a group only text can see + group = self.text_group(test.data) + test.group = group + if group is None: + if not test.macro: + self.check(test.start, f"test<{test.n}> belongs to no group this converter found") + test.macro = True + continue + group.tests.append(test) + test.fixture = self.spell_fixture(test.data, test.namespaces) + self.collect_names() + + def find_group(self, data): + """The group whose data is data, which the group may have named + without its namespaces.""" + plain = data.replace("(anonymous namespace)::", "") + for key, group in self.groups.items(): + k = key.replace("(anonymous namespace)::", "") + if k == plain or ("::" not in k and plain.rpartition("::")[2] == k): + return group + return None + + def text_group(self, data): + short = data.split("::")[-1] + for m in re.finditer(rb"typedef\s+(?:tut\s*::\s*)?test_group\s*<\s*([\w:]+)\s*(?:,\s*(\d+)\s*)?>\s*(\w+)\s*;", + self.src.masked): + if m.group(1).decode().split("::")[-1] != short: + continue + typedef = m.group(3).decode() + var = re.search(rb"(?:tut\s*::\s*)?" + typedef.encode() + rb"\s+(\w+)\s*\(\s*\"", self.src.masked) + if not var: + continue + q = var.end() - 1 + close = self.src.data.find(b'"', q + 1) + while close > 0 and self.src.data[close - 1:close] == b"\\": + close = self.src.data.find(b'"', close + 1) + name = c_unescape(self.text(q + 1, close)) + group = Group(name, data, int(m.group(2)) if m.group(2) else 50) + self.groups[data] = group + for start, end in ((m.start(), m.end()), (var.start(), self.src.masked.find(b";", var.end()) + 1)): + self.removals.append((start, end, "decl")) + return group + return None + + def spell_fixture(self, data, namespaces): + name = data.replace("(anonymous namespace)::", "") + prefix = "::".join(n for n in namespaces if n) + while prefix: + if name.startswith(prefix + "::"): + return name[len(prefix) + 2:] + prefix = prefix.rpartition("::")[0] + return name + + def collect_names(self): + """Each test's set_test_name, from the AST or, uncompiled, its text.""" + for offset, records in self.calls.items(): + for r in records: + if r["name"] != "set_test_name": + continue + test = self.test_containing(offset) + if test is None: + self.check(offset, "set_test_name outside a test's body") + continue + arg = r["args"][0] if r["args"] else None + literal = skip_implicit(arg) if arg else None + top = self.context(r)[0] == "block" and self.context(r)[1] is test_body(test) + test.names.append((offset, r, literal if literal and literal.get("kind") == "StringLiteral" + else None, top)) + name_call = re.compile(rb"(?])set_test_name\s*\(") + for test in self.tests: + if not test.inactive or test.body_end is None: + continue + for m in name_call.finditer(self.src.masked, test.body_start, test.body_end): + test.names.append((m.start(), None, "text", True)) + + def test_containing(self, offset): + for test in self.tests: + if test.body_start is not None and test.body_end is not None and test.body_start <= offset < test.body_end: + return test + return None + + def name_tests(self): + for data, group in self.groups.items(): + seen = [] # (the #if branches around a case, its name) + for test in sorted(group.tests, key=lambda t: t.n): + if test.macro: + test.tut_name = None # left as it is: no case, and parity says so + continue + text_name = None + if len(test.names) > 1: + self.check(test.start, f"test<{test.n}> names itself more than once") + elif test.names: + offset, record, literal, top = test.names[0] + if not top or literal is None: + self.check(offset, "set_test_name that is not a statement of the test's body, or not a literal") + else: + call_end = self.src.matching(self.src.masked.find(b"(", offset)) + inside = self.text(self.src.masked.find(b"(", offset) + 1, call_end) + pieces = re.findall(r'"((?:[^"\\\n]|\\.)*)"', inside) + stripped = re.sub(r'"((?:[^"\\\n]|\\.)*)"', "", inside).strip() + if not pieces or stripped: + self.check(offset, "set_test_name with a literal this converter cannot read") + else: + text_name = "".join(c_unescape(p) for p in pieces) + test.name_literal_inner = "".join(pieces) + self.remove_statement(offset, call_end + 1) + test.tut_name = text_name if text_name is not None else ("" if not test.names else None) + # A name given in --names wins; then set_test_name's; then the + # first sentence of the comment written above the test. + given = self.name_overrides.get((group.name, test.n)) + if given is None and text_name is None: + given = self.comment_name(test) + if given is not None: + text_name = given + test.name_literal_inner = c_escape(given) + base = f"{group.name}: {text_name}" if text_name is not None else f"{group.name}: #{test.n}" + # Only tests in different branches of one #if are never + # compiled together; any other two may be, so their names + # must differ. + where = self.src.conditionals_at(test.start) + name = base + if any(other == name.lower() and not exclusive(where, at) for at, other in seen): + name = f"{base} #{test.n}" + seen.append((where, name.lower())) + test.catch2_name = name + if text_name is not None and name == base: + test.catch2_literal = '"' + c_escape(group.name + ": ") + test.name_literal_inner + '"' + else: + test.catch2_literal = '"' + c_escape(name) + '"' + + def comment_name(self, test): + """A name from the comment lines right above a test, or None.""" + if test.start is None: + return None + start = self.src.line_start(test.start) + top = start + while top > 0: + prev = self.src.line_start(top - 1) + if self.src.masked[prev:top].strip() == b"" and self.src.data[prev:top].strip(): + top = prev + continue + break + text = self.text(top, start) + if not text.strip(): + text = "\n".join(self.comments_in(test.start, test.header_end)) + return name_from_comment(text) + + def remove_statement(self, start, end): + after = self.src.skip_space(end) + if self.src.masked[after:after + 1] == b";": + end = after + 1 + self.removals.append((start, end, "statement")) + + # ---- the plans ---------------------------------------------------- + def plan_fixtures(self): + """A group whose data struct holds nothing, and which nothing else in + the file names, is plain TEST_CASEs: the struct goes.""" + for data, group in self.groups.items(): + group.fixtureless = False + tests = [t for t in group.tests if not t.macro and t.catch2_name] + if not tests or any(t.inactive for t in group.tests): + continue + node = self.records.get(tests[0].data) or self.records.get(data) + if node is None: + short = data.replace("(anonymous namespace)::", "").rpartition("::")[2] + found = [n for k, n in self.records.items() if k.rpartition("::")[2] == short] + node = found[0] if len(found) == 1 else None + if node is None or node.get("bases") or node.get("name") in self.friend_names: + continue + if [c for c in node.get("inner", []) if "kind" in c and not c.get("isImplicit")]: + continue + rng = self.node_range(node) + if rng is None or rng[2] != "file": + continue + spans = [(rng[0], rng[1])] + [(s, e) for s, e, _ in self.removals] + \ + [(t.start, t.header_end) for t in group.tests if t.start is not None and t.header_end] + named = [m.start() for m in re.finditer(rb"\b" + re.escape(node["name"].encode()) + rb"\b", + self.src.masked)] + if any(not any(s <= at < e for s, e in spans) for at in named): + continue + group.fixtureless = True + self.remove_decl(node) + + def plan_structure(self): + for test in self.tests: + if test.macro or test.group is None or test.catch2_name is None: + continue + tag = group_tag(test.group.name) + if getattr(test.group, "fixtureless", False): + test.header_text = f"TEST_CASE({test.catch2_literal}," + header = f'TEST_CASE({test.catch2_literal}, "[{tag}]")' + else: + if "," in test.fixture: + self.check(test.start, f"the fixture {test.fixture} has a comma, which TEST_CASE_METHOD would split") + continue + test.header_text = f"TEST_CASE_METHOD({test.fixture}, {test.catch2_literal}," + header = f'TEST_CASE_METHOD({test.fixture}, {test.catch2_literal}, "[{tag}]")' + # A comment inside template<> template<> void X::test() stays, + # on a line of its own above the case. + indent = self.src.indent_at(test.start).decode() + comments = [c.strip() for c in self.comments_in(test.start, test.header_end)] + header = "".join(f"{c}\n{indent}" for c in comments) + header + self.plans.append(Plan(test.start, test.header_end, lambda conv, plan, h=header: h)) + if test.n > test.group.limit: + self.notes.append(f"{test.group.name} test<{test.n}> is past the group's limit of " + f"{test.group.limit}: TUT never ran it, Catch2 will") + spans = [] + for start, end, what in self.removals: + ls = self.src.line_start(start) + le = self.src.line_end(end) + if self.src.masked[ls:start].strip() == b"" and self.src.masked[end:le].strip() == b"": + start, end = ls, min(le + 1, len(self.src.data)) + if what == "decl": + start = self.comment_above(start) + spans.append((start, end)) + self.removed_spans = self.tidy_removals(spans) + self.remove_empty_namespaces() + self.removed_spans = self.tidy_removals(self.removed_spans) + for start, end in self.removed_spans: + self.plans.append(Plan(start, end, lambda conv, plan: "")) + + def can_unwrap(self, converted): + """Whether namespace tut is only TUT's left: nothing names tut::, no + type production code befriends as tut:: is here, and no call is left + unconverted that would look for TUT in it.""" + if self.checks or not self.tut_namespaces: + return False + masked = mask_source(converted.encode("utf-8", "surrogateescape")) + if re.search(rb"\btut\s*::|\busing\s+namespace\s+tut\b", masked): + return False + return not any(re.search(rb"\b" + re.escape(n.encode()) + rb"\b", masked) for n in self.friend_names) + + def plan_unwrap(self): + """namespace tut becomes an anonymous namespace, and an anonymous + namespace written directly inside it, on lines of its own, merges + into it.""" + taken = [(p.start, p.end) for p in self.plans if p.start != p.end] + + def free(s, e): + return not any(a < e and s < b for a, b in taken) + size = len(self.src.data) + for node, parents in self.tut_namespaces: + rng = self.node_range(node) + if rng is None or rng[2] != "file" or any(p.get("kind") == "NamespaceDecl" for p in parents): + continue + m = re.compile(rb"namespace\s+tut\b").match(self.src.masked, rng[0]) + if not m or not free(m.start(), m.end()): + continue + self.plans.append(Plan(m.start(), m.end(), lambda conv, plan: "namespace")) + # A comment on its closing brace that says whose it was says so no + # longer. + note = re.compile(rb"//[ \t]*(?:end[ \t]+(?:of[ \t]+)?)?(?:namespace[ \t]+)?tut\b[^\n]*").search( + self.src.data, rng[1], self.src.line_end(rng[1] - 1)) + if note and free(note.start(), note.end()): + self.plans.append(Plan(note.start(), note.end(), lambda conv, plan: "// anonymous namespace")) + for child in node.get("inner", []): + if child.get("kind") != "NamespaceDecl" or child.get("name"): + continue + crng = self.node_range(child) + if crng is None or crng[2] != "file": + continue + opener = self.src.masked.find(b"{", crng[0]) + close = crng[1] + if opener < 0 or not re.fullmatch(rb"namespace\s*", self.src.masked[crng[0]:opener]): + continue + os_, oe = self.src.line_start(crng[0]), self.src.line_end(opener) + cs, ce = self.src.line_start(close - 1), self.src.line_end(close - 1) + if self.src.masked[os_:crng[0]].strip() or self.src.masked[opener + 1:oe].strip() or \ + self.src.masked[cs:close - 1].strip() or self.src.masked[close:ce].strip(): + continue + oe, ce = min(oe + 1, size), min(ce + 1, size) + if not (free(os_, oe) and free(cs, ce)): + continue + self.plans.append(Plan(os_, oe, lambda conv, plan: "")) + self.plans.append(Plan(cs, ce, lambda conv, plan: "")) + taken += [(os_, oe), (cs, ce)] + + def comment_above(self, start): + """Where the comment lines right above a removed declaration begin, + if they stand apart from what precedes them, as a comment about the + group does: they go with it.""" + top = start + while top > 0: + prev = self.src.line_start(top - 1) + if self.src.masked[prev:top].strip() == b"" and self.src.data[prev:top].strip(): + top = prev + continue + break + if top == start: + return start + if top == 0: + return top + before = self.src.line_start(top - 1) + line = self.src.masked[before:top].strip() + return top if line == b"" or line.endswith(b"{") else start + + def comments_in(self, start, end): + """The comments between start and end, as written: a test's header + holds no string literal to mistake for one.""" + return [m.group(0).decode("utf-8", "surrogateescape") + for m in re.finditer(rb"//[^\n]*|/\*.*?\*/", self.src.data[start:end], re.S)] + + def tidy_removals(self, spans): + """Whole-line removals merged where only blank lines part them, and + the blank lines they leave doubled made single.""" + def blank(ls): + return ls < len(self.src.data) and self.src.data[ls:self.src.line_end(ls)].strip() == b"" + spans = sorted(spans) + merged = [] + for start, end in spans: + if merged: + ps, pe = merged[-1] + if start <= pe or (self.src.data[pe:start].strip() == b"" and pe == self.src.line_start(pe) + and start == self.src.line_start(start)): + merged[-1] = (ps, max(pe, end)) + continue + merged.append((start, end)) + out = [] + for start, end in merged: + whole = start == self.src.line_start(start) and (end == len(self.src.data) or + end == self.src.line_start(end)) + if whole and start > 0: + prev = self.src.line_start(start - 1) + prev_text = self.src.masked[prev:start].strip() + if (blank(prev) or prev_text.endswith(b"{")) and blank(end): + end = min(self.src.line_end(end) + 1, len(self.src.data)) + out.append((start, end)) + return out + + def remove_empty_namespaces(self): + """An anonymous namespace that held only the group goes with it.""" + for item in self.items: + for node in iter_nodes(item): + if node.get("kind") != "NamespaceDecl" or node.get("name"): + continue + rng = self.node_range(node) + if rng is None or rng[2] != "file": + continue + inner = [c for c in node.get("inner", []) if "kind" in c and not c.get("isImplicit")] + spans = [self.node_range(c) for c in inner] + if not inner or not all(s and any(a <= s[0] and s[1] <= b for a, b in self.removed_spans) + for s in spans): + continue + start, end = rng[0], rng[1] + le = self.src.line_end(end) + if self.src.masked[end:le].strip() == b"": + end = min(le + 1, len(self.src.data)) + ls = self.src.line_start(start) + if self.src.masked[ls:start].strip() == b"": + start = ls + self.removed_spans = [(a, b) for a, b in self.removed_spans if not (start <= a and b <= end)] + self.removed_spans.append((start, end)) + + def plan_order(self): + """TUT ran a file's groups in the byte order of their names (its + runner keeps them in a std::map keyed by name) and each group's + tests by number; Catch2 runs the cases as declared. So, walking + TUT's run order, a test declared before the one TUT ran just ahead + of it moves later in the file, never earlier, to just after the + latest-declared of those placed so far. A move stays in its own + namespace, though it may pass into a later block of it, and in its + #if branch: where one would have to leave them, the cases' order is + left as declared, but for each group's tests, put in their numeric + order where they can be, and the file is marked, since its cases + run in another order than TUT ran them.""" + # A test this host does not compile is in another #if branch: never + # declared beside these, it has no place in their order. + tests = [t for t in self.tests if not t.macro and t.body_end is not None and t.catch2_name + and not t.inactive and t.group is not None] + run = sorted(tests, key=lambda t: (t.group.name.encode("utf-8"), t.n)) + if run == sorted(tests, key=lambda t: t.start): + return + slot, stuck = self.slots(run) + if not stuck: + self.move(run, slot) + self.notes.append("tests moved into TUT's run order: groups by name, each group's tests by number") + return + first = min(stuck, key=lambda t: t.start) + groups = ", ".join(dict.fromkeys(t.group.name for t in run)) + self.check(first.start, f"TUT ran this file's groups by name ({groups}), each group's tests by number, and " + f"{first.group.name} test<{first.n}> cannot move to its place in that order out " + f"of its namespace or #if: these cases run in another order than TUT's") + self.notes.append("the cases run in another order than TUT ran them: a move out of a namespace or an #if " + "was needed") + for group in self.groups.values(): + own = [t for t in run if t.group is group] + if [t.n for t in sorted(own, key=lambda t: t.start)] == [t.n for t in own]: + continue + slot, stuck = self.slots(own) + if stuck: + for t in stuck: + self.check(t.start, f"test<{t.n}> is declared out of TUT's numeric order, and cannot move to " + f"its place out of its namespace or #if") + continue + self.move(own, slot) + self.notes.append(f"{group.name}: tests moved into TUT's numeric order") + + def slots(self, run): + """For tests in the order TUT ran them, the test each goes just after + (itself, where it stays), and those that would leave their + namespace or #if branch to get there.""" + slot = {} + latest = None + for test in run: + if latest is None or test.start > latest.start: + latest = test + slot[id(test)] = latest + stuck = [t for t in run if slot[id(t)] is not t and self.nesting(t.start) != self.nesting(slot[id(t)].start)] + return slot, stuck + + def move(self, run, slot): + """Plans that put each test just after its slot, in run order.""" + assigned = {} + for test in run: + assigned.setdefault(id(slot[id(test)]), []).append(test) + chunks = {id(t): self.chunk(t) for t in run} + for test in sorted(run, key=lambda t: t.start): + members = assigned.get(id(test), []) + if members == [test]: + continue + parts = [chunks[id(m)] for m in members] + start, end = chunks[id(test)] + if not members: + # A test moved away takes the blank line after it along, + # where nothing else is planned there. + after = self.src.line_end(end) + if end < len(self.src.data) and self.src.data[end:after].strip() == b"" and \ + not any(end <= p.start <= after or (p.start < end < p.end) for p in self.plans): + end = min(after + 1, len(self.src.data)) + plan = Plan(start, end, None) + plan.render = lambda conv, plan, parts=parts: "\n".join( + conv.render(s, e, skip=conv.slot_at.get(s)) for s, e in parts) + self.slot_at[plan.start] = plan + self.plans.append(plan) + + def nesting(self, offset): + """Where offset is, for a move: the namespaces around it, by name, + where it is directly in one, since every block of a namespace is + one scope and a later block of it has declared all an earlier one + did; else the brace of the block it is in; and its #if branches.""" + if self.live is None: + # Code under #if 0 need not even be C++: none of its braces count. + live = bytearray(self.src.masked) + for start, end, what in self.src.regions: + if what == "dead": + live[start:end] = re.sub(rb"[^\n]", b" ", bytes(live[start:end])) + self.live = bytes(live) + stack = [] # (where a { is, the namespace it opens or None) + last = 0 + for m in re.finditer(rb"[{};]", self.live[:offset]): + if m.group(0) == b"{": + head = b" ".join(line for line in self.live[last:m.start()].split(b"\n") + if not line.strip().startswith(b"#")) + name = re.fullmatch(rb"\s*(?:inline\s+)?namespace\s*([\w:]*)\s*", head) + stack.append((m.start(), name.group(1).decode() if name else None)) + elif m.group(0) == b"}" and stack: + stack.pop() + last = m.end() + if any(name is None for _, name in stack): + return ("block", stack[-1][0]), self.branch_of(offset) + path = tuple(part for _, name in stack for part in (name.split("::") if name else [""])) + return ("namespace", path), self.branch_of(offset) + + def chunk(self, test): + """A test's text with the comment lines right above it, through the end + of its last line.""" + start = self.src.line_start(test.start) + while start > 0: + prev = self.src.line_start(start - 1) + if self.src.masked[prev:start].strip() == b"" and self.src.data[prev:start].strip(): + start = prev + continue + break + end = self.src.line_end(test.body_end - 1) + return start, min(end + 1, len(self.src.data)) + + def branch_of(self, offset): + """The #if branches around offset, by where each starts.""" + stack = [] + for start, end, kw, _ in self.src.directives: + if start > offset: + break + if kw in ("if", "ifdef", "ifndef"): + stack.append(end) + elif kw in ("elif", "else", "elifdef", "elifndef") and stack: + stack[-1] = end + elif kw == "endif" and stack: + stack.pop() + return tuple(stack) + + def plan_includes(self): + found = list(TUT_INCLUDE.finditer(self.src.data)) + found = [m for m in found if self.src.state(m.start()) != "dead"] + headers = sorted(h for h in self.headers if h.startswith("<")) + \ + sorted(h for h in self.headers if h.startswith('"')) + lines = "".join(f"#include {h}\n" for h in headers) + if found: + first = found[0] + self.plans.append(Plan(first.start(), first.end(), lambda conv, plan: lines)) + for m in found[1:]: + self.plans.append(Plan(m.start(), m.end(), lambda conv, plan: "")) + else: + includes = list(re.finditer(rb"^[ \t]*#[ \t]*include[^\n]*\n", self.src.data, re.M)) + first_code = min((t.start for t in self.tests if t.start is not None), default=len(self.src.data)) + before = [m for m in includes if m.end() <= first_code and self.src.state(m.start()) == "active"] + at = before[-1].end() if before else 0 + self.plans.append(Plan(at, at, lambda conv, plan: lines)) + self.notes.append("no direct include of lltut.h: the Catch2 headers go after the last include") + + def plan_checks(self): + by_line = {} + for offset, why in self.checks: + by_line.setdefault(self.src.line_start(offset), []).append((offset, why)) + for start, entries in by_line.items(): + whys = "; ".join(dict.fromkeys(w for _, w in entries)) + if self.src.directive_at(start): + offset = min(o for o, _ in entries) + self.plans.append(Plan(offset, offset, lambda conv, plan, w=whys: f"/* TM-CHECK: {w} */ ")) + else: + indent = self.src.indent_at(start).decode() + self.plans.append(Plan(start, start, lambda conv, plan, w=whys, i=indent: f"{i}// TM-CHECK: {w}\n")) + + # ---- rendering ----------------------------------------------------- + def render(self, start, end, skip=None): + """The text of [start, end) with every plan inside it carried out.""" + out = [] + pos = start + for plan in self.sorted_plans: + if plan is skip or plan.start < pos: + continue + if plan.start == plan.end: + if plan.start >= end: + continue + elif plan.end > end: + if plan.start >= end: + break + continue + out.append(self.text(pos, plan.start)) + out.append(plan.text(self)) + pos = plan.end + out.append(self.text(pos, end)) + return "".join(out) + + def finish(self): + self.sorted_plans = sorted(self.plans, key=lambda p: (p.start, 0 if p.start == p.end else 1, -p.end)) + stack = [] + for plan in self.sorted_plans: + if plan.start == plan.end: + continue + while stack and stack[-1].end <= plan.start: + stack.pop() + if stack and plan.end > stack[-1].end: + raise ConvertError(f"{self.src.path}: edits overlap at line {self.src.line_of(plan.start)}") + stack.append(plan) + return self.render(0, len(self.src.data)) + + def counts(self, converted): + """Per test: the TUT assertion calls in its body before, the Catch2 + assertions in it after, and the Catch2 assertions those calls became; + and for the file, the TUT assertion calls outside every test's body + (a helper's, a fixture's, a lambda's at namespace scope) and the + Catch2 assertions those became. Calls are counted as in the bodies: + in a branch this host compiles or not, never under #if 0 or in a + comment, so a recount of the converted file finds as many.""" + tut = {} + expected = {} + inside = set() + for test in self.tests: + if test.body_start is None or test.body_end is None: + continue + calls = [o for o in self.counted if test.body_start <= o < test.body_end] + inside.update(calls) + tut[id(test)] = len(calls) + # A split && chain is more than one assertion for one TUT call. + expected[id(test)] = sum(self.emitted.get(o, 1) for o in calls) + data = converted.encode("utf-8", "surrogateescape") + catch = {id(t): n for t, n in count_catch_assertions(data, [ + (t, getattr(t, "header_text", f"TEST_CASE_METHOD({t.fixture}, {t.catch2_literal},")) + for t in self.tests if t.catch2_name and not t.macro])} + outside = [o for o in self.counted if o not in inside] + return tut, catch, expected, len(outside), sum(self.emitted.get(o, 1) for o in outside) + + +def count_catch_assertions(data, cases): + """For each (key, header text) in cases, the Catch2 assertions in the body + that follows the header in data: code under #if 0 left out.""" + masked = mask_source(data) + dead = dead_regions(masked) + out = [] + for key, header in cases: + at = data.find(header.encode("utf-8", "surrogateescape")) + if at < 0: + out.append((key, None)) + continue + brace = masked.find(b"{", at + len(header)) + depth = 0 + i = brace + while i < len(masked): + if masked[i] == 0x7B: + depth += 1 + elif masked[i] == 0x7D: + depth -= 1 + if depth == 0: + break + i += 1 + found = [m.start() for m in CATCH_ASSERTION.finditer(masked, brace, i + 1)] + out.append((key, sum(1 for f in found if not any(s <= f < e for s, e in dead)))) + return out + + +def dead_regions(masked): + """[start, end) of each #if 0 branch.""" + regions = [] + stack = [] + for m in re.finditer(rb"^[ \t]*#[ \t]*(if|ifdef|ifndef|elif|else|endif)\b([^\n]*)", masked, re.M): + kw, rest = m.group(1).decode(), m.group(2).decode("latin-1").strip() + if kw in ("if", "ifdef", "ifndef"): + stack.append([m.end(), kw == "if" and re.fullmatch(r"\(?\s*(0|false)\s*\)?", rest) is not None]) + elif stack: + start, dead = stack[-1] + if dead: + regions.append((start, m.start())) + if kw == "endif": + stack.pop() + else: + stack[-1] = [m.end(), kw == "elif" and re.fullmatch(r"\(?\s*(0|false)\s*\)?", rest) is not None] + return regions + + +def compared(node): + """The two operands of an == or != comparison, built in or overloaded, + or ().""" + kind = node.get("kind") + inner = [c for c in node.get("inner", []) if isinstance(c, dict) and "kind" in c] + if kind == "BinaryOperator" and node.get("opcode") in ("==", "!=") and len(inner) == 2: + return inner + if kind == "CXXOperatorCallExpr" and len(inner) == 3: + name = skip_implicit(inner[0]).get("referencedDecl", {}).get("name") + if name in ("operator==", "operator!="): + return inner[1:] + return () + + +def is_end(node): + """Whether a compared operand is where nothing is: null, a container's + end() and the like, or npos.""" + if node.get("kind") == "ImplicitCastExpr" and node.get("castKind") == "NullToPointer": + return True + node = skip_implicit(node) + kind = node.get("kind") + if kind in ("CXXNullPtrLiteralExpr", "GNUNullExpr"): + return True + if kind == "DeclRefExpr" and node.get("referencedDecl", {}).get("name") == "npos": + return True + if kind == "CXXMemberCallExpr": + callee = next((c for c in node.get("inner", []) if "kind" in c), {}) + return callee.get("kind") == "MemberExpr" and callee.get("name") in END_MEMBERS + return False + + +def variable_of(node): + """The declaration a node names, where it is a variable or a field.""" + kind = node.get("kind") + if kind == "DeclRefExpr": + ref = node.get("referencedDecl", {}) + if ref.get("kind") in ("VarDecl", "ParmVarDecl", "BindingDecl"): + return ref.get("id") + elif kind == "MemberExpr" and node.get("type", {}).get("qualType") != "": + return node.get("referencedMemberDecl") + return None + + +def variables(node, constants=True): + """The variables and fields a node reads, by declaration, without those + of default arguments; without, too, unless constants, a constant of + arithmetic type, which is no state.""" + found = set() + stack = [node] + while stack: + sub = stack.pop() + if sub.get("kind") == "CXXDefaultArgExpr": + continue + decl = variable_of(sub) + if decl: + t = sub.get("referencedDecl", {}).get("type", {}) if sub.get("kind") == "DeclRefExpr" \ + else sub.get("type", {}) + text = t.get("desugaredQualType") or t.get("qualType") or "" + if constants or not re.fullmatch(r"const\s+" + ARITHMETIC.pattern, text.strip()): + found.add(decl) + stack.extend(c for c in sub.get("inner", []) if isinstance(c, dict) and "kind" in c) + return found + + +# What a subscript through a class reads as a pointer would: a handle to +# something else, not a value of its own. +HANDLES = re.compile(r"iterator|__wrap_iter|optional|unique_ptr|shared_ptr|weak_ptr|LLPointer|_ptr\b|Ptr\b") + + +def side_effect(node): + """Whether evaluating node changes something of itself: ++ or --, an + assignment or compound assignment, built in or overloaded. A call may + too, but is not looked in: calls are too common in a condition, and + those whose failure leaves garbage are REQUIRE's business.""" + stack = [node] + while stack: + sub = stack.pop() + kind = sub.get("kind") + if kind in ("LambdaExpr", "CXXDefaultArgExpr"): + continue + inner = [c for c in sub.get("inner", []) if isinstance(c, dict) and "kind" in c] + if kind == "UnaryOperator" and sub.get("opcode") in ("++", "--"): + return True + if kind == "CompoundAssignOperator" or (kind == "BinaryOperator" and sub.get("opcode") == "="): + return True + if kind == "CXXOperatorCallExpr" and inner: + name = skip_implicit(inner[0]).get("referencedDecl", {}).get("name", "") + if re.fullmatch(r"operator(?:[-+*/%^&|]|<<|>>)?=|operator(?:\+\+|--)", name): + return True + stack.extend(inner) + return False + + +def leaned_on(node): + """The variables and fields a condition leans on for its own safety: + those it dereferences (unary *, ->, a class's operator* or + operator->), uses as a subscript or a pointer's offset, or subscripts + through where they are a pointer, an iterator, an optional or a smart + pointer. A value's member read with a dot, a constant subscript of a + value's array (v.mV[VX]) and a call are no such reliance. Constants of + arithmetic type are left out, as no state.""" + found = set() + stack = [node] + while stack: + sub = stack.pop() + kind = sub.get("kind") + if kind == "CXXDefaultArgExpr": + continue + inner = [c for c in sub.get("inner", []) if isinstance(c, dict) and "kind" in c] + if kind == "UnaryOperator" and sub.get("opcode") == "*" and inner: + found |= variables(inner[0], constants=False) + elif kind == "MemberExpr" and sub.get("isArrow") and inner: + found |= variables(inner[0], constants=False) + elif kind == "CXXOperatorCallExpr" and inner: + name = skip_implicit(inner[0]).get("referencedDecl", {}).get("name") + if name in ("operator*", "operator->") and len(inner) == 2: + found |= variables(inner[1], constants=False) + elif name == "operator[]" and len(inner) == 3: + found |= variables(inner[2], constants=False) + if HANDLES.search(value_type(skip_implicit(inner[1]))): + found |= variables(inner[1], constants=False) + elif kind == "ArraySubscriptExpr" and len(inner) == 2: + found |= variables(inner[1], constants=False) + # An array decays to the pointer subscripted; a pointer is one. + if "[" not in value_type(skip_implicit_casts(inner[0])): + found |= variables(inner[0], constants=False) + elif kind in ("BinaryOperator", "CompoundAssignOperator") and sub.get("opcode") in ("+", "-", "+=", "-=") \ + and len(inner) == 2: + for offset, other in ((inner[0], inner[1]), (inner[1], inner[0])): + if value_type(other).endswith("*") and not value_type(offset).endswith("*"): + found |= variables(offset, constants=False) + stack.extend(inner) + return found + + +def declared_bare(decl): + """Whether a variable is declared without an initializer: none at all, + or, for a class, its default constructor called with nothing written. + Whether that constructor sets the members is not in the AST read here + (the class is a header's; its JSON says nothing of it), and an empty + one, as LLVector4a's, leaves them indeterminate: a class counts, set + or not.""" + if "init" not in decl: + return True + if decl.get("init") != "call": + return False + inner = [c for c in decl.get("inner", []) if isinstance(c, dict) and "kind" in c] + if len(inner) != 1 or inner[0].get("kind") != "CXXConstructExpr" or inner[0].get("list"): + return False + return all(a.get("kind") == "CXXDefaultArgExpr" for a in inner[0].get("inner", []) if "kind" in a) + + +def written_root(node): + """The variable an expression a call writes through is part of: x of + x, x.m, x[i] where x is an array, &x; None where it is no variable's + own (what a pointer points at, a container's element).""" + while True: + kind = node.get("kind") + inner = [c for c in node.get("inner", []) if isinstance(c, dict) and "kind" in c] + if kind == "DeclRefExpr": + ref = node.get("referencedDecl", {}) + return ref.get("id") if ref.get("kind") == "VarDecl" else None + if not inner: + return None + if kind in ("ImplicitCastExpr", "ParenExpr", "MaterializeTemporaryExpr", "CXXBindTemporaryExpr") or \ + (kind == "MemberExpr" and not node.get("isArrow")) or \ + (kind == "UnaryOperator" and node.get("opcode") == "&") or \ + (kind == "ArraySubscriptExpr" and "[" in value_type(skip_implicit_casts(inner[0]))): + node = skip_implicit_casts(inner[0]) if kind == "ArraySubscriptExpr" else inner[0] + continue + return None + + +def const_object(base, arrow): + """Whether the object a member function is called on is const there, as + clang typed it: a const member function called on an object that is not + const has it converted to const, so a call on an object that stays + non-const is to a non-const member function.""" + text = base.get("type", {}).get("qualType", "") + if arrow and "*" in text: + text = text[:text.rfind("*")] + while re.search(r"<[^<>]*>", text): + text = re.sub(r"<[^<>]*>", "", text) + return re.search(r"\bconst\b", text) is not None + + +def fills(arg): + """Whether a call may fill an argument: an lvalue passed as it is, bound + to a non-const reference, or the address of one, to a pointer to + non-const. One bound to a const reference, or to a const pointer, is + converted to const first.""" + kind = arg.get("kind") + text = arg.get("type", {}).get("qualType", "") + if kind == "UnaryOperator" and arg.get("opcode") == "&": + return not const_object(arg, True) + if kind in ("ImplicitCastExpr", "CXXConstructExpr", "MaterializeTemporaryExpr", "CXXBindTemporaryExpr") or \ + arg.get("valueCategory") != "lvalue": + return False + while re.search(r"<[^<>]*>", text): + text = re.sub(r"<[^<>]*>", "", text) + return re.search(r"\bconst\b", text) is None + + +def names_only(record): + """Whether a class derived from test_group holds nothing but a + constructor, taking nothing and doing nothing, that passes the group its + name: nothing TUT ran around the group's tests goes with it.""" + own = [c for c in record.get("inner", []) if "kind" in c and not c.get("isImplicit") + and c.get("kind") != "AccessSpecDecl"] + if len(own) != 1 or own[0].get("kind") != "CXXConstructorDecl": + return False + parts = [c for c in own[0].get("inner", []) if "kind" in c] + inits = [c for c in parts if c.get("kind") == "CXXCtorInitializer"] + rest = [c for c in parts if c.get("kind") != "CXXCtorInitializer"] + if len(inits) != 1 or "baseInit" not in inits[0]: + return False + return len(rest) == 1 and rest[0].get("kind") == "CompoundStmt" and \ + not [c for c in rest[0].get("inner", []) if "kind" in c] + + +def exclusive(a, b): + """Whether code at two places, given by Source.conditionals_at, is never + compiled together: only where they lie in different branches of one + #if. Different #ifs may both hold, whatever their conditions say.""" + for (chain_a, branch_a), (chain_b, branch_b) in zip(a, b): + if chain_a != chain_b: + return False + if branch_a != branch_b: + return True + return False + + +def test_body(test): + return next((c for c in test.node.get("inner", []) if c.get("kind") == "CompoundStmt"), None) \ + if test.node else None + + +def skip_implicit_casts(node): + while node.get("kind") in ("ImplicitCastExpr", "MaterializeTemporaryExpr", "CXXBindTemporaryExpr") \ + and node.get("inner"): + node = node["inner"][0] + return node + + +def find_kind(node, kind): + for sub in iter_nodes(node): + if sub.get("kind") == kind: + return sub + return None + + +def iter_nodes(node): + stack = [node] + while stack: + cur = stack.pop() + if isinstance(cur, dict): + if "kind" in cur: + yield cur + stack.extend(reversed([c for c in cur.get("inner", []) if isinstance(c, dict)])) + + +def split_template_args(text): + out = [] + depth = 0 + cur = [] + for ch in text: + if ch in "<([{": + depth += 1 + elif ch in ">)]}": + depth -= 1 + if ch == "," and depth == 0: + out.append("".join(cur).strip()) + cur = [] + continue + cur.append(ch) + out.append("".join(cur).strip()) + return out + + +def needs_macro_guard(masked_text): + """Whether text, inside a macro that does not take __VA_ARGS__, would be + split: a comma outside parentheses, or a macro that could expand to one.""" + depth = 0 + for c in masked_text: + if c == 0x28: + depth += 1 + elif c == 0x29: + depth -= 1 + elif c == 0x2C and depth == 0: + return True + return bool(re.search(rb"\{|\b[A-Z][A-Z0-9_]{2,}\s*\(", masked_text)) + + +def text_statements(name, texts, macro, kind=None): + """The statements a call becomes, from its arguments' text alone: for + code no AST shows (only the calls whose types do not matter), and for a + call in a macro, whose kind the macro's uses decided and whose + arguments may be anything, so are parenthesised where they meet an + operator.""" + simple = re.compile(r'[\w.:]+(\(\s*\))?|"(?:[^"\\]|\\.)*"') + kind = kind or name + + def operand(t): + return t if simple.fullmatch(t) else f"({t})" + + def condition(t): + if macro: + return operand(t) + if re.search(r"&&|\|\||\?|[^=!<>]=[^=]|,", t) and not wrapped(t): + return f"({t})" + return t + + def message(t): + if t in ("NULL", "nullptr", "0", '""'): + return [] + return [f"UNSCOPED_INFO({t if simple.fullmatch(t) else f'({t})'})"] + n = len(texts) + if name in ("ensure", "ensure_not"): + macro_name = "REQUIRE_FALSE" if name == "ensure_not" else "REQUIRE" + if n not in (1, 2): + return None + return (message(texts[0]) if n == 2 else []) + [f"{macro_name}({condition(texts[-1])})"] + if name in ("fail", "skip", "throw"): + if n != 1: + return None + return [f"{'SKIP' if name == 'skip' else 'FAIL'}({operand(texts[0])})"] + if name in ("ensure_equals", "ensure_not_equals"): + if n not in (2, 3): + return None + msg = message(texts[0]) if n == 3 else [] + a, e = operand(texts[-2]), operand(texts[-1]) + if kind == "equals-llsd": + return msg + [f"REQUIRE_THAT({a}, ll_test::LLSDEquals({e}))"] + if kind == "equals-uri": + return msg + [f"REQUIRE({a}.asString() == {e}.asString())"] + return msg + [f"REQUIRE({a} {'==' if name == 'ensure_equals' else '!='} {e})"] + if name in ("ensure_contains", "ensure_does_not_contain", "ensure_starts_with", "ensure_ends_with"): + if n != 3: + return None + matcher = {"ensure_contains": "Catch::Matchers::ContainsSubstring", + "ensure_does_not_contain": "!Catch::Matchers::ContainsSubstring", + "ensure_starts_with": "Catch::Matchers::StartsWith", + "ensure_ends_with": "Catch::Matchers::EndsWith"}[name] + return message(texts[0]) + [f"REQUIRE_THAT({operand(texts[1])}, {matcher}({texts[2]}))"] + if name == "ensure_distance" and kind.startswith("distance-"): + if n not in (3, 4): + return None + msg = message(texts[0]) if n == 4 else [] + a, e, d = texts[-3:] + return msg + [f"REQUIRE_THAT({operand(a)}, ll_test::WithinDistance<{kind[9:]}>({e}, {d}))"] + if name == "ensure_approximately_equals_range": + if n != 4: + return None + a, e, d = texts[-3:] + return message(texts[0]) + [f"REQUIRE_THAT(F32({a}), Catch::Matchers::WithinAbs(F32({e}), F32({d})))"] + if name == "ensure_approximately_equals" and kind in ("bits-F32", "bits-F64"): + if n not in (3, 4): + return None + msg = message(texts[0]) if n == 4 else [] + a, e, bits = texts[-3:] + return msg + [f"REQUIRE_THAT({operand(a)}, ll_test::WithinBits({kind[5:]}({e}), {bits}))"] + return None + + +def wrapped(text): + """Whether text is one parenthesised whole.""" + if not (text.startswith("(") and text.endswith(")")): + return False + depth = 0 + for i, ch in enumerate(text): + depth += ch == "(" + depth -= ch == ")" + if depth == 0 and i < len(text) - 1: + return False + return True + + +def group_tag(name): + tag = re.sub(r"[^A-Za-z0-9_:+-]+", "_", name).strip("_") + if not tag or not tag[0].isalnum(): + tag = "tut_" + tag + return tag + + +# -------------------------------------------------------------------------- +# One file +# -------------------------------------------------------------------------- +_FRIENDS = None + + +def tut_friends(): + """The tut:: types production code names as friends: they keep their + namespace, so a file holding one keeps namespace tut.""" + global _FRIENDS + if _FRIENDS is None: + _FRIENDS = set() + for top, dirs, files in os.walk(os.path.join(ROOT, "indra")): + dirs[:] = [d for d in dirs if d not in ("tests", "test", "externals")] + for f in files: + if f.endswith((".h", ".hpp", ".inl")): + with open(os.path.join(top, f), "rb") as fh: + for m in re.finditer(rb"friend\s+(?:struct|class)\s+(?:::\s*)?tut\s*::\s*(\w+)", fh.read()): + _FRIENDS.add(m.group(1).decode()) + return _FRIENDS + + +def load_names(path): + """--names: {"": {"": {"": ""}}}.""" + if not path: + return {} + with open(path) as f: + data = json.load(f) + out = {} + for file, groups in data.items(): + for group, tests in groups.items(): + for n, name in tests.items(): + out[(os.path.normpath(file), group, int(n))] = name + return out + + +def convert_file(path, build_dir, config, dry_run, output_dir=None, names=None): + db = CompileDatabase(build_dir, config) + source = Source(path) + if not TUT_INCLUDE.search(source.data) and b"test_group" not in source.data: + return {"file": path, "skipped": "no TUT here", "tests": [], "checks": [], "notes": []} + items, tut_ids, active_lines = run_clang(db, path) + if os.environ.get("TUT2CATCH_DUMP"): + with open(os.environ["TUT2CATCH_DUMP"], "w") as f: + json.dump(items, f, indent=1) + mangled = sorted({n.get("mangledName") for item in items for n in iter_nodes(item) + if n.get("kind") == "CXXMethodDecl" and n.get("name") == "test" + and "test_object" in n.get("mangledName", "")}) + conv = Converter(source, items, tut_ids, active_lines, demangle(mangled)) + rel = os.path.normpath(os.path.relpath(path, ROOT)) + conv.name_overrides = {(g, n): name for (f, g, n), name in (names or {}).items() if f == rel} + conv.friend_names = tut_friends() + sys.setrecursionlimit(max(sys.getrecursionlimit(), 20000)) + conv.analyse() + conv.resolve_calls() + conv.convert_uncompiled() + conv.resolve_tests() + conv.remove_uncompiled_groups() + conv.name_tests() + conv.plan_fixtures() + conv.plan_structure() + conv.plan_order() + conv.plan_includes() + conv.plan_checks() + converted = conv.finish() + if conv.can_unwrap(converted): + conv.plan_unwrap() + converted = conv.finish() + tut_counts, catch_counts, expected_counts, tut_outside, catch_outside = conv.counts(converted) + if output_dir: + out = os.path.join(output_dir, os.path.relpath(path, ROOT)) + os.makedirs(os.path.dirname(out), exist_ok=True) + with open(out, "wb") as f: + f.write(converted.encode("utf-8", "surrogateescape")) + elif not dry_run: + with open(path, "wb") as f: + f.write(converted.encode("utf-8", "surrogateescape")) + tests = [] + for test in sorted(conv.tests, key=lambda t: ((t.group.name if t.group else ""), t.n or 0)): + tests.append({ + "file": os.path.relpath(path, ROOT), + "group": test.group.name if test.group else None, + "n": test.n, + "line": source.line_of(test.start) if test.start is not None else None, + "tut_name": test.tut_name, + "catch2_name": test.catch2_name, + "tut_assertions": tut_counts.get(id(test)), + "catch2_expected": expected_counts.get(id(test)), + "catch2_assertions": catch_counts.get(id(test)), + "not_compiled_here": test.inactive, + "past_group_limit": bool(test.group and test.n and test.n > test.group.limit), + }) + checks = [{"file": os.path.relpath(path, ROOT), "line": source.line_of(o), "why": w} + for o, w in sorted(conv.checks)] + totals = {"file": os.path.relpath(path, ROOT), "tut_sha256": hashlib.sha256(source.data).hexdigest(), + "catch2_sha256": hashlib.sha256(converted.encode("utf-8", "surrogateescape")).hexdigest(), + "tut_outside_calls": tut_outside, "catch2_outside_expected": catch_outside} + return {"file": os.path.relpath(path, ROOT), "tests": tests, "totals": totals, "checks": checks, + "notes": conv.notes, + "groups": sorted(g.name for g in conv.groups.values())} + + +def convert_one(args): + path, build_dir, config, dry_run, output_dir, names = args + result = {} + + def work(): + try: + result["value"] = convert_file(path, build_dir, config, dry_run, output_dir, names) + except ConvertError as e: + result["value"] = {"file": os.path.relpath(path, ROOT), "error": str(e), "tests": [], "checks": [], + "notes": []} + except Exception: + # A bug of the converter's own, met in one file: it is that + # file's error, traceback and all, and the others go on. + result["value"] = {"file": os.path.relpath(path, ROOT), "error": traceback.format_exc().rstrip(), + "tests": [], "checks": [], "notes": []} + # Deep ASTs recurse deeply. + threading.stack_size(512 * 1024 * 1024) + t = threading.Thread(target=work) + t.start() + t.join() + return result["value"] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__) + parser.add_argument("files", nargs="+", help="TUT test files to convert in place") + parser.add_argument("--build-dir", default=os.path.join(ROOT, DEFAULT_BUILD), + help="the build tree whose compile_commands.json gives each file's flags") + parser.add_argument("--config", default=DEFAULT_CONFIG, help="which configuration's flags to use") + parser.add_argument("--mapping", help="write the TUT-to-Catch2 mapping, as JSON, here") + parser.add_argument("--dry-run", action="store_true", help="convert nothing; report and map only") + parser.add_argument("--output-dir", help="write each converted file under here, at its path from the " + "repository's root, instead of over the original") + parser.add_argument("--jobs", type=int, default=1, help="files to convert at once") + parser.add_argument("--quiet", action="store_true", help="print only the marks and the totals") + parser.add_argument("--names", help='cases\' names, as JSON: {"": {"": ' + '{"": ""}}}; each wins over the test\'s own') + args = parser.parse_args() + + files = [os.path.abspath(f) for f in args.files] + names = load_names(args.names) + work = [(f, os.path.abspath(args.build_dir), args.config, args.dry_run, args.output_dir, names) for f in files] + if args.jobs > 1: + with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: + results = list(pool.map(convert_one, work)) + else: + results = [convert_one(w) for w in work] + + mapping = [] + totals = [] + total_checks = 0 + errors = 0 + for r in results: + if r.get("error"): + errors += 1 + print(f"{r['file']}: error: {r['error']}") + continue + if r.get("skipped"): + if not args.quiet: + print(f"{r['file']}: {r['skipped']}") + continue + mapping.extend(r["tests"]) + totals.append(r["totals"]) + total_checks += len(r["checks"]) + if not args.quiet: + converted = sum(1 for t in r["tests"] if t["catch2_name"]) + print(f"{r['file']}: {converted} of {len(r['tests'])} tests in {len(r['groups'])} group(s), " + f"{len(r['checks'])} TM-CHECK") + for note in r["notes"]: + print(f" note: {note}") + for c in r["checks"]: + print(f"{c['file']}:{c['line']}: TM-CHECK: {c['why']}") + if args.mapping: + with open(args.mapping, "w") as f: + json.dump({"tests": mapping, "files": totals}, f, indent=1) + print(f"{len(results)} file(s), {len(mapping)} test(s), {total_checks} TM-CHECK(s), {errors} error(s)") + return 2 if errors else (1 if total_checks else 0) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/testing/tut_parity.py b/scripts/testing/tut_parity.py new file mode 100644 index 0000000000..41825ad42e --- /dev/null +++ b/scripts/testing/tut_parity.py @@ -0,0 +1,821 @@ +#!/usr/bin/env python3 +"""Proves a TUT file and its Catch2 conversion test the same things. + +Two steps, either side of scripts/testing/tut2catch.py: + + record before converting: runs TUT test executables, verbose, through + the commands, working directories and environments their CTest + entries give, and keeps every line a test's end prints: + [group, N: name] ok | fail | exception: ... | skipped known failure | ... + A test TUT never reported (a dummy test slot) is not a test. + With them it keeps the sha256 of the TUT source, the one + indra/**/tests/.cpp git tracks, and it refuses an + executable built before that source last changed. A test built at other + x86-64 tiers too (an entry ending _baseline, _v2, _v3, _v4) is + kept a tier apart, 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 + beside this script: the converted file is read as it reads one. + A file built at other tiers too has an entry + _/ at each, held to TUT's run at that + 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. + +Parity holds for a file when: + - its inputs are the ones the run is of: the converted file is no newer + than the executable its entry runs, and where the recorded results + and the mapping both have the TUT source's sha256, the two agree (where + either has none, that is listed); and the file is the one the converter + wrote, by the mapping's catch2_sha256, unless --recounted says it was + edited by hand and the edits were reviewed -- an edit that weakens an + assertion keeps every count, so nothing else here would see it; + - every TUT test it reported has exactly one Catch2 case, and every case + of the file is one of its TUT tests; + - each pair agrees: ok is a pass, a TUT skip is a Catch2 skip, and every + TUT failure (fail, exception, abnormal end) is a Catch2 failure; and the + name TUT printed is the one the mapping folded into the case's; + - the cases run in the order TUT ran their tests, groups by name and + each group's tests by number, since a test may lean on what the ones + before it left; + - no two cases share a name (Catch2 takes two that differ in their tags), + none is tagged [!mayfail] or [!shouldfail], under which Catch2 passes a + case whatever it does, and the run's exit code fits its cases: 0 when + some passed and none failed, 125 when all skipped, neither when any + failed -- or, where none failed, 1 when the harness says its state + guard found a change, but not when it says a teardown step threw; + - each case's assertions, counted in the converted file (read as + tut2catch.py reads it: comments, literals and #if 0 code left out), + number its TUT test's ensure, fail and skip calls, as the mapping + counted them in the TUT file, where a && chain the converter split + counts one a term (catch2_expected). After a file has been edited by + hand past what the converter wrote, --recounted lists the counts + instead of holding them to the mapping; names and results must still + agree, and a case whose TUT test asserted anything must still assert; + - the assertions outside every case (helpers, fixtures) number what the + mapping's record of the file says those outside every TUT test became + (catch2_outside_expected); under --recounted, listed, but none where + TUT had some is still a problem. A mapping in its first form, a bare + list of tests, has no file records: that is listed; + - every assertion counted in a passing case's body ran: the run keeps its + passing assertions (-s), and each CHECK, REQUIRE and _THAT site must + report from at least one line it spans. A FAIL or SKIP that ran would + have failed or skipped the case, and a case that failed or skipped may + have stopped early, so neither is held to it; nor is one the build + leaves out, which the file's compile command in the build's + compile_commands.json, run as clang++ -E, says. Where there is no such + command, one under an #if that never ran is listed. + +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 +outside every case (a helper's) that no case ran. So is what the Catch2 +harness's state guard says a case left changed: TUT never looked, so it is +a finding about the test, not a difference the conversion made; the +table's exit column shows the run's exit code. It prints a table, one row a +file (and one a tier it is built at besides), and exits 1 on any mismatch, +listing each. + + python3 -I scripts/testing/tut_parity.py record --out /tmp/tut.json \\ + INTEGRATION_TEST_RUNNER_v3math PROJECT_llmath_TEST_v3color + python3 -I scripts/testing/tut2catch.py --mapping /tmp/map.json indra/llmath/tests/v3*_test.cpp + (build the Catch2 binary) + python3 -I scripts/testing/tut_parity.py check --tut /tmp/tut.json --mapping /tmp/map.json +""" +import argparse +import bisect +import hashlib +import importlib.util +import json +import os +import re +import subprocess +import sys +import tempfile +import xml.etree.ElementTree as ElementTree + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +DEFAULT_BUILD = "build-Darwin-ninja-os" +DEFAULT_CONFIG = "RelWithDebInfo" +# What every test entry exits with when it skips: the harness turns Catch2's +# "all skipped" into it, and CTest reads it as skipped. +SKIP_CODE = 125 +# The x86-64 tiers a test is built for beside the tree's own, lowest first. +TIERS = ("baseline", "v2", "v3", "v4") +# Tags under which Catch2 passes a case whatever it does. +FORGIVING_TAGS = ("[!mayfail]", "[!shouldfail]") +RESULT_LINE = re.compile( + r"^\[(?P.+?), (?P\d+)(?:: (?P.*?))?\] " + r"(?Pok|fail|exception|test destructor throw|abnormal termination|skipped known failure|unknown)\b") +# Assertions that report only when they run, and then fail or skip the case: +# in a case that passed, one of these not running is the pass. +REPORTING_ONLY = {"FAIL", "FAIL_CHECK", "SKIP"} +# What a run's XML says an assertion ran with, and where. +RAN_ELEMENTS = {"Expression", "Failure", "Skip"} + + +# -------------------------------------------------------------------------- +# CTest's view of a test +# -------------------------------------------------------------------------- +def ctest_tests(build_dir, config): + out = subprocess.run(["ctest", "--test-dir", build_dir, "-C", config, "--show-only=json-v1"], + capture_output=True, text=True, check=True) + tests = {} + for t in json.loads(out.stdout).get("tests", []): + props = {p["name"]: p["value"] for p in t.get("properties", [])} + tests[t["name"]] = {"command": t.get("command", []), "props": props} + return tests + + +def environment(props): + env = dict(os.environ) + for item in props.get("ENVIRONMENT", []): + key, _, value = item.partition("=") + env[key] = value + for item in props.get("ENVIRONMENT_MODIFICATION", []): + key, _, rest = item.partition("=") + op, _, value = rest.partition(":") + old = env.get(key, "") + if op == "reset": + env.pop(key, None) if key not in os.environ else env.__setitem__(key, os.environ[key]) + elif op == "set": + env[key] = value + elif op == "unset": + env.pop(key, None) + elif op == "string_append": + env[key] = old + value + elif op == "string_prepend": + env[key] = value + old + elif op == "path_list_append": + env[key] = old + os.pathsep + value if old else value + elif op == "path_list_prepend": + env[key] = value + os.pathsep + old if old else value + elif op == "cmake_list_append": + env[key] = old + ";" + value if old else value + elif op == "cmake_list_prepend": + env[key] = value + ";" + old if old else value + return env + + +def run(test, extra, timeout): + props = test["props"] + try: + proc = subprocess.run(test["command"] + extra, cwd=props.get("WORKING_DIRECTORY") or None, + env=environment(props), capture_output=True, timeout=timeout) + return proc.returncode, proc.stdout.decode("utf-8", "replace") + proc.stderr.decode("utf-8", "replace") + except subprocess.TimeoutExpired as e: + return "timeout", (e.stdout or b"").decode("utf-8", "replace") + + +def isa_tier(name): + """The x86-64 tier a TUT entry's or a Catch2 binary's name ends in + (_baseline, _v2, _v3, _v4), it being built for that tier; None for one + built at the tree's own.""" + m = re.search(r"_(" + "|".join(TIERS) + r")$", 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.""" + 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 + + +def tut_source(stem): + """The TUT source a stem names, as git tracks it under ROOT, and the + sha256 of its bytes: (None, None) unless exactly one + indra/**/tests/.cpp is tracked.""" + try: + listing = subprocess.run(["git", "-C", ROOT, "ls-files", "-z", "--", f":(glob)indra/**/tests/{stem}.cpp"], + capture_output=True, check=True).stdout + except (OSError, subprocess.CalledProcessError): + return None, None + paths = [p.decode("utf-8", "surrogateescape") for p in listing.split(b"\0") if p] + if len(paths) != 1: + return None, None + with open(os.path.join(ROOT, paths[0]), "rb") as f: + return paths[0], hashlib.sha256(f.read()).hexdigest() + + +# -------------------------------------------------------------------------- +# record +# -------------------------------------------------------------------------- +def record(args): + tests = ctest_tests(args.build_dir, args.config) + out = {} + if args.out and os.path.exists(args.out): + with open(args.out) as f: + out = json.load(f) + for name in args.tests: + test = tests.get(name) + if test is None: + print(f"{name}: no such CTest entry", file=sys.stderr) + return 2 + stem, tier = tut_identity(name) + # 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) + if source is None: + print(f"{name}: not exactly one tracked indra/**/tests/{stem}.cpp, so no source hash kept", + file=sys.stderr) + # An executable older than its source is not of it: its results + # would stand for tests the file no longer has. + command = test["command"] + if source is not None and command and os.path.isfile(command[0]) and \ + os.path.getmtime(os.path.join(ROOT, source)) > os.path.getmtime(command[0]): + print(f"{name}: {source} changed since {command[0]} was built; build it again to record it", + file=sys.stderr) + return 2 + code, text = run(test, ["-v"], args.timeout) + results = [] + for line in text.splitlines(): + m = RESULT_LINE.match(line) + if not m: + continue + result = m.group("result") + results.append({"group": m.group("group"), "n": int(m.group("n")), "name": m.group("name") or "", + "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} + counts = {} + for r in results: + counts[r["result"]] = counts.get(r["result"], 0) + 1 + print(f"{name}: exit {code}, " + (", ".join(f"{v} {k}" for k, v in sorted(counts.items())) or "no results")) + if args.out: + with open(args.out, "w") as f: + json.dump(out, f, indent=1) + return 0 + + +# -------------------------------------------------------------------------- +# check +# -------------------------------------------------------------------------- +def catch2_results(test, timeout): + """The cases a Catch2 entry ran, as XML with its passing assertions too + (-s), in the order it ran them: each {name, tags, line, result: ok | skip + | fail, ran: {(file, line) of each assertion that ran}}. A list, not a + dict by name: Catch2 takes two cases of one name that differ in their + tags. Read as it streams: a case that loops over its checks writes + megabytes.""" + with tempfile.TemporaryDirectory() as tmp: + report = os.path.join(tmp, "report.xml") + code, text = run(test, ["-s", "-r", f"xml::out={report}"], timeout) + cases = [] + case = None + try: + for event, element in ElementTree.iterparse(report, events=("start", "end")): + if event == "start": + if element.tag == "TestCase": + case = {"name": element.get("name"), "tags": element.get("tags", ""), + "line": int(element.get("line", "0")), "result": "fail", "ran": set()} + continue + if case is None: + continue + if element.tag in RAN_ELEMENTS: + case["ran"].add((element.get("filename", ""), int(element.get("line", "0")))) + element.clear() + elif element.tag == "OverallResult": + if element.get("success") != "true": + case["result"] = "fail" + elif int(element.get("skips", "0")) > 0: + case["result"] = "skip" + else: + case["result"] = "ok" + elif element.tag == "TestCase": + cases.append(case) + case = None + element.clear() + except (ElementTree.ParseError, FileNotFoundError): + return code, None, text + return code, cases, text + + +def exit_fits(code, cases, output): + """Whether a run's exit code says what its cases do: 0 when some passed + and none failed, the skip code when every case skipped, and neither when + any failed or none ran -- a filter that matches nothing exits 2. Where + none failed, the harness exits 1 for the state guard's finding a change, + which is listed, and for a teardown step's throwing, which is not: a + check at static destruction ended a TUT process.""" + results = {c["result"] for c in cases} + if "fail" in results or not results: + return code not in (0, SKIP_CODE, "timeout") + if code == 1 and "The state guard found" in output and "teardown step(s) threw" not in output: + return True + return code == (SKIP_CODE if results == {"skip"} else 0) + + +_CONVERTER = None + + +def converter(): + """tut2catch.py, beside this script: the file is read with the masking + and the #if 0 regions the mapping's counts were made with. By path, as + python -I puts no script's directory on sys.path.""" + global _CONVERTER + if _CONVERTER is None: + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tut2catch.py") + spec = importlib.util.spec_from_file_location("tut2catch", path) + _CONVERTER = importlib.util.module_from_spec(spec) + spec.loader.exec_module(_CONVERTER) + return _CONVERTER + + +def read_cases(path): + """The converted file's test cases and assertion sites, read from the file + as tut2catch.py reads it: comments and literals masked, #if 0 branches + left out. A site is (first line, last line, macro, the #if blocks that + hold it), the lines its call spans; a case is {name, lines, sites, the + #if blocks that hold it}. Returns (the cases, the sites outside every + case).""" + tc = converter() + with open(path, "rb") as f: + data = f.read() + masked = tc.mask_source(data) + dead = tc.dead_regions(masked) + conditional = conditional_regions(masked) + newlines = [m.start() for m in re.finditer(rb"\n", masked)] + + def live(at): + return not any(start <= at < end for start, end in dead) + + def held(at): + return frozenset(i for i, (start, end) in enumerate(conditional) if start <= at < end) + + def line(at): + return bisect.bisect_left(newlines, at) + 1 + + sites = [] + for m in tc.CATCH_ASSERTION.finditer(masked): + if live(m.start()): + close = matching(masked, m.end() - 1) + macro = m.group(0).rstrip(b"( \t\r\n").decode() + sites.append((m.start(), (line(m.start()), line(close), macro, held(m.start())))) + cases = [] + inside = set() + for m in re.finditer(rb"\bTEST_CASE(_METHOD)?\s*\(", masked): + if not live(m.start()): + continue + open_paren = m.end() - 1 + close = matching(masked, open_paren) + header = data[open_paren + 1:close].decode("utf-8", "surrogateescape") + # The name is the first argument of a TEST_CASE, the second of a + # TEST_CASE_METHOD. + literal = re.search(r',\s*((?:"(?:[^"\\]|\\.)*"\s*)+),' if m.group(1) else + r'^\s*((?:"(?:[^"\\]|\\.)*"\s*)+),', header) + if not literal: + continue + name = "".join(unescape(p) for p in re.findall(r'"((?:[^"\\]|\\.)*)"', literal.group(1))) + brace = masked.find(b"{", close) + end = matching(masked, brace) + body = [(at, site) for at, site in sites if brace < at < end] + inside.update(at for at, _ in body) + cases.append({"name": name, "lines": (line(m.start()), line(brace)), "sites": [site for _, site in body], + "held": held(m.start())}) + return cases, [site for at, site in sites if at not in inside] + + +def conditional_regions(masked): + """[start, end) of each #if block, but an include guard's: code a build + may leave out.""" + regions = [] + stack = [] + for m in re.finditer(rb"^[ \t]*#[ \t]*(if|ifdef|ifndef|endif)\b([^\n]*)", masked, re.M): + if m.group(1) != b"endif": + name = m.group(2).strip() + guard = m.group(1) == b"ifndef" and re.match(rb"\s*#[ \t]*define[ \t]+" + re.escape(name) + rb"\b", + masked[m.end():]) is not None + stack.append((m.end(), guard)) + elif stack: + start, guard = stack.pop() + if not guard: + regions.append((start, m.start())) + return regions + + +class CompiledLines: + """The lines of a file this build compiles: its compile command, from + compile_commands.json as tut2catch.py reads it, run as clang++ -E. None + for a file with no command to be had, or one that fails.""" + + def __init__(self, build_dir, config): + self.build_dir = build_dir + self.config = config + self.database = None + self.loaded = False + + def __call__(self, path): + tc = converter() + # As the build names it, through any link on the way. + path = os.path.realpath(path) + if not self.loaded: + self.loaded = True + try: + self.database = tc.CompileDatabase(self.build_dir, self.config) + except tc.ConvertError: + pass + if self.database is None: + return None + try: + directory, args, compiled = self.database.command(path) + proc = subprocess.run(args + ["-E", compiled], cwd=directory, capture_output=True) + except (tc.ConvertError, OSError): + return None + if proc.returncode != 0: + return None + kept = set() + line = 0 + in_main = False + for raw in proc.stdout.split(b"\n"): + m = re.match(rb'^# (\d+) "((?:[^"\\]|\\.)*)"', raw) + if m: + line = int(m.group(1)) + named = os.path.join(directory, m.group(2).decode("utf-8", "replace")) + in_main = os.path.basename(named) == os.path.basename(path) and os.path.realpath(named) == path + continue + if in_main and raw.strip(): + kept.add(line) + line += 1 + return kept + + +def matching(masked, at): + """Where the bracket at at closes, in masked text.""" + depth = 0 + for i in range(at, len(masked)): + c = masked[i] + if c in b"({[": + depth += 1 + elif c in b")}]": + depth -= 1 + if depth == 0: + return i + return len(masked) + + +def ran_here(case, path): + """The lines of path a case's run reported an assertion from.""" + suffix = "/" + path.replace("\\", "/") + return {line for file, line in case["ran"] if ("/" + file.replace("\\", "/")).endswith(suffix)} + + +def body_of(case, file_cases): + """The file's case a run's case is: by name, and by the line Catch2 says + it starts on where two have one name.""" + named = [c for c in file_cases if c["name"] == case["name"]] + if len(named) > 1: + named = [c for c in named if c["lines"][0] <= case["line"] <= c["lines"][1]] + return named[0] if len(named) == 1 else None + + +def unescape(body): + """The value of the inside of a narrow string literal.""" + simple = {"n": "\n", "t": "\t", "r": "\r", "0": "\0", "\\": "\\", '"': '"', "'": "'", "a": "\a", + "b": "\b", "f": "\f", "v": "\v", "?": "?"} + out = [] + i = 0 + while i < len(body): + ch = body[i] + if ch == "\\" and i + 1 < len(body): + nxt = body[i + 1] + if nxt == "x": + m = re.match(r"[0-9A-Fa-f]+", body[i + 2:]) + out.append(chr(int(m.group(0), 16)) if m else "x") + i += 2 + (len(m.group(0)) if m else 0) + continue + if nxt in "01234567": + m = re.match(r"[0-7]{1,3}", body[i + 1:]) + out.append(chr(int(m.group(0), 8))) + i += 1 + len(m.group(0)) + continue + out.append(simple.get(nxt, nxt)) + i += 2 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def read_mapping(path): + """tut2catch.py's mapping: its tests, and its records of the files by path + -- None for a mapping in the first form it took, a bare list of tests.""" + with open(path) as f: + mapping = json.load(f) + if isinstance(mapping, list): + return mapping, None + return mapping["tests"], {record["file"]: record for record in mapping.get("files", [])} + + +def tree_tier(build_dir): + """The ISA tier the build tree is built for: AL_ISA_TIER in its cache.""" + try: + with open(os.path.join(build_dir, "CMakeCache.txt"), errors="replace") as f: + m = re.search(r"^AL_ISA_TIER:\w+=(\S+)$", f.read(), re.M) + except OSError: + return None + return m.group(1) if m else None + + +def check_run(args, label, path, entry_name, test, recorded, entries, file_cases, outside, kept, record, problems): + """One Catch2 entry's run of a converted file against TUT's run of its + original: appends what does not hold to problems, prints what is listed, + and returns the table's row, or None where there is no run to show. kept + is the file's lines this build compiles, or None where that is not + known.""" + 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. + command = test["command"] + if command and os.path.isfile(command[0]) and \ + os.path.getmtime(os.path.join(ROOT, path)) > os.path.getmtime(command[0]): + problems.append(f"{label}: changed since {command[0]} was built; build it again to check it") + return None + code, cases, output = catch2_results(test, args.timeout) + # Where the host cannot run a tier, either side exits with the skip code + # before running anything: TUT with no results, Catch2 with no XML. + tut_skipped = recorded["exit"] == SKIP_CODE and not recorded["results"] + catch2_skipped = code == SKIP_CODE and cases is None + if tut_skipped and catch2_skipped: + print(f"{label}: listed: TUT and Catch2 both skipped it (exit {SKIP_CODE}): the host does not run its tier") + return label, 0, 0, 0, 0, 0, 0, code, "skipped" + if tut_skipped or catch2_skipped: + problems.append(f"{label}: TUT {'skipped' if tut_skipped else 'ran'} it (exit {recorded['exit']}), " + f"{entry_name} {'skipped' if catch2_skipped else 'ran'} it (exit {code})") + return None + if cases is None: + problems.append(f"{label}: {entry_name} gave no XML (exit {code}):\n{output[-2000:]}") + return None + guard = [line for line in output.splitlines() if line.startswith("State guard:")] + # The first case of a name is the one matched; a second is an error of + # its own. + by_name = {} + for case in cases: + if case["name"] in by_name: + problems.append(f"{label}: Catch2 has more than one case named {case['name']!r}") + else: + by_name[case["name"]] = case + for tag in FORGIVING_TAGS: + if tag in case["tags"]: + problems.append(f"{label}: {case['name']!r} is tagged {tag}, under which Catch2 passes it " + f"whatever it does") + if not exit_fits(code, cases, output): + counts = {} + for case in cases: + counts[case["result"]] = counts.get(case["result"], 0) + 1 + said = [line.strip() for line in output.splitlines() + if "teardown step(s) threw" in line or "The state guard found" in line] + problems.append(f"{label}: {entry_name} exited {code}, which does not fit its cases (" + + (", ".join(f"{v} {k}" for k, v in sorted(counts.items())) or "none") + ")" + + "".join(f"; it said: {line}" for line in said)) + tut_results = {(r["group"], r["n"]): r for r in recorded["results"]} + # Counted in the body that ran: a test in both branches of an #if is + # two cases of one name in the file, and the run names the line of its. + counted = {} + for name, case in by_name.items(): + body = body_of(case, file_cases) + counted[name] = len(body["sites"]) if body else None + # An assertion counted is not one that ran: a case passes as well when a + # loop around its CHECK never turns. Each site in a passing case's body + # this build compiles must have reported at least once. Where that is + # not known, one under an #if the case is not under (a case that ran was + # compiled) is listed. + def compiled(first, last, conditional): + return any(n in kept for n in range(first, last + 1)) if kept is not None else \ + (None if conditional else True) + + # Listed for the run, apart from the tests listed. + notes = [] + ran_anywhere = set() + for case in cases: + ran = ran_here(case, path) + ran_anywhere |= ran + if case["result"] != "ok": + continue + body = body_of(case, file_cases) + for first, last, macro, blocks in body["sites"] if body else []: + if macro in REPORTING_ONLY or any(first <= n <= last for n in ran): + continue + here = compiled(first, last, bool(blocks - body["held"])) + if here: + problems.append(f"{label}: {case['name']!r} passed without running the {macro} on line {first}") + elif here is None: + notes.append(f"{case['name']!r}: the {macro} on line {first} never ran, and is under an #if no " + f"compile command here says this build compiles") + notes += [f"the {macro} on line {first}, outside every case, never ran" + for first, last, macro, blocks in outside + if macro not in REPORTING_ONLY and not any(first <= n <= last for n in ran_anywhere) + and compiled(first, last, bool(blocks)) is not False] + # The TUT results must be of the source the mapping was made from. + hashes = {"the recorded results": recorded.get("tut_sha256"), "the mapping": (record or {}).get("tut_sha256")} + if all(hashes.values()): + if len(set(hashes.values())) > 1: + problems.append(f"{label}: TUT's results were recorded from another {os.path.basename(path)} than the " + f"one converted (sha256 {hashes['the recorded results'][:12]}, the mapping's " + f"{hashes['the mapping'][:12]})") + else: + notes.append(f"no TUT source hash in {' or '.join(k for k, v in hashes.items() if not v)}, " + f"so the two are not known to be of one source") + matched = agree = counts_ok = 0 + listed = [] + seen_cases = set() + paired = {} + for e in entries: + key = (e["group"], e["n"]) + name = e["catch2_name"] + # Before taking TUT's result: a test defined in both branches of + # an #if is two entries, and the result is the active twin's. + if e.get("not_compiled_here") or (e.get("past_group_limit") and key not in tut_results): + listed.append(f"{e['group']} {e['n']}: " + + ("not compiled here" if e.get("not_compiled_here") else "past its group's limit")) + if name in by_name: + seen_cases.add(name) + continue + t = tut_results.pop(key, None) + if t is None: + problems.append(f"{label}: {e['group']} test<{e['n']}> did not run under TUT") + continue + if not name or name not in by_name: + problems.append(f"{label}: {e['group']} test<{e['n']}> has no Catch2 case" + + (f" ({name!r})" if name else "")) + continue + seen_cases.add(name) + paired[name] = key + matched += 1 + if e.get("tut_name") is not None and e["tut_name"] != t["name"]: + problems.append(f"{label}: {e['group']} test<{e['n']}>: TUT named it {t['name']!r}, " + f"the mapping {e['tut_name']!r}") + if by_name[name]["result"] == t["result"]: + agree += 1 + else: + problems.append(f"{label}: {name!r}: TUT {t['said']!r}, Catch2 {by_name[name]['result']}") + static = counted.get(name) + # One TUT call is one Catch2 assertion, but for a && chain split into + # one assertion a term, which the mapping counted. + expected = e.get("catch2_expected", e["tut_assertions"]) + if expected == e["catch2_assertions"] == static: + counts_ok += 1 + elif args.recounted and static == 0 and e["tut_assertions"] > 0: + problems.append(f"{label}: {name!r}: {e['tut_assertions']} TUT assertion calls, none in the file") + elif args.recounted and static is not None: + counts_ok += 1 + print(f"{label}: listed: {name!r}: {expected} assertions converted, {static} in the file now") + else: + problems.append(f"{label}: {name!r}: {e['tut_assertions']} TUT assertion calls, " + f"{expected} Catch2 assertions they became, {e['catch2_assertions']} mapped, " + f"{static} in the file") + for key, t in sorted(tut_results.items()): + problems.append(f"{label}: TUT ran [{key[0]}, {key[1]}], which the mapping does not have") + for name in sorted(set(by_name) - seen_cases): + problems.append(f"{label}: Catch2 case {name!r} is no TUT test of the mapping") + # TUT ran a file's groups by name and each group's tests by number, and a + # test may lean on what the ones before it left. + keys = set(paired.values()) + tut_order = [(r["group"], r["n"]) for r in recorded["results"] if (r["group"], r["n"]) in keys] + catch2_order = [(paired[c["name"]], c["name"]) for c in cases if c["name"] in paired and by_name[c["name"]] is c] + for at, (tut_key, (key, name)) in enumerate(zip(tut_order, catch2_order)): + if tut_key != key: + problems.append(f"{label}: Catch2 runs {name!r}, [{key[0]}, {key[1]}], as test {at + 1} of " + f"{len(tut_order)}, where TUT ran [{tut_key[0]}, {tut_key[1]}]: not TUT's order") + break + for line in listed + notes + guard: + print(f"{label}: listed: {line}") + return (label, len(recorded["results"]), len(cases), matched, agree, counts_ok, len(listed), code, + "ok" if len(problems) == first_problem else "MISMATCH") + + +def check(args): + mapping, file_records = read_mapping(args.mapping) + 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. + records = {} + for key, recorded in tut.items(): + 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) + by_file = {} + for entry in mapping: + by_file.setdefault(entry["file"], []).append(entry) + files = sorted(by_file) if not args.files else [os.path.relpath(os.path.abspath(f), ROOT) for f in args.files] + + rows = [] + problems = [] + for path in files: + entries = by_file.get(path, []) + stem = os.path.splitext(os.path.basename(path))[0] + file_problems = [] + file_cases, outside = read_cases(os.path.join(ROOT, path)) + kept = compiled_lines(os.path.join(ROOT, path)) + record = (file_records or {}).get(path) + # A file edited past what the converter wrote is held to the + # converter's counts only once --recounted says it was looked at: an + # edit that kept every count, a weakened assertion, shows nowhere + # else. + if record is not None: + with open(os.path.join(ROOT, path), "rb") as f: + written = hashlib.sha256(f.read()).hexdigest() == record.get("catch2_sha256") + if not record.get("catch2_sha256"): + print(f"{path}: listed: no hash in the mapping of the file the converter wrote") + elif not written and args.recounted: + print(f"{path}: listed: edited since the converter wrote it") + elif not written: + file_problems.append(f"{path}: edited since the converter wrote it; review the edits, then " + f"check it with --recounted") + # What no test's count holds: the assertions outside every test body + # (helpers, fixtures), which the mapping totals for the file. + if file_records is None: + print(f"{path}: listed: no file totals in the mapping") + elif record is None: + file_problems.append(f"{path}: the mapping has no record of the file") + elif len(outside) != record["catch2_outside_expected"]: + if args.recounted and (outside or not record["tut_outside_calls"]): + print(f"{path}: listed: {record['catch2_outside_expected']} assertions outside every case " + f"converted, {len(outside)} in the file now") + else: + file_problems.append(f"{path}: {record['tut_outside_calls']} TUT assertion calls outside every " + f"test, {record['catch2_outside_expected']} Catch2 assertions they became, " + f"{len(outside)} outside every case in the file") + # The file's Catch2 entries, /, by the tier the binary + # 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. + runs = {} + for name in tests: + if name.endswith("/" + stem): + runs.setdefault(isa_tier(name[:-len(stem) - 1]), []).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): + label = f"{path} ({tier})" if tier else path + if len(runs[tier]) != 1: + if tier: + file_problems.append(f"{label}: {len(runs[tier])} Catch2 CTest entries end in _{tier}/{stem}") + continue + key = (stem, tier) + 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 "")) + continue + used.add(key) + row = check_run(args, label, path, runs[tier][0], tests[runs[tier][0]], records[key], entries, + file_cases, outside, kept, record, problems) + if row: + file_rows.append(row) + # TUT's runs no Catch2 entry was checked against: its run at the + # tree's own tier is the untiered one again, where that was checked. + for key in sorted((k for k in records if k[0] == stem and k not in used), key=lambda k: k[1] or ""): + if key[1] and key[1] == own_tier and (stem, None) in used: + 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") + + f" ({records[key]['ctest']}), and no Catch2 entry does") + problems.extend(file_problems) + for row in file_rows: + rows.append(row[:-1] + ("MISMATCH",) if file_problems else row) + + header = ("file", "TUT", "Catch2", "matched", "agree", "counts", "listed", "exit", "parity") + widths = [max(len(str(r[i])) for r in rows + [header]) for i in range(len(header))] + for r in [header] + rows: + print(" ".join(str(v).ljust(w) if i == 0 else str(v).rjust(w) for i, (v, w) in enumerate(zip(r, widths)))) + for p in problems: + print(f"mismatch: {p}") + return 1 if problems else 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], + formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__) + parser.add_argument("--build-dir", default=os.path.join(ROOT, DEFAULT_BUILD)) + parser.add_argument("--config", default=DEFAULT_CONFIG) + parser.add_argument("--timeout", type=float, default=900, help="seconds a run may take") + sub = parser.add_subparsers(dest="step", required=True) + rec = sub.add_parser("record", help="run TUT executables and keep their results") + rec.add_argument("tests", nargs="+", help="CTest names of TUT tests") + rec.add_argument("--out", required=True, help="JSON to add the results to") + chk = sub.add_parser("check", help="run the converted files' Catch2 entries and compare") + chk.add_argument("--tut", required=True, help="what record kept") + chk.add_argument("--mapping", required=True, help="tut2catch.py's mapping") + chk.add_argument("--recounted", action="store_true", + 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("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) + + +if __name__ == "__main__": + sys.exit(main())