diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9c3bb2c04..1e66acd18 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -108,9 +108,16 @@ The tests are located in [`tests/src/unit-*.cpp`](https://github.com/nlohmann/js are structured along the features of the library or the nature of the tests. Usually, it should be clear from the context which existing file needs to be extended, and only very few cases require creating new test files. -When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue. -`unit-regression2.cpp` holds the older tests; the two files exist because a single one grew large enough for the -MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing the larger one. +When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue. Tests that need C++17 or +C++20 go into `unit-regression3-cpp17.cpp` or `unit-regression3-cpp20.cpp` instead, so the large file is not rebuilt +for every C++ standard. `unit-regression2.cpp` holds the older tests; the two files exist because a single one grew +large enough for the MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing +the larger one. + +To keep compile times down, several test files are compiled together as one translation unit. This sets a few rules +for test files, such as giving file-scope helpers file-specific names. See the README in the +[`tests`](https://github.com/nlohmann/json/tree/develop/tests) folder for how the tests are built and what to keep in +mind when adding them. #### Exceptions diff --git a/cmake/ci.cmake b/cmake/ci.cmake index 15345c5d6..be2338030 100644 --- a/cmake/ci.cmake +++ b/cmake/ci.cmake @@ -871,11 +871,11 @@ add_custom_target(ci_icpx # The following tests are excluded as they trigger known nvc++ 25.5 defects (not # library bugs); see https://github.com/nlohmann/json for tracking. Only the # affected language-standard variants are excluded so coverage is otherwise kept: -# - test-comparison_cpp20, test-comparison_legacy_cpp20 +# - test-comparison-cpp20_cpp20, test-comparison_legacy-cpp20_cpp20 # miscompiles cross-type/<=> comparison (e.g. `-17 <= null`) # - test-constructor1_cpp11 # std::initializer_list lifetime bug -> SIGSEGV -# - test-deserialization_cpp20 +# - test-deserialization-cpp20_cpp20 # mangles the UTF-8 u8"" string literal in the char8_t (C++20) section add_custom_target(ci_nvhpc COMMAND ${CMAKE_COMMAND} @@ -887,7 +887,7 @@ add_custom_target(ci_nvhpc COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_nvhpc # the pipes are escaped so the surrounding shell passes them to ctest verbatim # instead of treating them as shell pipe operators - COMMAND cd ${PROJECT_BINARY_DIR}/build_nvhpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-comparison_cpp20\\|test-comparison_legacy_cpp20\\|test-constructor1_cpp11\\|test-deserialization_cpp20" --output-on-failure + COMMAND cd ${PROJECT_BINARY_DIR}/build_nvhpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-comparison-cpp20_cpp20\\|test-comparison_legacy-cpp20_cpp20\\|test-constructor1_cpp11\\|test-deserialization-cpp20_cpp20" --output-on-failure COMMENT "Compile and test with NVIDIA HPC SDK (nvc++)" ) diff --git a/cmake/test.cmake b/cmake/test.cmake index 5922c5106..2c622dab3 100644 --- a/cmake/test.cmake +++ b/cmake/test.cmake @@ -109,6 +109,55 @@ function(_json_test_apply_test_properties test_target properties_target) endif() endfunction() +# for internal use by _json_test_add_test() and _json_test_add_unity_batch(): +# registers the CTest test _cpp (plus its Valgrind +# variant), which runs the executable target with the arguments +# in ARGN, and applies the test properties of the test- and standard-specific +# interface targets +function(_json_test_register_test test_name test_target cxx_standard) + set(ctest_name ${test_name}_cpp${cxx_standard}) + + if (JSON_FastTests) + add_test(NAME ${ctest_name} + COMMAND ${test_target} ${DOCTEST_TEST_FILTER} ${ARGN} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + else() + add_test(NAME ${ctest_name} + COMMAND ${test_target} ${DOCTEST_TEST_FILTER} ${ARGN} --no-skip + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + endif() + set_tests_properties(${ctest_name} PROPERTIES LABELS "all" FIXTURES_REQUIRED TEST_DATA) + + # apply standard-specific test properties + if(TARGET _json_test_interface__cpp_${cxx_standard}) + _json_test_apply_test_properties(${ctest_name} _json_test_interface__cpp_${cxx_standard}) + endif() + + # apply test-specific test properties + if(TARGET _json_test_interface_${test_name}) + _json_test_apply_test_properties(${ctest_name} _json_test_interface_${test_name}) + endif() + + # apply test- and standard-specific test properties + if(TARGET _json_test_interface_${test_name}_cpp_${cxx_standard}) + _json_test_apply_test_properties(${ctest_name} + _json_test_interface_${test_name}_cpp_${cxx_standard} + ) + endif() + + if(JSON_Valgrind) + add_test(NAME ${ctest_name}_valgrind + COMMAND ${memcheck_command} $ ${DOCTEST_TEST_FILTER} ${ARGN} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + set_tests_properties(${ctest_name}_valgrind PROPERTIES + LABELS "valgrind" FIXTURES_REQUIRED TEST_DATA + ) + endif() +endfunction() + # for internal use by json_test_add_test_for() function(_json_test_add_test test_name file main cxx_standard) set(test_target ${test_name}_cpp${cxx_standard}) @@ -143,45 +192,7 @@ function(_json_test_add_test test_name file main cxx_standard) ) endif() - if (JSON_FastTests) - add_test(NAME ${test_target} - COMMAND ${test_target} ${DOCTEST_TEST_FILTER} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - ) - else() - add_test(NAME ${test_target} - COMMAND ${test_target} ${DOCTEST_TEST_FILTER} --no-skip - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - ) - endif() - set_tests_properties(${test_target} PROPERTIES LABELS "all" FIXTURES_REQUIRED TEST_DATA) - - # apply standard-specific test properties - if(TARGET _json_test_interface__cpp_${cxx_standard}) - _json_test_apply_test_properties(${test_target} _json_test_interface__cpp_${cxx_standard}) - endif() - - # apply test-specific test properties - if(TARGET _json_test_interface_${test_name}) - _json_test_apply_test_properties(${test_target} _json_test_interface_${test_name}) - endif() - - # apply test- and standard-specific test properties - if(TARGET _json_test_interface_${test_name}_cpp_${cxx_standard}) - _json_test_apply_test_properties(${test_target} - _json_test_interface_${test_name}_cpp_${cxx_standard} - ) - endif() - - if(JSON_Valgrind) - add_test(NAME ${test_target}_valgrind - COMMAND ${memcheck_command} $ ${DOCTEST_TEST_FILTER} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - ) - set_tests_properties(${test_target}_valgrind PROPERTIES - LABELS "valgrind" FIXTURES_REQUIRED TEST_DATA - ) - endif() + _json_test_register_test(${test_name} ${test_target} ${cxx_standard}) endfunction() ############################################################################# @@ -200,6 +211,14 @@ endfunction() # Use NAME to override the filename-derived test name. # Use FORCE to create the test regardless of the file containing # JSON_HAS_CPP_. +# +# Tests that depend on the C++ standard (e.g., because they use the macros +# JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +# should not make the whole of a large unit-foo.cpp be rebuilt for every +# standard. Put them into a separate file unit-foo-cpp.cpp (see, e.g., +# unit-items-cpp17.cpp) which wraps its content in #ifdef JSON_HAS_CPP_. +# Then, unit-foo.cpp itself contains no JSON_HAS_CPP_ and is only built for +# C++11. # Test targets are linked against
. # CXX_STANDARDS defaults to "11". ############################################################################# @@ -243,6 +262,300 @@ function(json_test_add_test_for file) endforeach() endfunction() +# for internal use by json_test_add_unity_tests(): sets to whether the +# (absolute) is built for ; same rule as in +# json_test_add_test_for(): C++11 always, others only if the file contains +# JSON_HAS_CPP_ or is set +function(_json_test_unity_applies file cxx_standard force result) + set(${result} TRUE PARENT_SCOPE) + if(NOT ("${cxx_standard}" STREQUAL 11 OR force)) + file(READ ${file} file_content) + string(FIND "${file_content}" JSON_HAS_CPP_${cxx_standard} has_cpp_found) + if(${has_cpp_found} EQUAL -1) + set(${result} FALSE PARENT_SCOPE) + endif() + endif() +endfunction() + +# for internal use by json_test_add_unity_tests(): creates the executable +# test-unity-_cpp from the (absolute) source files +# in ARGN, which are #include-d by a generated source file, and registers one +# CTest test per source file that runs only the test cases of that file; if +# is true, the generated file defines JSON_TESTS_PRIVATE first +function(_json_test_add_unity_batch batch_name cxx_standard main private) + set(batch_target test-unity-${batch_name}_cpp${cxx_standard}) + set(batch_source ${PROJECT_BINARY_DIR}/tests/unity/${batch_target}.cpp) + + set(batch_content "// generated by cmake/test.cmake; do not edit\n") + if(private) + string(APPEND batch_content "// at least one file of this batch needs access to private members of the library\n") + string(APPEND batch_content "#define JSON_TESTS_PRIVATE\n") + endif() + foreach(file ${ARGN}) + string(APPEND batch_content "#include \"${file}\"\n") + endforeach() + + # only touch the generated file if it changed to keep incremental builds incremental + set(old_content "") + if(EXISTS ${batch_source}) + file(READ ${batch_source} old_content) + endif() + if(NOT "${old_content}" STREQUAL "${batch_content}") + file(WRITE ${batch_source} "${batch_content}") + endif() + + add_executable(${batch_target} ${batch_source}) + target_link_libraries(${batch_target} PRIVATE ${main}) + set_target_properties(${batch_target} PROPERTIES + CXX_STANDARD ${cxx_standard} + CXX_STANDARD_REQUIRED ON + ) + if(TARGET _json_test_interface__cpp_${cxx_standard}) + target_link_libraries(${batch_target} PRIVATE _json_test_interface__cpp_${cxx_standard}) + endif() + + # rebuild the batch when one of its files changes, and show the files in IDEs; + # files that are also built standalone (VARIANT_FILES) are left out, as + # HEADER_FILE_ONLY is a per-file property and would also affect those targets + set_source_files_properties(${batch_source} PROPERTIES OBJECT_DEPENDS "${ARGN}") + foreach(file ${ARGN}) + if(NOT file IN_LIST _json_test_unity_variant_files) + set_source_files_properties(${file} PROPERTIES HEADER_FILE_ONLY ON) + target_sources(${batch_target} PRIVATE ${file}) + endif() + endforeach() + + foreach(file ${ARGN}) + get_filename_component(file_basename ${file} NAME_WE) + string(REGEX REPLACE "unit-(.+)" "test-\\1" test_name ${file_basename}) + + # run only the test cases defined in this file (and in the shared + # make_test_data_available.hpp if the file uses it) + file(READ ${file} file_content) + set(source_filter "--source-file=*${file_basename}.cpp") + string(FIND "${file_content}" make_test_data_available.hpp uses_test_data) + if(NOT ${uses_test_data} EQUAL -1) + string(APPEND source_filter ",*make_test_data_available.hpp") + endif() + + _json_test_register_test(${test_name} ${batch_target} ${cxx_standard} "${source_filter}") + endforeach() +endfunction() + +############################################################################# +# json_test_add_unity_tests( +# FILES ... +# MAIN
+# [CXX_STANDARDS ...] [FORCE] +# [BATCH_SIZE ] +# [GROUPS ...] +# [VARIANT_FILES ...]) +# +# Like calling json_test_add_test_for( MAIN
...) for each of the +# , but compiles several files together to speed up the build: for each +# C++ standard, the files are split into batches of files (default: 8) +# and each batch is built as a single executable +# +# test-unity-_cpp +# +# whose generated source file #include-s the files of the batch (so they share +# the template instantiations of the library). The tests are still named +# test-foo_cpp, one per file, but run the batch executable with +# a doctest filter that selects the test cases of that file only. +# +# Files are only batched with files that agree on the macros defined before +# the library is included: files that define at most JSON_TESTS_PRIVATE (or +# macros derived from global compile definitions) form the pools "plain" and +# "private". All other files are added with json_test_add_test_for() as usual, +# as are files with test-specific build settings (see +# json_test_set_test_options()) and the files listed in the explicit +# exclusion list below. +# Each names a list variable json_test_unity_group_ of test file +# stems (file names without "unit-" and ".cpp"). The batchable files of a group +# are compiled together (regardless of BATCH_SIZE) as one executable +# +# test-unity-_cpp +# +# so that related tests, which instantiate the same templates, share one +# translation unit. A group may mix the pools "plain" and "private"; if any of +# its files needs JSON_TESTS_PRIVATE, the whole group is built with it. Files +# that cannot be batched stay standalone even if they are listed in a group. +# Files in no group are batched by BATCH_SIZE as described above. +# in VARIANT_FILES are also built standalone with other settings, so +# they are not marked as header-only sources of the batch. +############################################################################# + +function(json_test_add_unity_tests) + cmake_parse_arguments(args "FORCE" "MAIN;BATCH_SIZE" "FILES;CXX_STANDARDS;VARIANT_FILES;GROUPS" ${ARGN}) + + if("${args_MAIN}" STREQUAL "") + message(FATAL_ERROR "Required argument MAIN
missing.") + endif() + + if("${args_BATCH_SIZE}" STREQUAL "") + set(args_BATCH_SIZE 8) + endif() + + if("${args_CXX_STANDARDS}" STREQUAL "") + set(args_CXX_STANDARDS 11) + endif() + + if(args_FORCE) + set(force FORCE) + else() + set(force "") + endif() + + set(_json_test_unity_variant_files "") + foreach(file ${args_VARIANT_FILES}) + get_filename_component(file ${file} ABSOLUTE) + list(APPEND _json_test_unity_variant_files ${file}) + endforeach() + + # files that must not be merged into a batch: unit-32bit.cpp is only built + # for 32bit targets, and unit-no-macro-leak.cpp checks that including the + # library defines no unprefixed macro, which any other file would disturb + set(standalone_files unit-32bit.cpp unit-no-macro-leak.cpp) + + set(harmless_macros "^(DOCTEST_.*|SKIP_TESTS_FOR_.*|JSON_TEST_DEPRECATED_FUNCTIONS_DELETED|JSON_TEST_STRICT_NUL_HANDLING_ENABLED|JSON_TEST_STRINGIZE)$") + + # classify the files + set(plain_files "") + set(private_files "") + set(standalone_abs_files "") + foreach(file ${args_FILES}) + get_filename_component(file_name ${file} NAME) + get_filename_component(file_basename ${file} NAME_WE) + string(REGEX REPLACE "unit-(.+)" "test-\\1" test_name ${file_basename}) + + set(batchable TRUE) + if(file_name IN_LIST standalone_files) + set(batchable FALSE) + endif() + if(TARGET _json_test_interface_${test_name}) + set(batchable FALSE) + endif() + foreach(cxx_standard ${args_CXX_STANDARDS}) + if(TARGET _json_test_interface_${test_name}_cpp_${cxx_standard}) + set(batchable FALSE) + endif() + endforeach() + + set(pool plain) + if(batchable) + # collect the macros (un)defined before the library is included + file(READ ${file} file_content) + string(FIND "${file_content}" "#include _files are the (absolute) files + # of the group, group__private tells whether one of them is private + set(grouped_files "") + foreach(group ${args_GROUPS}) + if(NOT DEFINED json_test_unity_group_${group}) + message(FATAL_ERROR "Unity test group '${group}' is not defined (json_test_unity_group_${group}).") + endif() + set(group_${group}_files "") + set(group_${group}_private FALSE) + foreach(stem ${json_test_unity_group_${group}}) + # check against the source directory, because FILES may be filtered (JSON_TestShard) + get_filename_component(file ${CMAKE_CURRENT_SOURCE_DIR}/src/unit-${stem}.cpp ABSOLUTE) + if(NOT EXISTS ${file}) + message(FATAL_ERROR "Unity test group '${group}' lists '${stem}', but ${file} does not exist.") + endif() + if(file IN_LIST grouped_files) + message(FATAL_ERROR "Unity test file unit-${stem}.cpp is listed in more than one group (second: '${group}').") + endif() + list(APPEND grouped_files ${file}) + + if(file IN_LIST standalone_abs_files) + message(STATUS "Unity test group '${group}': unit-${stem}.cpp cannot be batched and stays standalone") + elseif(file IN_LIST plain_files) + list(APPEND group_${group}_files ${file}) + list(REMOVE_ITEM plain_files ${file}) + elseif(file IN_LIST private_files) + list(APPEND group_${group}_files ${file}) + list(REMOVE_ITEM private_files ${file}) + set(group_${group}_private TRUE) + endif() + endforeach() + endforeach() + + foreach(cxx_standard ${args_CXX_STANDARDS}) + if(NOT compiler_supports_cpp_${cxx_standard}) + continue() + endif() + + # explicit groups: one batch per group + foreach(group ${args_GROUPS}) + set(batch_files "") + foreach(file ${group_${group}_files}) + _json_test_unity_applies(${file} ${cxx_standard} "${force}" applies) + if(applies) + list(APPEND batch_files ${file}) + endif() + endforeach() + if(batch_files) + _json_test_add_unity_batch(${group} ${cxx_standard} ${args_MAIN} ${group_${group}_private} ${batch_files}) + endif() + endforeach() + + # remaining files: batches of BATCH_SIZE files per pool + foreach(pool plain private) + set(is_private FALSE) + if(pool STREQUAL private) + set(is_private TRUE) + endif() + + set(batch_files "") + set(batch_count 0) + set(batch_index 0) + foreach(file ${${pool}_files}) + _json_test_unity_applies(${file} ${cxx_standard} "${force}" applies) + if(NOT applies) + continue() + endif() + + list(APPEND batch_files ${file}) + math(EXPR batch_count "${batch_count} + 1") + if(batch_count EQUAL args_BATCH_SIZE) + _json_test_add_unity_batch(${pool}${batch_index} ${cxx_standard} ${args_MAIN} ${is_private} ${batch_files}) + set(batch_files "") + set(batch_count 0) + math(EXPR batch_index "${batch_index} + 1") + endif() + endforeach() + if(batch_files) + _json_test_add_unity_batch(${pool}${batch_index} ${cxx_standard} ${args_MAIN} ${is_private} ${batch_files}) + endif() + endforeach() + endforeach() +endfunction() + ############################################################################# # json_test_should_build_32bit_test( # ) diff --git a/docs/mkdocs/docs/integration/cmake.md b/docs/mkdocs/docs/integration/cmake.md index 9492efe9f..4ae1a7721 100644 --- a/docs/mkdocs/docs/integration/cmake.md +++ b/docs/mkdocs/docs/integration/cmake.md @@ -235,6 +235,17 @@ Build the unit tests against the [simdutf](https://github.com/simdutf/simdutf) U its version is set by the cache variable `JSON_SIMDUTF_VERSION`. This option is `OFF` by default. Depends on `JSON_BuildTests`. +### `JSON_TestUnityBuild` + +Build the unit tests in batches of several test files per executable to speed up compilation: the files of a batch are +compiled as one translation unit, so the template instantiations of the library are shared. Every test file still gets +its own CTest test (e.g., `test-foo_cpp11`), which runs only the test cases of that file from the shared executable. +This option is `ON` by default (and `OFF` with MinGW). Set it to `OFF` to get one executable per test file, e.g., when +debugging a single file. The number of files per executable is set by the cache variable `JSON_TestUnityBatchSize` +(default: `8`). Related test files (e.g., all binary formats) are grouped explicitly in `tests/CMakeLists.txt`; each +group is compiled as one executable per C++ standard regardless of the batch size, and only the remaining files are +batched by size. Depends on `JSON_BuildTests`. + ### `JSON_Valgrind` Execute the test suite with [Valgrind](https://valgrind.org). This option is `OFF` by default. Depends on `JSON_BuildTests`. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9318838bb..66b0cb01b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,12 +2,14 @@ cmake_minimum_required(VERSION 3.13...4.0) option(JSON_Valgrind "Execute test suite with Valgrind." OFF) option(JSON_FastTests "Skip expensive/slow tests." OFF) +option(JSON_TestUnityBuild "Build the unit tests in batches of several files per executable to speed up compilation." ON) option(JSON_TestSimdutf "Build the unit tests against the simdutf UTF-8 validation backend." OFF) set(JSON_SIMDUTF_VERSION 9.1.0 CACHE STRING "The simdutf version used by JSON_TestSimdutf.") set(JSON_32bitTest AUTO CACHE STRING "Enable the 32bit unit test (ON/OFF/AUTO/ONLY).") set(JSON_TestStandards "" CACHE STRING "The list of standards to test explicitly.") +set(JSON_TestUnityBatchSize 8 CACHE STRING "The number of unit test files compiled into one executable with JSON_TestUnityBuild.") set(JSON_TestShard "" CACHE STRING "Build only a part of the unit tests, given as / (e.g. 0/2), to split them across CI jobs with a time limit.") # using an env var, since this will also affect targets executing cmake (such as "ci_test_compiler_default") @@ -20,6 +22,11 @@ endif() include(test) +# MinGW's GNU ld hits section and relocation limits with the large merged translation units +if(MINGW) + set(JSON_TestUnityBuild OFF) +endif() + ############################################################################# # override standard support ############################################################################# @@ -322,9 +329,28 @@ if(NOT "${JSON_TestShard}" STREQUAL "") message(STATUS "Test shard ${JSON_TestShard}: ${shard_file_count} of ${test_file_count} unit test files") endif() -foreach(file ${files}) - json_test_add_test_for(${file} MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}) -endforeach() +# Explicit groups of test files (file names without "unit-" and ".cpp") for +# JSON_TestUnityBuild: all files of a group are compiled into one translation +# unit per C++ standard, so that related tests share their template +# instantiations (all binary formats instantiate the same readers and +# writers). Only add a group where this was measured to pay off: grouping +# files that share little just creates one slow translation unit. Files in no +# group are batched alphabetically by JSON_TestUnityBatchSize. +set(json_test_unity_groups binary) +set(json_test_unity_group_binary binary_formats binary_utf8_error_handler binary_writer_sinks bjdata bon8 bon8-cpp17 bson byte_container_with_subtype cbor msgpack msgpack-cpp17 ubjson) + +if(JSON_TestUnityBuild) + message(STATUS "Building the unit tests in batches of ${JSON_TestUnityBatchSize} files (JSON_TestUnityBuild)") + json_test_add_unity_tests(FILES ${files} MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} + BATCH_SIZE ${JSON_TestUnityBatchSize} + GROUPS ${json_test_unity_groups} + VARIANT_FILES src/unit-comparison.cpp src/unit-comparison-cpp20.cpp src/unit-class_parser.cpp src/unit-diagnostic-positions.cpp + ) +else() + foreach(file ${files}) + json_test_add_test_for(${file} MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}) + endforeach() +endif() # tests/src/unit-no-macro-leak.cpp #include-s the generated leak-check file, # so its test targets must be built after generate_hedley_undef_checks. @@ -350,6 +376,13 @@ json_test_add_test_for(src/unit-comparison.cpp NAME test-comparison_legacy MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} ) +json_test_set_test_options(test-comparison_legacy-cpp20 + COMPILE_DEFINITIONS JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1 +) +json_test_add_test_for(src/unit-comparison-cpp20.cpp + NAME test-comparison_legacy-cpp20 + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} +) # test the parser again with JSON_DIAGNOSTIC_POSITIONS enabled json_test_set_test_options(test-class_parser_diagnostic_positions diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..c55d93afa --- /dev/null +++ b/tests/README.md @@ -0,0 +1,92 @@ +# Unit tests + +The unit tests are in [`src/unit-*.cpp`](src) and use [doctest](https://github.com/doctest/doctest). Each file becomes +one CTest test per C++ standard it is built for, named after the file: `src/unit-foo.cpp` becomes `test-foo_cpp11`. + +## Build and run + +```sh +cmake -S . -B build -DJSON_BuildTests=ON +cmake --build build -j 10 +ctest --test-dir build -j 10 +``` + +The [CMake options](../docs/mkdocs/docs/integration/cmake.md) starting with `JSON_Test` (and `JSON_FastTests`, +`JSON_Valgrind`) control the test build. The most relevant ones: + +- `JSON_TestStandards`: build every test file for the given standards, e.g., `-DJSON_TestStandards=17`. By default, + a file is built for C++11 and for each standard whose `JSON_HAS_CPP_` macro it mentions (see below). +- `JSON_TestUnityBuild`: compile several test files together (see below). Set it to `OFF` to get one executable per + test file, e.g., to debug a single test in a debugger. +- `JSON_TestShard`: build only a part of the test files, e.g., `-DJSON_TestShard=0/2`. + +To run the tests of one file only, run its CTest test, e.g., `ctest --test-dir build -R test-foo_cpp11`. + +## How test files are built + +Two mechanisms keep the compile time down. Both are automatic, but they set a few rules for test files. + +### C++ standards + +A test file is built for a C++ standard beyond C++11 if its text contains `JSON_HAS_CPP_`, for instance in +`#ifdef JSON_HAS_CPP_17`, or in a comment `// JSON_HAS_CPP_17` next to code that needs a macro only defined for that +standard, such as `JSON_HAS_FILESYSTEM`. Then the *whole file* is compiled again for that standard. + +So that this does not happen for the large files, tests that need C++14, C++17, or C++20 go into a separate file +`src/unit--cpp.cpp` (e.g., [`src/unit-regression3-cpp17.cpp`](src/unit-regression3-cpp17.cpp)), whose body +is wrapped in `#ifdef JSON_HAS_CPP_`. The main file `src/unit-.cpp` must not mention `JSON_HAS_CPP_` at +all, not even in a comment, so it is built for C++11 only. The CI jobs `ci_test_*_cxx` still build every test file +for every standard. + +### Unity build + +With `JSON_TestUnityBuild` (`ON` by default, `OFF` with MinGW), CMake compiles several test files as one translation +unit, so they share the template instantiations of the library. CMake generates `build/tests/unity/test-unity-*.cpp`, +which `#include` the test files of a batch. Every test file still gets its own CTest test, which runs the batch +executable with `--source-file=*unit-foo.cpp`, so only the test cases of that file run. + +CMake decides from the macros a file defines *before* including ``: + +| Macros defined before the include | Built as | +|-------------------------------------------------------------------------------------------|-----------------------------------| +| none (or only `SKIP_TESTS_FOR_*` and similar macros derived from global definitions) | batch with the other plain files | +| only `JSON_TESTS_PRIVATE` | batch with the other such files | +| any library configuration macro (`JSON_DIAGNOSTICS`, `JSON_NO_IO`, `JSON_ASSERT`, ...) | its own executable | + +Files with test-specific build options (`json_test_set_test_options(test-foo ...)` in +[`CMakeLists.txt`](CMakeLists.txt)) also get their own executable. The batchable files are split alphabetically into +batches of `JSON_TestUnityBatchSize` files, except for explicit groups of related files: `json_test_unity_group_binary` +compiles all binary-format tests together, because they share the binary readers and writers. Only add a group if it +measurably pays off; files that share little just make one slow translation unit. + +## Rules for test files + +Because the files of a batch share one translation unit, a test file must not affect the files that follow it: + +1. **Give file-scope helpers file-specific names.** An anonymous namespace does not help, as the batch is one + translation unit. Use a name such as `my_allocator_2982`, or wrap the helpers in a namespace named after the file + (e.g., `namespace unit_comparison_detail`). Clashes show up as compile errors. +2. **Do not `#undef` library macros**, such as `JSON_HAS_CPP_17`, at the end of a file: this silently disables the tests + that depend on them in the later files of the batch. `#undef` macros you define yourself after including the library. +3. **Put version-dependent tests into `unit--cpp.cpp`** (see [C++ standards](#c-standards)). +4. **Define test cases in the `.cpp` file itself.** The batch executable selects the tests of a file by its name, so a + `TEST_CASE` in a shared header is not run. The only exception is the test case in + [`src/make_test_data_available.hpp`](src/make_test_data_available.hpp), which CMake handles explicitly. + +To check a new file for clashes with all others at once, build with `-DJSON_TestUnityBatchSize=1000`, which puts all +batchable files of a kind into one translation unit. + +## Where to add tests + +Tests are structured along the features of the library. Usually, an existing file is the right place: + +- For a bug fix, add a section referencing the issue to [`src/unit-regression3.cpp`](src/unit-regression3.cpp), or to + [`src/unit-regression3-cpp17.cpp`](src/unit-regression3-cpp17.cpp) / + [`src/unit-regression3-cpp20.cpp`](src/unit-regression3-cpp20.cpp) if the test needs C++17 / C++20. + [`src/unit-regression2.cpp`](src/unit-regression2.cpp) holds older tests and should not grow further. +- When testing exceptions, use `CHECK_THROWS_WITH_AS`, which also checks the `what()` message. + +A new file `src/unit-.cpp` is picked up automatically when CMake runs again; no change to `CMakeLists.txt` is +needed. + +See also [`fuzzing.md`](fuzzing.md) for fuzz testing. diff --git a/tests/src/unit-allocator-cpp17.cpp b/tests/src/unit-allocator-cpp17.cpp new file mode 100644 index 000000000..f188b9408 --- /dev/null +++ b/tests/src/unit-allocator-cpp17.cpp @@ -0,0 +1,98 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-allocator.cpp (std::string_view key +// lookup with a failing allocator). It is kept in a separate translation unit so the (much +// larger) unit-allocator.cpp is built for C++11 only and not rebuilt for every C++ +// standard. + +#include "doctest_compatibility.h" + +#include + +#ifdef JSON_HAS_CPP_17 +#include +#include +#include +#include + +namespace +{ +bool next_construct_fails_cpp17 = false; +bool next_destroy_fails_cpp17 = false; +bool next_deallocate_fails_cpp17 = false; + +template +struct my_allocator_cpp17 : std::allocator +{ + using std::allocator::allocator; + + template + void construct(T* p, Args&& ... args) + { + if (next_construct_fails_cpp17) + { + next_construct_fails_cpp17 = false; + throw std::bad_alloc(); + } + + ::new (reinterpret_cast(p)) T(std::forward(args)...); + } + + void deallocate(T* p, std::size_t n) + { + if (next_deallocate_fails_cpp17) + { + next_deallocate_fails_cpp17 = false; + throw std::bad_alloc(); + } + + std::allocator::deallocate(p, n); + } + + void destroy(T* p) + { + if (next_destroy_fails_cpp17) + { + next_destroy_fails_cpp17 = false; + throw std::bad_alloc(); + } + + static_cast(p); // fix MSVC's C4100 warning + p->~T(); + } + + template + struct rebind + { + using other = my_allocator_cpp17; + }; +}; +} // namespace + +// the no-exceptions CI job skips every CHECK_THROWS_AS, which would leave +// next_construct_fails_cpp17 set for the next allocation outside a check +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("a failed allocation leaves the value unchanged (C++17)") +{ + // create JSON type using the throwing allocator + using my_json = nlohmann::json::with_allocator_t; + + SECTION("turning a null value into an array or object") + { + my_json j; + + next_construct_fails_cpp17 = true; + CHECK_THROWS_AS(j[std::string_view("key")], std::bad_alloc&); + CHECK(j.is_null()); + + next_construct_fails_cpp17 = false; + } +} +#endif +#endif diff --git a/tests/src/unit-allocator-cpp20.cpp b/tests/src/unit-allocator-cpp20.cpp new file mode 100644 index 000000000..06fd84afd --- /dev/null +++ b/tests/src/unit-allocator-cpp20.cpp @@ -0,0 +1,115 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-allocator.cpp (creating a value from a +// std::ranges view with a failing allocator). It is kept in a separate translation unit so +// the (much larger) unit-allocator.cpp is built for C++11 only and not rebuilt for every +// C++ standard. + +#include "doctest_compatibility.h" + +#include + +#ifdef JSON_HAS_CPP_20 +#if JSON_HAS_RANGES + #include +#endif + +#include +#include +#include +#include + +namespace +{ +bool next_construct_fails_cpp20 = false; +bool next_destroy_fails_cpp20 = false; +bool next_deallocate_fails_cpp20 = false; + +template +struct my_allocator_cpp20 : std::allocator +{ + using std::allocator::allocator; + + template + void construct(T* p, Args&& ... args) + { + if (next_construct_fails_cpp20) + { + next_construct_fails_cpp20 = false; + throw std::bad_alloc(); + } + + ::new (reinterpret_cast(p)) T(std::forward(args)...); + } + + void deallocate(T* p, std::size_t n) + { + if (next_deallocate_fails_cpp20) + { + next_deallocate_fails_cpp20 = false; + throw std::bad_alloc(); + } + + std::allocator::deallocate(p, n); + } + + void destroy(T* p) + { + if (next_destroy_fails_cpp20) + { + next_destroy_fails_cpp20 = false; + throw std::bad_alloc(); + } + + static_cast(p); // fix MSVC's C4100 warning + p->~T(); + } + + template + struct rebind + { + using other = my_allocator_cpp20; + }; +}; +} // namespace + +// the no-exceptions CI job skips every CHECK_THROWS_AS, which would leave +// next_construct_fails_cpp20 set for the next allocation outside a check +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("a failed allocation leaves the value unchanged (C++20)") +{ + // create JSON type using the throwing allocator + using my_json = nlohmann::json::with_allocator_t; + + // With iterator debugging, VS 2015's containers construct a proxy with the + // allocator in constructors that cannot report its failure, so a failing + // allocator crashes this section there (SIGSEGV with VS 2015 Debug x86). +#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0) + SECTION("converting into an existing value") + { + // to_json replaces the value it is given; the old one must survive a + // failed creation of the new one + my_json j = "old"; + +#if JSON_HAS_RANGES && !defined(__MINGW32__) + const std::vector numbers = {1, 2}; + next_construct_fails_cpp20 = true; + CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/) + { + return true; + })), std::bad_alloc&); + CHECK(j == "old"); +#endif + + next_construct_fails_cpp20 = false; + } +#endif +} +#endif +#endif diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index 4062870d5..4b6eee7b4 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -13,9 +13,6 @@ using nlohmann::json; #include -#if JSON_HAS_RANGES - #include -#endif namespace { @@ -684,12 +681,6 @@ TEST_CASE("a failed allocation leaves the value unchanged") CHECK_THROWS_AS(j["key"], std::bad_alloc&); CHECK(j.is_null()); -#ifdef JSON_HAS_CPP_17 - next_construct_fails = true; - CHECK_THROWS_AS(j[std::string_view("key")], std::bad_alloc&); - CHECK(j.is_null()); -#endif - next_construct_fails = true; CHECK_THROWS_AS(j.push_back(my_json(1)), std::bad_alloc&); CHECK(j.is_null()); @@ -787,16 +778,6 @@ TEST_CASE("a failed allocation leaves the value unchanged") CHECK_THROWS_AS(nlohmann::to_json(j, std::valarray {1, 2}), std::bad_alloc&); CHECK(j == "old"); -#if JSON_HAS_RANGES && !defined(__MINGW32__) - const std::vector numbers = {1, 2}; - next_construct_fails = true; - CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/) - { - return true; - })), std::bad_alloc&); - CHECK(j == "old"); -#endif - next_construct_fails = false; nlohmann::to_json(j, std::vector {1, 2}); CHECK(j == my_json({1, 2})); diff --git a/tests/src/unit-bon8-cpp17.cpp b/tests/src/unit-bon8-cpp17.cpp new file mode 100644 index 000000000..8350ca994 --- /dev/null +++ b/tests/src/unit-bon8-cpp17.cpp @@ -0,0 +1,60 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-bon8.cpp (BON8 with std::byte +// containers). It is kept in a separate translation unit so the (much larger) unit- +// bon8.cpp is built for C++11 only and not rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#ifdef JSON_HAS_CPP_17 +#include +#include +#include + +TEST_CASE("BON8 with std::byte") +{ + SECTION("vector roundtrip") + { + const json original = + { + {"name", "test"}, + {"value", 42}, + {"array", {1, 2, 3}} + }; + + const std::vector temp = json::to_bon8(original); + std::vector bon8_data(temp.size()); + for (size_t i = 0; i < temp.size(); ++i) + { + bon8_data[i] = std::byte(temp[i]); + } + + json from_bytes; + CHECK_NOTHROW(from_bytes = json::from_bon8(bon8_data)); + CHECK(from_bytes == original); + } + + SECTION("empty vector") + { + const std::vector empty_data; + CHECK_THROWS_WITH_AS([&]() + { + [[maybe_unused]] auto result = json::from_bon8(empty_data); + return true; + } + (), + "[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input", + json::parse_error&); + } +} + +#endif diff --git a/tests/src/unit-bon8.cpp b/tests/src/unit-bon8.cpp index b1d82ca29..7d93947f5 100644 --- a/tests/src/unit-bon8.cpp +++ b/tests/src/unit-bon8.cpp @@ -1052,41 +1052,3 @@ TEST_CASE("issue #5648 - from_bon8(ptr, len) must read len bytes, not treat ptr #endif } -#ifdef JSON_HAS_CPP_17 -TEST_CASE("BON8 with std::byte") -{ - SECTION("vector roundtrip") - { - const json original = - { - {"name", "test"}, - {"value", 42}, - {"array", {1, 2, 3}} - }; - - const std::vector temp = json::to_bon8(original); - std::vector bon8_data(temp.size()); - for (size_t i = 0; i < temp.size(); ++i) - { - bon8_data[i] = std::byte(temp[i]); - } - - json from_bytes; - CHECK_NOTHROW(from_bytes = json::from_bon8(bon8_data)); - CHECK(from_bytes == original); - } - - SECTION("empty vector") - { - const std::vector empty_data; - CHECK_THROWS_WITH_AS([&]() - { - [[maybe_unused]] auto result = json::from_bon8(empty_data); - return true; - } - (), - "[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input", - json::parse_error&); - } -} -#endif diff --git a/tests/src/unit-comparison-cpp20.cpp b/tests/src/unit-comparison-cpp20.cpp new file mode 100644 index 000000000..0bcf6253d --- /dev/null +++ b/tests/src/unit-comparison-cpp20.cpp @@ -0,0 +1,496 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-comparison.cpp (tests of operator<=> and +// other three-way comparison specific behavior). It is kept in a separate translation unit +// so the (much larger) unit-comparison.cpp is built for C++11 only and not rebuilt for +// every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#ifdef JSON_HAS_CPP_20 +#if JSON_HAS_THREE_WAY_COMPARISON +#include +#include +#include +#include +#include +#include +#include + +// this can be replaced with the doctest stl extension header in version 2.5 +namespace doctest +{ +template<> struct StringMaker +{ + static String convert(const std::partial_ordering& order) + { + if (order == std::partial_ordering::less) + { + return "std::partial_ordering::less"; + } + if (order == std::partial_ordering::equivalent) + { + return "std::partial_ordering::equivalent"; + } + if (order == std::partial_ordering::greater) + { + return "std::partial_ordering::greater"; + } + if (order == std::partial_ordering::unordered) + { + return "std::partial_ordering::unordered"; + } + return "{?}"; + } +}; +} // namespace doctest + +TEST_CASE("lexicographical comparison operators (C++20)") +{ + constexpr auto f_ = false; + constexpr auto _t = true; + constexpr auto nan = std::numeric_limits::quiet_NaN(); + constexpr auto lt = std::partial_ordering::less; + constexpr auto gt = std::partial_ordering::greater; + constexpr auto eq = std::partial_ordering::equivalent; + constexpr auto un = std::partial_ordering::unordered; + + INFO("using 3-way comparison"); + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + INFO("using legacy comparison"); +#endif + + SECTION("types") + { + std::vector j_types = + { + json::value_t::null, + json::value_t::boolean, + json::value_t::number_integer, + json::value_t::number_unsigned, + json::value_t::number_float, + json::value_t::object, + json::value_t::array, + json::value_t::string, + json::value_t::binary, + json::value_t::discarded + }; + + std::vector> expected_lt = + { + //0 1 2 3 4 5 6 7 8 9 + {f_, _t, _t, _t, _t, _t, _t, _t, _t, f_}, // 0 + {f_, f_, _t, _t, _t, _t, _t, _t, _t, f_}, // 1 + {f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 2 + {f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 3 + {f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 4 + {f_, f_, f_, f_, f_, f_, _t, _t, _t, f_}, // 5 + {f_, f_, f_, f_, f_, f_, f_, _t, _t, f_}, // 6 + {f_, f_, f_, f_, f_, f_, f_, f_, _t, f_}, // 7 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 8 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 9 + }; + + SECTION("comparison: less") + { + for (size_t i = 0; i < j_types.size(); ++i) + { + for (size_t j = 0; j < j_types.size(); ++j) + { + CAPTURE(i) + CAPTURE(j) + // check precomputed values + CHECK((j_types[i] < j_types[j]) == expected_lt[i][j]); + } + } + } + + SECTION("comparison: 3-way") + { + // doctest runs the REQUIRE in the test case body once per leaf section; keep it + // here to run it as often as before the 3-way sections moved from unit-comparison.cpp + REQUIRE(std::isnan(nan)); + + std::vector> expected = + { + //0 1 2 3 4 5 6 7 8 9 + {eq, lt, lt, lt, lt, lt, lt, lt, lt, un}, // 0 + {gt, eq, lt, lt, lt, lt, lt, lt, lt, un}, // 1 + {gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 2 + {gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 3 + {gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 4 + {gt, gt, gt, gt, gt, eq, lt, lt, lt, un}, // 5 + {gt, gt, gt, gt, gt, gt, eq, lt, lt, un}, // 6 + {gt, gt, gt, gt, gt, gt, gt, eq, lt, un}, // 7 + {gt, gt, gt, gt, gt, gt, gt, gt, eq, un}, // 8 + {un, un, un, un, un, un, un, un, un, un}, // 9 + }; + + // check expected partial_ordering against expected boolean + REQUIRE(expected.size() == expected_lt.size()); + for (size_t i = 0; i < expected.size(); ++i) + { + REQUIRE(expected[i].size() == expected_lt[i].size()); + for (size_t j = 0; j < expected[i].size(); ++j) + { + CAPTURE(i) + CAPTURE(j) + CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]); + } + } + + // check 3-way comparison against expected partial_ordering + REQUIRE(expected.size() == j_types.size()); + for (size_t i = 0; i < j_types.size(); ++i) + { + REQUIRE(expected[i].size() == j_types.size()); + for (size_t j = 0; j < j_types.size(); ++j) + { + CAPTURE(i) + CAPTURE(j) + CHECK((j_types[i] <=> j_types[j]) == expected[i][j]); // *NOPAD* + } + } + } + } + + SECTION("values") + { + json j_values = + { + nullptr, nullptr, // 0 1 + -17, 42, // 2 3 + 8u, 13u, // 4 5 + 3.14159, 23.42, // 6 7 + nan, nan, // 8 9 + "foo", "bar", // 10 11 + true, false, // 12 13 + {1, 2, 3}, {"one", "two", "three"}, // 14 15 + {{"first", 1}, {"second", 2}}, {{"a", "A"}, {"b", {"B"}}}, // 16 17 + json::binary({1, 2, 3}), json::binary({1, 2, 4}), // 18 19 + json(json::value_t::discarded), json(json::value_t::discarded) // 20 21 + }; + + std::vector> expected_eq = + { + //0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + {_t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 0 + {_t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 1 + {f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 2 + {f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 3 + {f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 4 + {f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 5 + {f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 6 + {f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 7 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 8 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 9 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 10 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 11 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 12 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_}, // 13 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_}, // 14 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_}, // 15 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_}, // 16 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_}, // 17 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_}, // 18 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_}, // 19 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 20 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21 + }; + + std::vector> expected_lt = + { + //0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + {f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_}, // 0 + {f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_}, // 1 + {f_, f_, f_, _t, _t, _t, _t, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 2 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 3 + {f_, f_, f_, _t, f_, _t, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 4 + {f_, f_, f_, _t, f_, f_, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 5 + {f_, f_, f_, _t, _t, _t, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 6 + {f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 7 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 8 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 9 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 10 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 11 + {f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 12 + {f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 13 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, _t, f_, f_, _t, _t, f_, f_}, // 14 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 15 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, f_, f_, _t, _t, f_, f_}, // 16 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, f_, _t, _t, f_, f_}, // 17 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_}, // 18 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 19 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 20 + {f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21 + }; + + SECTION("signed/unsigned mixed comparison above INT64_MAX") + { + const json above_int64_max = static_cast((std::numeric_limits::max)()) + 1ULL; + const json max_uint64 = (std::numeric_limits::max)(); + const json negative_one = -1; + const json one = 1; + const json max_int64 = (std::numeric_limits::max)(); + + CHECK((negative_one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* + CHECK((above_int64_max <=> negative_one) == std::partial_ordering::greater); // *NOPAD* + CHECK((negative_one <=> max_uint64) == std::partial_ordering::less); // *NOPAD* + CHECK((max_uint64 <=> negative_one) == std::partial_ordering::greater); // *NOPAD* + CHECK((one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* + CHECK((above_int64_max <=> one) == std::partial_ordering::greater); // *NOPAD* + CHECK((max_int64 <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* + CHECK((above_int64_max <=> max_int64) == std::partial_ordering::greater); // *NOPAD* + } + + SECTION("integer/float mixed comparison is exact") + { + // Widening the integer to a double loses precision past the + // mantissa, so 2^63-2 and 2^63-1 both used to compare equal to the + // double 2^63 while differing from each other. That makes equality + // intransitive and the ordering not a strict weak ordering. + const json below_two_63 = static_cast(9223372036854775806LL); + const json max_int64 = (std::numeric_limits::max)(); + const json two_63 = 9223372036854775808.0; + + // the same past the unsigned range + const json max_uint64 = (std::numeric_limits::max)(); + const json two_64 = 18446744073709551616.0; + + CHECK((max_int64 <=> two_63) == std::partial_ordering::less); // *NOPAD* + CHECK((two_63 <=> max_int64) == std::partial_ordering::greater); // *NOPAD* + CHECK((below_two_63 <=> max_int64) == std::partial_ordering::less); // *NOPAD* + CHECK((max_uint64 <=> two_64) == std::partial_ordering::less); // *NOPAD* + CHECK((json(1) <=> json(1.0)) == std::partial_ordering::equivalent); // *NOPAD* + CHECK((json(1) <=> json(nan)) == std::partial_ordering::unordered); // *NOPAD* + } + + SECTION("comparison: 3-way") + { + // doctest runs the REQUIRE in the test case body once per leaf section; keep it + // here to run it as often as before the 3-way sections moved from unit-comparison.cpp + REQUIRE(std::isnan(nan)); + + std::vector> expected = + { + //0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + {eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 0 + {eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 1 + {gt, gt, eq, lt, lt, lt, lt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 2 + {gt, gt, gt, eq, gt, gt, gt, gt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 3 + {gt, gt, gt, lt, eq, lt, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 4 + {gt, gt, gt, lt, gt, eq, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 5 + {gt, gt, gt, lt, lt, lt, eq, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 6 + {gt, gt, gt, lt, gt, gt, gt, eq, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 7 + {gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 8 + {gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 9 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, gt, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 10 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, eq, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 11 + {gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, gt, lt, lt, lt, lt, lt, lt, un, un}, // 12 + {gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, lt, lt, lt, lt, lt, lt, un, un}, // 13 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, eq, lt, gt, gt, lt, lt, un, un}, // 14 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, gt, eq, gt, gt, lt, lt, un, un}, // 15 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, eq, gt, lt, lt, un, un}, // 16 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, lt, eq, lt, lt, un, un}, // 17 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, lt, un, un}, // 18 + {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, un, un}, // 19 + {un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 20 + {un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 21 + }; + + // check expected partial_ordering against expected booleans + REQUIRE(expected.size() == expected_eq.size()); + REQUIRE(expected.size() == expected_lt.size()); + for (size_t i = 0; i < expected.size(); ++i) + { + REQUIRE(expected[i].size() == expected_eq[i].size()); + REQUIRE(expected[i].size() == expected_lt[i].size()); + for (size_t j = 0; j < expected[i].size(); ++j) + { + CAPTURE(i) + CAPTURE(j) + CHECK(std::is_eq(expected[i][j]) == expected_eq[i][j]); + CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]); + if (std::is_gt(expected[i][j])) + { + CHECK((!expected_eq[i][j] && !expected_lt[i][j])); + } + } + } + + // check that two values compare according to their expected ordering + REQUIRE(expected.size() == j_values.size()); + for (size_t i = 0; i < j_values.size(); ++i) + { + REQUIRE(expected[i].size() == j_values.size()); + for (size_t j = 0; j < j_values.size(); ++j) + { + CAPTURE(i) + CAPTURE(j) + CHECK((j_values[i] <=> j_values[j]) == expected[i][j]); // *NOPAD* + } + } + } + } + +} + +TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P2468R2)") +{ + // Issue #3868: operator!= was preventing compiler from synthesizing reversed + // operator== candidates under C++20's P2468R2 rewritten candidate rules. + // Verify that heterogeneous comparisons now work. + + SECTION("string vs json") + { + std::string s = "string"; + json j = "string"; + CHECK(s == j); + CHECK(j == s); + CHECK_FALSE(s != j); + CHECK_FALSE(j != s); + } + + SECTION("other heterogeneous types") + { + int i = 42; + json j = 42; + CHECK(i == j); + CHECK(j == i); + CHECK_FALSE(i != j); + CHECK_FALSE(j != i); + } +} + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON +TEST_CASE("regression #5665 - scalar <= discarded and scalar >= discarded in C++20 legacy mode") +{ + // Issue #5665: with a scalar on the left-hand side, <= and >= only had the + // candidate rewritten from operator<=>, which does not emulate the legacy + // discarded-value behavior. Check that scalar-on-the-left now matches the + // other three operand orders. + const json discarded(json::value_t::discarded); + const json one = 1; + + CHECK(discarded <= 1); + CHECK(discarded >= 1); + CHECK(one <= discarded); + CHECK(one >= discarded); + CHECK(1 <= discarded); + CHECK(1 >= discarded); + CHECK(1.5 <= discarded); + CHECK(1.5 >= discarded); +} + +#endif + +TEST_CASE("containers are compared element by element (C++20)") +{ + // Containers nested deeper than a bound are compared without the call + // stack, by code of their own; every relation is checked both at the top + // level and below that bound. + const auto deep = [](const json & j, const std::size_t depth) + { + json result = j; + for (std::size_t i = 0; i < depth; ++i) + { + result = json::array({std::move(result)}); + } + return result; + }; + + for (const std::size_t depth : std::vector {0, 200}) + { + CAPTURE(depth) + + // objects with different keys + { + const json a = deep({{"a", 1}}, depth); + const json b = deep({{"b", 1}}, depth); + CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD* + CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD* + CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD* + } + + // a container that is a prefix of the other one + { + // the one that runs out of elements first is the smaller one + const json shorter = deep({1}, depth); + const json longer = deep({1, 2}, depth); + + const json smaller_object = deep({{"a", 1}}, depth); + const json larger_object = deep({{"a", 1}, {"b", 2}}, depth); + CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD* + CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD* + } + + // elements that cannot be ordered + { + const double nan = std::numeric_limits::quiet_NaN(); + const json lhs = deep({nan, 1}, depth); + const json rhs = deep({nan, 2}, depth); + // operator<=> stops there, as std::lexicographical_compare_three_way + // does, and operator< is derived from it + CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD* + CHECK_FALSE(lhs < rhs); + } + } +} + +TEST_CASE("operator<=> of binary values with a different subtype does not depend on nesting depth") +{ + // #5654: std::vector::operator<=>, which the binary type's + // own operator<=> uses, ignores the subtype that operator== checks. So a + // pair of binary values with the same bytes but a different subtype is + // unequal, yet <=>-equivalent - the same inconsistency between == and <=> + // that a NaN has. Within the nesting bound, an array compares itself + // with std::vector's own operator<=>, which treats an equivalent pair as + // undecided and lets the next element decide, same as + // std::lexicographical_compare_three_way does. Past the bound, + // compare_iteratively() takes over and must classify the pair the + // same way, or the result of operator<=> - and of <, which C++20 derives + // from it - depends on how deeply the values are nested. + const json a = json::array({json::binary({1}, 1), 1}); + const json b = json::array({json::binary({1}, 2), 2}); + + // the root inconsistency: unequal, yet <=>-equivalent + CHECK_FALSE(a[0] == b[0]); + CHECK((a[0] <=> b[0]) == std::partial_ordering::equivalent); // *NOPAD* + + const auto deep = [](const json & j, const std::size_t depth) + { + json result = j; + for (std::size_t i = 0; i < depth; ++i) + { + result = json::array({std::move(result)}); + } + return result; + }; + + // 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not, + // and must still agree with the levels that do + for (const std::size_t depth : std::vector {0, 127, 128, 200}) + { + CAPTURE(depth) + const json x = deep(a, depth); + const json y = deep(b, depth); + CHECK((x <=> y) == std::partial_ordering::less); // *NOPAD* + CHECK((y <=> x) == std::partial_ordering::greater); // *NOPAD* + CHECK(x < y); + CHECK(y > x); + CHECK_FALSE(y < x); + } +} + +#endif // JSON_HAS_THREE_WAY_COMPARISON +#endif diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 1e409bfb0..c9e600c32 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -6,12 +6,11 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. To avoid rebuilding this +// large file for every standard, tests that depend on the standard version (e.g., +// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +// go into a separate file unit-comparison-cpp.cpp. This file stays C++11-only. #include "doctest_compatibility.h" @@ -28,37 +27,6 @@ #include using nlohmann::json; -#if JSON_HAS_THREE_WAY_COMPARISON -// this can be replaced with the doctest stl extension header in version 2.5 -namespace doctest -{ -template<> struct StringMaker -{ - static String convert(const std::partial_ordering& order) - { - if (order == std::partial_ordering::less) - { - return "std::partial_ordering::less"; - } - if (order == std::partial_ordering::equivalent) - { - return "std::partial_ordering::equivalent"; - } - if (order == std::partial_ordering::greater) - { - return "std::partial_ordering::greater"; - } - if (order == std::partial_ordering::unordered) - { - return "std::partial_ordering::unordered"; - } - return "{?}"; - } -}; -} // namespace doctest - -#endif - namespace { // helper function to check std::less @@ -75,16 +43,6 @@ TEST_CASE("lexicographical comparison operators") constexpr auto f_ = false; constexpr auto _t = true; constexpr auto nan = std::numeric_limits::quiet_NaN(); -#if JSON_HAS_THREE_WAY_COMPARISON - constexpr auto lt = std::partial_ordering::less; - constexpr auto gt = std::partial_ordering::greater; - constexpr auto eq = std::partial_ordering::equivalent; - constexpr auto un = std::partial_ordering::unordered; -#endif - -#if JSON_HAS_THREE_WAY_COMPARISON - INFO("using 3-way comparison"); -#endif #if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON INFO("using legacy comparison"); @@ -135,62 +93,13 @@ TEST_CASE("lexicographical comparison operators") CAPTURE(i) CAPTURE(j) // check precomputed values -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - CHECK((j_types[i] < j_types[j]) == expected_lt[i][j]); -#else +#if !JSON_HAS_THREE_WAY_COMPARISON CHECK(operator<(j_types[i], j_types[j]) == expected_lt[i][j]); #endif CHECK(f(j_types[i], j_types[j]) == expected_lt[i][j]); } } } -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - SECTION("comparison: 3-way") - { - std::vector> expected = - { - //0 1 2 3 4 5 6 7 8 9 - {eq, lt, lt, lt, lt, lt, lt, lt, lt, un}, // 0 - {gt, eq, lt, lt, lt, lt, lt, lt, lt, un}, // 1 - {gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 2 - {gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 3 - {gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 4 - {gt, gt, gt, gt, gt, eq, lt, lt, lt, un}, // 5 - {gt, gt, gt, gt, gt, gt, eq, lt, lt, un}, // 6 - {gt, gt, gt, gt, gt, gt, gt, eq, lt, un}, // 7 - {gt, gt, gt, gt, gt, gt, gt, gt, eq, un}, // 8 - {un, un, un, un, un, un, un, un, un, un}, // 9 - }; - - // check expected partial_ordering against expected boolean - REQUIRE(expected.size() == expected_lt.size()); - for (size_t i = 0; i < expected.size(); ++i) - { - REQUIRE(expected[i].size() == expected_lt[i].size()); - for (size_t j = 0; j < expected[i].size(); ++j) - { - CAPTURE(i) - CAPTURE(j) - CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]); - } - } - - // check 3-way comparison against expected partial_ordering - REQUIRE(expected.size() == j_types.size()); - for (size_t i = 0; i < j_types.size(); ++i) - { - REQUIRE(expected[i].size() == j_types.size()); - for (size_t j = 0; j < j_types.size(); ++j) - { - CAPTURE(i) - CAPTURE(j) - CHECK((j_types[i] <=> j_types[j]) == expected[i][j]); // *NOPAD* - } - } - } -#endif } SECTION("values") @@ -319,18 +228,6 @@ TEST_CASE("lexicographical comparison operators") CHECK_FALSE(above_int64_max <= max_int64); CHECK(above_int64_max > max_int64); CHECK(above_int64_max >= max_int64); - -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - CHECK((negative_one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* - CHECK((above_int64_max <=> negative_one) == std::partial_ordering::greater); // *NOPAD* - CHECK((negative_one <=> max_uint64) == std::partial_ordering::less); // *NOPAD* - CHECK((max_uint64 <=> negative_one) == std::partial_ordering::greater); // *NOPAD* - CHECK((one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* - CHECK((above_int64_max <=> one) == std::partial_ordering::greater); // *NOPAD* - CHECK((max_int64 <=> above_int64_max) == std::partial_ordering::less); // *NOPAD* - CHECK((above_int64_max <=> max_int64) == std::partial_ordering::greater); // *NOPAD* -#endif } SECTION("integer/float mixed comparison is exact") @@ -381,16 +278,6 @@ TEST_CASE("lexicographical comparison operators") CHECK_FALSE(json(1) < json(nan)); CHECK_FALSE(json(nan) < json(1)); CHECK_FALSE(json(1u) == json(nan)); - -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - CHECK((max_int64 <=> two_63) == std::partial_ordering::less); // *NOPAD* - CHECK((two_63 <=> max_int64) == std::partial_ordering::greater); // *NOPAD* - CHECK((below_two_63 <=> max_int64) == std::partial_ordering::less); // *NOPAD* - CHECK((max_uint64 <=> two_64) == std::partial_ordering::less); // *NOPAD* - CHECK((json(1) <=> json(1.0)) == std::partial_ordering::equivalent); // *NOPAD* - CHECK((json(1) <=> json(nan)) == std::partial_ordering::unordered); // *NOPAD* -#endif } SECTION("compares unordered") @@ -617,72 +504,6 @@ TEST_CASE("lexicographical comparison operators") } } } - -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - SECTION("comparison: 3-way") - { - std::vector> expected = - { - //0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 - {eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 0 - {eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 1 - {gt, gt, eq, lt, lt, lt, lt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 2 - {gt, gt, gt, eq, gt, gt, gt, gt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 3 - {gt, gt, gt, lt, eq, lt, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 4 - {gt, gt, gt, lt, gt, eq, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 5 - {gt, gt, gt, lt, lt, lt, eq, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 6 - {gt, gt, gt, lt, gt, gt, gt, eq, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 7 - {gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 8 - {gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 9 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, gt, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 10 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, eq, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 11 - {gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, gt, lt, lt, lt, lt, lt, lt, un, un}, // 12 - {gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, lt, lt, lt, lt, lt, lt, un, un}, // 13 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, eq, lt, gt, gt, lt, lt, un, un}, // 14 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, gt, eq, gt, gt, lt, lt, un, un}, // 15 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, eq, gt, lt, lt, un, un}, // 16 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, lt, eq, lt, lt, un, un}, // 17 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, lt, un, un}, // 18 - {gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, un, un}, // 19 - {un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 20 - {un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 21 - }; - - // check expected partial_ordering against expected booleans - REQUIRE(expected.size() == expected_eq.size()); - REQUIRE(expected.size() == expected_lt.size()); - for (size_t i = 0; i < expected.size(); ++i) - { - REQUIRE(expected[i].size() == expected_eq[i].size()); - REQUIRE(expected[i].size() == expected_lt[i].size()); - for (size_t j = 0; j < expected[i].size(); ++j) - { - CAPTURE(i) - CAPTURE(j) - CHECK(std::is_eq(expected[i][j]) == expected_eq[i][j]); - CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]); - if (std::is_gt(expected[i][j])) - { - CHECK((!expected_eq[i][j] && !expected_lt[i][j])); - } - } - } - - // check that two values compare according to their expected ordering - REQUIRE(expected.size() == j_values.size()); - for (size_t i = 0; i < j_values.size(); ++i) - { - REQUIRE(expected[i].size() == j_values.size()); - for (size_t j = 0; j < j_values.size(); ++j) - { - CAPTURE(i) - CAPTURE(j) - CHECK((j_values[i] <=> j_values[j]) == expected[i][j]); // *NOPAD* - } - } - } -#endif } #if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON @@ -721,60 +542,7 @@ TEST_CASE("lexicographical comparison operators") #endif } -#if JSON_HAS_THREE_WAY_COMPARISON -// JSON_HAS_CPP_20 (do not remove; see note at top of file) - -TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P2468R2)") -{ - // Issue #3868: operator!= was preventing compiler from synthesizing reversed - // operator== candidates under C++20's P2468R2 rewritten candidate rules. - // Verify that heterogeneous comparisons now work. - - SECTION("string vs json") - { - std::string s = "string"; - json j = "string"; - CHECK(s == j); - CHECK(j == s); - CHECK_FALSE(s != j); - CHECK_FALSE(j != s); - } - - SECTION("other heterogeneous types") - { - int i = 42; - json j = 42; - CHECK(i == j); - CHECK(j == i); - CHECK_FALSE(i != j); - CHECK_FALSE(j != i); - } -} - -#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON -TEST_CASE("regression #5665 - scalar <= discarded and scalar >= discarded in C++20 legacy mode") -{ - // Issue #5665: with a scalar on the left-hand side, <= and >= only had the - // candidate rewritten from operator<=>, which does not emulate the legacy - // discarded-value behavior. Check that scalar-on-the-left now matches the - // other three operand orders. - const json discarded(json::value_t::discarded); - const json one = 1; - - CHECK(discarded <= 1); - CHECK(discarded >= 1); - CHECK(one <= discarded); - CHECK(one >= discarded); - CHECK(1 <= discarded); - CHECK(1 >= discarded); - CHECK(1.5 <= discarded); - CHECK(1.5 >= discarded); -} -#endif - -#endif - -namespace +namespace unit_comparison_detail { // orders keys ascending or descending, as chosen when a map is created template @@ -906,7 +674,9 @@ struct case_insensitive_less template using case_insensitive_map = std::map; using ci_json = nlohmann::json::with_object_t; -} // namespace +} // namespace unit_comparison_detail + +using namespace unit_comparison_detail; // NOLINT(google-build-using-namespace) TEST_CASE("equality of objects whose entries have no fixed order") { @@ -1049,12 +819,6 @@ TEST_CASE("containers are compared element by element") CHECK(a < b); CHECK(b > a); CHECK_FALSE(b < a); -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD* - CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD* - CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD* -#endif } // a container that is a prefix of the other one @@ -1072,11 +836,6 @@ TEST_CASE("containers are compared element by element") CHECK(smaller_object < larger_object); CHECK(larger_object > smaller_object); CHECK_FALSE(smaller_object == larger_object); -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD* - CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD* -#endif } // elements that cannot be ordered @@ -1087,13 +846,7 @@ TEST_CASE("containers are compared element by element") CHECK_FALSE(lhs == lhs); CHECK_FALSE(rhs < lhs); -#if JSON_HAS_THREE_WAY_COMPARISON - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - // operator<=> stops there, as std::lexicographical_compare_three_way - // does, and operator< is derived from it - CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD* - CHECK_FALSE(lhs < rhs); -#else +#if !JSON_HAS_THREE_WAY_COMPARISON // operator< skips a pair of elements that cannot be ordered, as // std::lexicographical_compare does, and the next pair decides CHECK(lhs < rhs); @@ -1102,50 +855,3 @@ TEST_CASE("containers are compared element by element") } } -#if JSON_HAS_THREE_WAY_COMPARISON -// JSON_HAS_CPP_20 (do not remove; see note at top of file) -TEST_CASE("operator<=> of binary values with a different subtype does not depend on nesting depth") -{ - // #5654: std::vector::operator<=>, which the binary type's - // own operator<=> uses, ignores the subtype that operator== checks. So a - // pair of binary values with the same bytes but a different subtype is - // unequal, yet <=>-equivalent - the same inconsistency between == and <=> - // that a NaN has. Within the nesting bound, an array compares itself - // with std::vector's own operator<=>, which treats an equivalent pair as - // undecided and lets the next element decide, same as - // std::lexicographical_compare_three_way does. Past the bound, - // compare_iteratively() takes over and must classify the pair the - // same way, or the result of operator<=> - and of <, which C++20 derives - // from it - depends on how deeply the values are nested. - const json a = json::array({json::binary({1}, 1), 1}); - const json b = json::array({json::binary({1}, 2), 2}); - - // the root inconsistency: unequal, yet <=>-equivalent - CHECK_FALSE(a[0] == b[0]); - CHECK((a[0] <=> b[0]) == std::partial_ordering::equivalent); // *NOPAD* - - const auto deep = [](const json & j, const std::size_t depth) - { - json result = j; - for (std::size_t i = 0; i < depth; ++i) - { - result = json::array({std::move(result)}); - } - return result; - }; - - // 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not, - // and must still agree with the levels that do - for (const std::size_t depth : std::vector {0, 127, 128, 200}) - { - CAPTURE(depth) - const json x = deep(a, depth); - const json y = deep(b, depth); - CHECK((x <=> y) == std::partial_ordering::less); // *NOPAD* - CHECK((y <=> x) == std::partial_ordering::greater); // *NOPAD* - CHECK(x < y); - CHECK(y > x); - CHECK_FALSE(y < x); - } -} -#endif diff --git a/tests/src/unit-conversions1-cpp17.cpp b/tests/src/unit-conversions1-cpp17.cpp new file mode 100644 index 000000000..9325406b9 --- /dev/null +++ b/tests/src/unit-conversions1-cpp17.cpp @@ -0,0 +1,95 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-conversions1.cpp (conversions to and from +// std::string_view). It is kept in a separate translation unit so the (much larger) unit- +// conversions1.cpp is built for C++11 only and not rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +// workaround for MSVC, which does not set __cplusplus to the language version (#464) +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 +#endif + +#ifdef JSON_HAS_CPP_17 +#include +#include + +#if __has_include() + #include +#elif __has_include() + #include +#endif + +TEST_CASE("value conversion (C++17)") +{ + SECTION("get a string (explicit)") + { + const json::string_t s_reference{"Hello world"}; + json j(s_reference); + + SECTION("std::string_view") + { + std::string_view const s = j.get(); + CHECK(json(s) == j); + } + + SECTION("exception in case of a non-string type using string_view") + { + CHECK_THROWS_WITH_AS(json(json::value_t::null).get(), + "[json.exception.type_error.302] type must be string, but is null", json::type_error&); + CHECK_THROWS_WITH_AS(json(json::value_t::object).get(), + "[json.exception.type_error.302] type must be string, but is object", json::type_error&); + CHECK_THROWS_WITH_AS(json(json::value_t::array).get(), + "[json.exception.type_error.302] type must be string, but is array", json::type_error&); + CHECK_THROWS_WITH_AS(json(json::value_t::boolean).get(), + "[json.exception.type_error.302] type must be string, but is boolean", json::type_error&); + CHECK_THROWS_WITH_AS(json(json::value_t::number_integer).get(), + "[json.exception.type_error.302] type must be string, but is number", json::type_error&); + CHECK_THROWS_WITH_AS(json(json::value_t::number_unsigned).get(), + "[json.exception.type_error.302] type must be string, but is number", json::type_error&); + CHECK_THROWS_WITH_AS(json(json::value_t::number_float).get(), + "[json.exception.type_error.302] type must be string, but is number", json::type_error&); + } + } + + SECTION("get a string (explicit, get_to)") + { + const json::string_t s_reference{"Hello world"}; + json j(s_reference); + + SECTION("std::string_view") + { + std::string const s = "previous value"; + std::string_view sv = s; + j.get_to(sv); + CHECK(json(sv) == j); + } + } + +#if JSON_USE_IMPLICIT_CONVERSIONS + SECTION("get a string (implicit)") + { + const json::string_t s_reference{"Hello world"}; + json j(s_reference); + + SECTION("std::string_view") + { + std::string_view const s = j.get(); + CHECK(json(s) == j); + } + } +#endif + +} + +#endif diff --git a/tests/src/unit-conversions1.cpp b/tests/src/unit-conversions1.cpp index 3d39e10c9..b0cf159b8 100644 --- a/tests/src/unit-conversions1.cpp +++ b/tests/src/unit-conversions1.cpp @@ -6,12 +6,11 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. To avoid rebuilding this +// large file for every standard, tests that depend on the standard version (e.g., +// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +// go into a separate file unit-conversions1-cpp.cpp. This file stays C++11-only. #include "doctest_compatibility.h" @@ -31,25 +30,6 @@ using nlohmann::json; DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") -#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 - #define JSON_HAS_CPP_17 - #define JSON_HAS_CPP_14 -#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) - #define JSON_HAS_CPP_14 -#endif - -#ifdef JSON_HAS_CPP_17 - #if __has_include() - #include - #elif __has_include() - #include - #endif -#endif - -#if defined(JSON_HAS_CPP_17) - #include -#endif - TEST_CASE("value conversion") { SECTION("get an object (explicit)") @@ -519,13 +499,6 @@ TEST_CASE("value conversion") const std::string s = j.get(); CHECK(json(s) == j); } -#if defined(JSON_HAS_CPP_17) - SECTION("std::string_view") - { - std::string_view const s = j.get(); - CHECK(json(s) == j); - } -#endif SECTION("exception in case of a non-string type") { @@ -551,26 +524,6 @@ TEST_CASE("value conversion") json(json::value_t::number_float).get(), "[json.exception.type_error.302] type must be string, but is number", json::type_error&); } - -#if defined(JSON_HAS_CPP_17) - SECTION("exception in case of a non-string type using string_view") - { - CHECK_THROWS_WITH_AS(json(json::value_t::null).get(), - "[json.exception.type_error.302] type must be string, but is null", json::type_error&); - CHECK_THROWS_WITH_AS(json(json::value_t::object).get(), - "[json.exception.type_error.302] type must be string, but is object", json::type_error&); - CHECK_THROWS_WITH_AS(json(json::value_t::array).get(), - "[json.exception.type_error.302] type must be string, but is array", json::type_error&); - CHECK_THROWS_WITH_AS(json(json::value_t::boolean).get(), - "[json.exception.type_error.302] type must be string, but is boolean", json::type_error&); - CHECK_THROWS_WITH_AS(json(json::value_t::number_integer).get(), - "[json.exception.type_error.302] type must be string, but is number", json::type_error&); - CHECK_THROWS_WITH_AS(json(json::value_t::number_unsigned).get(), - "[json.exception.type_error.302] type must be string, but is number", json::type_error&); - CHECK_THROWS_WITH_AS(json(json::value_t::number_float).get(), - "[json.exception.type_error.302] type must be string, but is number", json::type_error&); - } -#endif } SECTION("get a string (explicit, get_to)") @@ -591,15 +544,6 @@ TEST_CASE("value conversion") j.get_to(s); CHECK(json(s) == j); } -#if defined(JSON_HAS_CPP_17) - SECTION("std::string_view") - { - std::string const s = "previous value"; - std::string_view sv = s; - j.get_to(sv); - CHECK(json(sv) == j); - } -#endif } SECTION("get null (explicit)") @@ -638,14 +582,6 @@ TEST_CASE("value conversion") CHECK(json(s) == j); } -#if defined(JSON_HAS_CPP_17) - SECTION("std::string_view") - { - std::string_view const s = j.get(); - CHECK(json(s) == j); - } -#endif - SECTION("std::string") { const std::string s = j; @@ -1256,11 +1192,4 @@ TEST_CASE("value conversion") #endif } -#ifdef JSON_HAS_CPP_17 - #undef JSON_HAS_CPP_17 -#endif - -#ifdef JSON_HAS_CPP_14 - #undef JSON_HAS_CPP_14 -#endif DOCTEST_CLANG_SUPPRESS_WARNING_POP diff --git a/tests/src/unit-conversions2-cpp17.cpp b/tests/src/unit-conversions2-cpp17.cpp new file mode 100644 index 000000000..ff578179a --- /dev/null +++ b/tests/src/unit-conversions2-cpp17.cpp @@ -0,0 +1,225 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-conversions2.cpp (conversions of +// std::filesystem::path, std::u8string and std::optional). It is kept in a separate +// translation unit so the (much larger) unit-conversions2.cpp is built for C++11 only and +// not rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +// workaround for MSVC, which does not set __cplusplus to the language version (#464) +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 +#endif + +// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") + +#ifdef JSON_HAS_CPP_17 +#include +#include +#include +#include +#include +#include + +#if __has_include() + #include +#elif __has_include() + #include +#endif + +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +TEST_CASE("std::filesystem::path") +{ + SECTION("ascii") + { + json const j_string = "Path"; + auto p = j_string.template get(); + json const j_path = p; + + CHECK(j_path.template get() == + j_string.template get()); + } + + SECTION("utf-8") + { + json const j_string = "P\xc4\x9b\xc5\xa1ina"; + auto p = j_string.template get(); + json const j_path = p; + + CHECK(j_path.template get() == + j_string.template get()); + } +} + +#endif + +// the ADL to_json overload for std::u8string only exists under the same guard +// as std::filesystem::path support (it is otherwise only reached indirectly, +// via std::filesystem::path::u8string()) -- mirror both #if conditions from +// include/nlohmann/detail/conversions/to_json.hpp exactly +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +#if defined(__cpp_lib_char8_t) +TEST_CASE("std::u8string") +{ + SECTION("ascii") + { + const std::u8string s = u8"Path"; + json const j = s; + + CHECK(j.template get() == "Path"); + } + + SECTION("utf-8") + { + // use \u universal-character-names (rather than raw \x byte escapes + // or literal non-ASCII source bytes) to compose the multi-byte UTF-8 + // encoding -- MSVC treats \x escapes used that way inside a u8 + // literal as a nonstandard extension (warning C5321), which some of + // our CI configs promote to an error; \u is portable and produces + // the exact same encoded bytes without depending on the source + // file's encoding + const std::u8string s = u8"P\u011B\u0161ina"; + json const j = s; + + CHECK(j.template get() == "P\xc4\x9b\xc5\xa1ina"); + } +} + +#endif +#endif + +#if !defined(JSON_NOEXCEPTION) +namespace +{ +// a type whose to_json reports an error by throwing, used below to check that +// converting a std::optional to JSON propagates an exception thrown while +// converting its contained value instead of calling std::terminate (#5642) +struct throwing_to_json_type {}; + +[[noreturn]] void to_json(json& /*unused*/, const throwing_to_json_type& /*unused*/) +{ + throw std::runtime_error("cannot serialize throwing_to_json_type"); +} +} // namespace +#endif + +TEST_CASE("std::optional") +{ + SECTION("null") + { + const json j_null; + const std::optional opt_null; + + CHECK(json(opt_null) == j_null); + CHECK(j_null.get>() == std::nullopt); + + // Constructing std::optional directly from JSON null throws because + // std::optional's own converting constructor is chosen over basic_json's + // operator T(). This is a language-level limitation (std::optional is + // constructible from T, and T is constructible from basic_json via the + // operator); there is no SFINAE path that distinguishes "call from inside + // std::optional's constructor" from "direct call". Use get>() + // or get_to() instead for correct null handling. See #4864 and #5246. + CHECK_THROWS_WITH_AS(std::optional(j_null), + "[json.exception.type_error.302] type must be string, but is null", json::type_error&); + CHECK_THROWS_WITH_AS(std::optional(j_null), + "[json.exception.type_error.302] type must be number, but is null", json::type_error&); + + // Assignment goes through the same overload resolution as direct + // construction, so it throws for the same reason. This relies on + // basic_json's implicit conversion operator, so it only applies + // when JSON_USE_IMPLICIT_CONVERSIONS is enabled (the default). +#if JSON_USE_IMPLICIT_CONVERSIONS + std::optional opt_assign; + CHECK_THROWS_WITH_AS(opt_assign = j_null, + "[json.exception.type_error.302] type must be string, but is null", json::type_error&); +#endif + + // get_to() is the correct way to obtain std::nullopt from a JSON null. + std::optional opt_get_to = "placeholder"; + j_null.get_to(opt_get_to); + CHECK(opt_get_to == std::nullopt); + } + + SECTION("string") + { + json j_string = "string"; + std::optional opt_string = "string"; + + CHECK(json(opt_string) == j_string); + CHECK(std::optional(j_string) == opt_string); + // false positive: Infer attributes the destruction of the temporaries above to opt_string + // @infer-ignore USE_AFTER_DELETE + } + + SECTION("bool") + { + json j_bool = true; + std::optional opt_bool = true; + + CHECK(json(opt_bool) == j_bool); + CHECK(std::optional(j_bool) == opt_bool); + } + + SECTION("number") + { + json j_number = 1; + std::optional opt_int = 1; + + CHECK(json(opt_int) == j_number); + CHECK(j_number.get>() == opt_int); + } + + SECTION("array") + { + json j_array = {1, 2, nullptr}; + std::vector> opt_array = {{1, 2, std::nullopt}}; + + CHECK(json(opt_array) == j_array); + CHECK(j_array.get>>() == opt_array); + } + + SECTION("object") + { + json j_object = {{"one", 1}, {"two", 2}, {"zero", nullptr}}; + std::map> opt_object {{"one", 1}, {"two", 2}, {"zero", std::nullopt}}; + + CHECK(json(opt_object) == j_object); + CHECK(std::map>(j_object) == opt_object); + } + +#if !defined(JSON_NOEXCEPTION) + SECTION("exception from contained value's to_json propagates (#5642)") + { + // to_json(BasicJsonType&, const std::optional&) must not be + // noexcept: it calls T's to_json, which may throw (a user-defined + // to_json that reports an error, or std::bad_alloc for T = + // std::string/vector/json). Before the fix, this called + // std::terminate() instead of letting the exception propagate. + const std::optional opt = throwing_to_json_type{}; + CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&); + + // the conversion is noexcept exactly when converting the contained value is + // (except with MSVC 2017, where it is never noexcept, see to_json.hpp) +#if !defined(_MSC_VER) || defined(__clang__) || _MSC_VER >= 1920 + static_assert(!std::is_nothrow_constructible&>::value); + static_assert(std::is_nothrow_constructible&>::value); +#endif + } +#endif +} + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +#endif diff --git a/tests/src/unit-conversions2.cpp b/tests/src/unit-conversions2.cpp index c543e4ff8..e83782576 100644 --- a/tests/src/unit-conversions2.cpp +++ b/tests/src/unit-conversions2.cpp @@ -6,12 +6,11 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. To avoid rebuilding this +// large file for every standard, tests that depend on the standard version (e.g., +// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +// go into a separate file unit-conversions2-cpp.cpp. This file stays C++11-only. #include "doctest_compatibility.h" @@ -36,25 +35,6 @@ using nlohmann::json; DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") -#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 - #define JSON_HAS_CPP_17 - #define JSON_HAS_CPP_14 -#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) - #define JSON_HAS_CPP_14 -#endif - -#ifdef JSON_HAS_CPP_17 - #if __has_include() - #include - #elif __has_include() - #include - #endif -#endif - -#if defined(JSON_HAS_CPP_17) - #include -#endif - TEST_CASE("value conversion") { SECTION("get a binary value (explicit)") @@ -680,194 +660,4 @@ TEST_CASE("Strict JSON to enum mapping") } } - -#ifdef JSON_HAS_CPP_17 -#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM -TEST_CASE("std::filesystem::path") -{ - SECTION("ascii") - { - json const j_string = "Path"; - auto p = j_string.template get(); - json const j_path = p; - - CHECK(j_path.template get() == - j_string.template get()); - } - - SECTION("utf-8") - { - json const j_string = "P\xc4\x9b\xc5\xa1ina"; - auto p = j_string.template get(); - json const j_path = p; - - CHECK(j_path.template get() == - j_string.template get()); - } -} -#endif - -// the ADL to_json overload for std::u8string only exists under the same guard -// as std::filesystem::path support (it is otherwise only reached indirectly, -// via std::filesystem::path::u8string()) -- mirror both #if conditions from -// include/nlohmann/detail/conversions/to_json.hpp exactly -#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM -#if defined(__cpp_lib_char8_t) -TEST_CASE("std::u8string") -{ - SECTION("ascii") - { - const std::u8string s = u8"Path"; - json const j = s; - - CHECK(j.template get() == "Path"); - } - - SECTION("utf-8") - { - // use \u universal-character-names (rather than raw \x byte escapes - // or literal non-ASCII source bytes) to compose the multi-byte UTF-8 - // encoding -- MSVC treats \x escapes used that way inside a u8 - // literal as a nonstandard extension (warning C5321), which some of - // our CI configs promote to an error; \u is portable and produces - // the exact same encoded bytes without depending on the source - // file's encoding - const std::u8string s = u8"P\u011B\u0161ina"; - json const j = s; - - CHECK(j.template get() == "P\xc4\x9b\xc5\xa1ina"); - } -} -#endif -#endif - -#if !defined(JSON_NOEXCEPTION) -namespace -{ -// a type whose to_json reports an error by throwing, used below to check that -// converting a std::optional to JSON propagates an exception thrown while -// converting its contained value instead of calling std::terminate (#5642) -struct throwing_to_json_type {}; - -[[noreturn]] void to_json(json& /*unused*/, const throwing_to_json_type& /*unused*/) -{ - throw std::runtime_error("cannot serialize throwing_to_json_type"); -} -} // namespace -#endif - -TEST_CASE("std::optional") -{ - SECTION("null") - { - const json j_null; - const std::optional opt_null; - - CHECK(json(opt_null) == j_null); - CHECK(j_null.get>() == std::nullopt); - - // Constructing std::optional directly from JSON null throws because - // std::optional's own converting constructor is chosen over basic_json's - // operator T(). This is a language-level limitation (std::optional is - // constructible from T, and T is constructible from basic_json via the - // operator); there is no SFINAE path that distinguishes "call from inside - // std::optional's constructor" from "direct call". Use get>() - // or get_to() instead for correct null handling. See #4864 and #5246. - CHECK_THROWS_WITH_AS(std::optional(j_null), - "[json.exception.type_error.302] type must be string, but is null", json::type_error&); - CHECK_THROWS_WITH_AS(std::optional(j_null), - "[json.exception.type_error.302] type must be number, but is null", json::type_error&); - - // Assignment goes through the same overload resolution as direct - // construction, so it throws for the same reason. This relies on - // basic_json's implicit conversion operator, so it only applies - // when JSON_USE_IMPLICIT_CONVERSIONS is enabled (the default). -#if JSON_USE_IMPLICIT_CONVERSIONS - std::optional opt_assign; - CHECK_THROWS_WITH_AS(opt_assign = j_null, - "[json.exception.type_error.302] type must be string, but is null", json::type_error&); -#endif - - // get_to() is the correct way to obtain std::nullopt from a JSON null. - std::optional opt_get_to = "placeholder"; - j_null.get_to(opt_get_to); - CHECK(opt_get_to == std::nullopt); - } - - SECTION("string") - { - json j_string = "string"; - std::optional opt_string = "string"; - - CHECK(json(opt_string) == j_string); - CHECK(std::optional(j_string) == opt_string); - // false positive: Infer attributes the destruction of the temporaries above to opt_string - // @infer-ignore USE_AFTER_DELETE - } - - SECTION("bool") - { - json j_bool = true; - std::optional opt_bool = true; - - CHECK(json(opt_bool) == j_bool); - CHECK(std::optional(j_bool) == opt_bool); - } - - SECTION("number") - { - json j_number = 1; - std::optional opt_int = 1; - - CHECK(json(opt_int) == j_number); - CHECK(j_number.get>() == opt_int); - } - - SECTION("array") - { - json j_array = {1, 2, nullptr}; - std::vector> opt_array = {{1, 2, std::nullopt}}; - - CHECK(json(opt_array) == j_array); - CHECK(j_array.get>>() == opt_array); - } - - SECTION("object") - { - json j_object = {{"one", 1}, {"two", 2}, {"zero", nullptr}}; - std::map> opt_object {{"one", 1}, {"two", 2}, {"zero", std::nullopt}}; - - CHECK(json(opt_object) == j_object); - CHECK(std::map>(j_object) == opt_object); - } - -#if !defined(JSON_NOEXCEPTION) - SECTION("exception from contained value's to_json propagates (#5642)") - { - // to_json(BasicJsonType&, const std::optional&) must not be - // noexcept: it calls T's to_json, which may throw (a user-defined - // to_json that reports an error, or std::bad_alloc for T = - // std::string/vector/json). Before the fix, this called - // std::terminate() instead of letting the exception propagate. - const std::optional opt = throwing_to_json_type{}; - CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&); - - // the conversion is noexcept exactly when converting the contained value is - // (except with MSVC 2017, where it is never noexcept, see to_json.hpp) -#if !defined(_MSC_VER) || defined(__clang__) || _MSC_VER >= 1920 - static_assert(!std::is_nothrow_constructible&>::value); - static_assert(std::is_nothrow_constructible&>::value); -#endif - } -#endif -} -#endif - -#ifdef JSON_HAS_CPP_17 - #undef JSON_HAS_CPP_17 -#endif - -#ifdef JSON_HAS_CPP_14 - #undef JSON_HAS_CPP_14 -#endif DOCTEST_CLANG_SUPPRESS_WARNING_POP diff --git a/tests/src/unit-custom-binary-type-cpp17.cpp b/tests/src/unit-custom-binary-type-cpp17.cpp new file mode 100644 index 000000000..7cc0e2e6e --- /dev/null +++ b/tests/src/unit-custom-binary-type-cpp17.cpp @@ -0,0 +1,58 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-custom-binary-type.cpp (binary types +// whose value type is std::byte). It is kept in a separate translation unit so the (much +// larger) unit-custom-binary-type.cpp is built for C++11 only and not rebuilt for every +// C++ standard. + +#include "doctest_compatibility.h" + +#include + +#ifdef JSON_HAS_CPP_17 +#include +#include +#include +#include +#include +#include +#include + +// a BinaryType whose value type is not an integer type at all +using byte_binary_json = nlohmann::basic_json < + std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, + double, std::allocator, nlohmann::adl_serializer, std::vector, void >; + +TEST_CASE("binary type whose value type is not std::uint8_t (C++17)") +{ + SECTION("dumping a value type that is not an integer") + { + const std::vector bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}}; + CHECK(byte_binary_json::binary(bytes).dump() == R"({"bytes":[0,1,255],"subtype":null})"); + CHECK(byte_binary_json::binary(bytes, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})"); + CHECK(byte_binary_json::binary({}).dump() == R"({"bytes":[],"subtype":null})"); + } + + SECTION("hashing and the binary formats") + { + const std::vector bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}}; + const auto j = byte_binary_json::binary(bytes); + + CHECK(std::hash {}(j) == std::hash {}(j)); + CHECK(byte_binary_json::from_cbor(byte_binary_json::to_cbor(j)) == j); + CHECK(byte_binary_json::from_msgpack(byte_binary_json::to_msgpack(j)) == j); + + // UBJSON has no binary type, so binary values are written as an array + CHECK(byte_binary_json::from_ubjson(byte_binary_json::to_ubjson(j)) == byte_binary_json({0, 1, 255})); + // the same holds for BON8 + CHECK(byte_binary_json::from_bon8(byte_binary_json::to_bon8(j)) == byte_binary_json({0, 1, 255})); + } +} + +#endif diff --git a/tests/src/unit-custom-binary-type.cpp b/tests/src/unit-custom-binary-type.cpp index d885376cb..d85aa8593 100644 --- a/tests/src/unit-custom-binary-type.cpp +++ b/tests/src/unit-custom-binary-type.cpp @@ -17,10 +17,6 @@ #include #include -#ifdef JSON_HAS_CPP_17 - #include -#endif - namespace { @@ -30,13 +26,6 @@ using char_binary_json = nlohmann::basic_json < std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, double, std::allocator, nlohmann::adl_serializer, std::vector, void >; -#ifdef JSON_HAS_CPP_17 - // a BinaryType whose value type is not an integer type at all - using byte_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; -#endif - } // namespace TEST_CASE("binary type whose value type is not std::uint8_t") @@ -63,29 +52,4 @@ TEST_CASE("binary type whose value type is not std::uint8_t") { CHECK(nlohmann::json::binary({0, 1, 255}, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})"); } - -#ifdef JSON_HAS_CPP_17 - SECTION("dumping a value type that is not an integer") - { - const std::vector bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}}; - CHECK(byte_binary_json::binary(bytes).dump() == R"({"bytes":[0,1,255],"subtype":null})"); - CHECK(byte_binary_json::binary(bytes, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})"); - CHECK(byte_binary_json::binary({}).dump() == R"({"bytes":[],"subtype":null})"); - } - - SECTION("hashing and the binary formats") - { - const std::vector bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}}; - const auto j = byte_binary_json::binary(bytes); - - CHECK(std::hash {}(j) == std::hash {}(j)); - CHECK(byte_binary_json::from_cbor(byte_binary_json::to_cbor(j)) == j); - CHECK(byte_binary_json::from_msgpack(byte_binary_json::to_msgpack(j)) == j); - - // UBJSON has no binary type, so binary values are written as an array - CHECK(byte_binary_json::from_ubjson(byte_binary_json::to_ubjson(j)) == byte_binary_json({0, 1, 255})); - // the same holds for BON8 - CHECK(byte_binary_json::from_bon8(byte_binary_json::to_bon8(j)) == byte_binary_json({0, 1, 255})); - } -#endif } diff --git a/tests/src/unit-deserialization-cpp20.cpp b/tests/src/unit-deserialization-cpp20.cpp new file mode 100644 index 000000000..850606d66 --- /dev/null +++ b/tests/src/unit-deserialization-cpp20.cpp @@ -0,0 +1,177 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-deserialization.cpp (char8_t support: +// _json with char8_t literals and char8_t input). It is kept in a separate translation +// unit so the (much larger) unit-deserialization.cpp is built for C++11 only and not +// rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#ifdef JSON_HAS_CPP_20 +#include +#include + +#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L) +namespace +{ +// copy of SaxEventLogger from unit-deserialization.cpp (renamed to avoid clashes in unity builds) +struct SaxEventLoggerChar8 : public nlohmann::json_sax +{ + bool null() override + { + events.emplace_back("null()"); + return true; + } + + bool boolean(bool val) override + { + events.emplace_back(val ? "boolean(true)" : "boolean(false)"); + return true; + } + + bool number_integer(json::number_integer_t val) override + { + events.push_back("number_integer(" + std::to_string(val) + ")"); + return true; + } + + bool number_unsigned(json::number_unsigned_t val) override + { + events.push_back("number_unsigned(" + std::to_string(val) + ")"); + return true; + } + + bool number_float(json::number_float_t /*val*/, const std::string& s) override + { + events.push_back("number_float(" + s + ")"); + return true; + } + + bool string(std::string& val) override + { + events.push_back("string(" + val + ")"); + return true; + } + + bool binary(json::binary_t& val) override + { + std::string binary_contents = "binary("; + std::string comma_space; + for (auto b : val) + { + binary_contents.append(comma_space); + binary_contents.append(std::to_string(static_cast(b))); + comma_space = ", "; + } + binary_contents.append(")"); + events.push_back(binary_contents); + return true; + } + + bool start_object(std::size_t elements) override + { + if (elements == (std::numeric_limits::max)()) + { + events.emplace_back("start_object()"); + } + else + { + events.push_back("start_object(" + std::to_string(elements) + ")"); + } + return true; + } + + bool key(std::string& val) override + { + events.push_back("key(" + val + ")"); + return true; + } + + bool end_object() override + { + events.emplace_back("end_object()"); + return true; + } + + bool start_array(std::size_t elements) override + { + if (elements == (std::numeric_limits::max)()) + { + events.emplace_back("start_array()"); + } + else + { + events.push_back("start_array(" + std::to_string(elements) + ")"); + } + return true; + } + + bool end_array() override + { + events.emplace_back("end_array()"); + return true; + } + + bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& /*ex*/) override + { + events.push_back("parse_error(" + std::to_string(position) + ")"); + return false; + } + + std::vector events {}; // NOLINT(readability-redundant-member-init) +}; +} // namespace + +TEST_CASE_TEMPLATE("deserialization of different character types (ASCII) (C++20)", T, char8_t) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) +{ + std::vector const v = {'t', 'r', 'u', 'e'}; + CHECK(json::parse(v) == json(true)); + CHECK(json::accept(v)); + + SaxEventLoggerChar8 l; + CHECK(json::sax_parse(v, &l)); + CHECK(l.events.size() == 1); + CHECK(l.events == std::vector({"boolean(true)"})); +} +#endif + +TEST_CASE("deserialization (C++20)") +{ +#if defined(__cpp_char8_t) + SECTION("Using _json with char8_t literals #4945") + { + // Regular narrow string literal + const auto j1 = R"({"key": "value", "num": 42})"_json; + CHECK(j1["key"] == "value"); + CHECK(j1["num"] == 42); + + // UTF-8 prefixed literal (C++20 and later); the emoji is written as a + // \U escape rather than a raw multibyte character so this does not + // depend on the compiler's source-file encoding (e.g., MSVC without + // /utf-8, or classic ICC, which does not encode non-ASCII narrow + // string literals as UTF-8 - compare against a \x-escaped expectation + // for the same reason) + const auto j2 = u8"{\"emoji\": \"\U0001F600\", \"msg\": \"hello\"}"_json; + CHECK(j2["emoji"] == "\xF0\x9F\x98\x80"); + CHECK(j2["msg"] == "hello"); + + const auto j3 = u8R"({"key": "value", "num": 42})"_json; + CHECK(j3["key"] == "value"); + CHECK(j3["num"] == 42); + } +#endif +} + +#endif diff --git a/tests/src/unit-deserialization.cpp b/tests/src/unit-deserialization.cpp index ca9b63112..b586e3006 100644 --- a/tests/src/unit-deserialization.cpp +++ b/tests/src/unit-deserialization.cpp @@ -1306,44 +1306,9 @@ TEST_CASE("deserialization") CHECK(line == "foo"); } } - - // build with C++20 - // JSON_HAS_CPP_20 -#if defined(__cpp_char8_t) - SECTION("Using _json with char8_t literals #4945") - { - // Regular narrow string literal - const auto j1 = R"({"key": "value", "num": 42})"_json; - CHECK(j1["key"] == "value"); - CHECK(j1["num"] == 42); - - // UTF-8 prefixed literal (C++20 and later); the emoji is written as a - // \U escape rather than a raw multibyte character so this does not - // depend on the compiler's source-file encoding (e.g., MSVC without - // /utf-8, or classic ICC, which does not encode non-ASCII narrow - // string literals as UTF-8 - compare against a \x-escaped expectation - // for the same reason) - const auto j2 = u8"{\"emoji\": \"\U0001F600\", \"msg\": \"hello\"}"_json; - CHECK(j2["emoji"] == "\xF0\x9F\x98\x80"); - CHECK(j2["msg"] == "hello"); - - const auto j3 = u8R"({"key": "value", "num": 42})"_json; - CHECK(j3["key"] == "value"); - CHECK(j3["num"] == 42); - } -#endif } -// select the types to test - char8_t is only available since C++20 if and only -// if __cpp_char8_t is defined. -#define TYPE_LIST(...) __VA_ARGS__ -#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L) - #define ASCII_TYPES TYPE_LIST(char, wchar_t, char16_t, char32_t, char8_t) -#else - #define ASCII_TYPES TYPE_LIST(char, wchar_t, char16_t, char32_t) -#endif - -TEST_CASE_TEMPLATE("deserialization of different character types (ASCII)", T, ASCII_TYPES) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) +TEST_CASE_TEMPLATE("deserialization of different character types (ASCII)", T, char, wchar_t, char16_t, char32_t) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) { std::vector const v = {'t', 'r', 'u', 'e'}; CHECK(json::parse(v) == json(true)); diff --git a/tests/src/unit-element_access2-cpp17.cpp b/tests/src/unit-element_access2-cpp17.cpp new file mode 100644 index 000000000..6417eb9a9 --- /dev/null +++ b/tests/src/unit-element_access2-cpp17.cpp @@ -0,0 +1,1043 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-element_access2.cpp (lookup with +// std::string_view keys). It is kept in a separate translation unit so the (much +// larger) unit-element_access2.cpp is built for C++11 only. + +#include "doctest_compatibility.h" + +#include +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#ifdef JSON_HAS_CPP_17 +#include +#include +#include +#include + +// see unit-element_access2.cpp; renamed to avoid clashes in unity builds +template +using can_call_sv_find = decltype(std::declval().find(std::declval())); + +template +using can_call_sv_count = decltype(std::declval().count(std::declval())); + +template +using can_call_sv_contains = decltype(std::declval().contains(std::declval())); + +TEST_CASE_TEMPLATE("element access 2 (C++17)", Json, nlohmann::json, nlohmann::ordered_json) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) +{ + SECTION("object") + { + Json j = {{"integer", 1}, {"unsigned", 1u}, {"floating", 42.23}, {"null", nullptr}, {"string", "hello world"}, {"boolean", true}, {"object", Json::object()}, {"array", {1, 2, 3}}}; + const Json j_const = j; + + SECTION("access specified element with bounds checking") + { + SECTION("access within bounds") + { + CHECK(j.at(std::string_view("integer")) == Json(1)); + CHECK(j.at(std::string_view("unsigned")) == Json(1u)); + CHECK(j.at(std::string_view("boolean")) == Json(true)); + CHECK(j.at(std::string_view("null")) == Json(nullptr)); + CHECK(j.at(std::string_view("string")) == Json("hello world")); + CHECK(j.at(std::string_view("floating")) == Json(42.23)); + CHECK(j.at(std::string_view("object")) == Json::object()); + CHECK(j.at(std::string_view("array")) == Json({1, 2, 3})); + + CHECK(j_const.at(std::string_view("integer")) == Json(1)); + CHECK(j_const.at(std::string_view("unsigned")) == Json(1u)); + CHECK(j_const.at(std::string_view("boolean")) == Json(true)); + CHECK(j_const.at(std::string_view("null")) == Json(nullptr)); + CHECK(j_const.at(std::string_view("string")) == Json("hello world")); + CHECK(j_const.at(std::string_view("floating")) == Json(42.23)); + CHECK(j_const.at(std::string_view("object")) == Json::object()); + CHECK(j_const.at(std::string_view("array")) == Json({1, 2, 3})); + } + + SECTION("access outside bounds") + { + CHECK_THROWS_WITH_AS(j.at(std::string_view("foo")), "[json.exception.out_of_range.403] key 'foo' not found", typename Json::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at(std::string_view("foo")), "[json.exception.out_of_range.403] key 'foo' not found", typename Json::out_of_range&); + } + + SECTION("access on non-object type") + { + SECTION("null") + { + Json j_nonobject(Json::value_t::null); + const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) + + CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view(std::string_view("foo"))), "[json.exception.type_error.304] cannot use at() with null", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view(std::string_view("foo"))), "[json.exception.type_error.304] cannot use at() with null", typename Json::type_error&); + } + + SECTION("boolean") + { + Json j_nonobject(Json::value_t::boolean); + const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) + + CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with boolean", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with boolean", typename Json::type_error&); + } + + SECTION("string") + { + Json j_nonobject(Json::value_t::string); + const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) + + CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with string", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with string", typename Json::type_error&); + } + + SECTION("array") + { + Json j_nonobject(Json::value_t::array); + const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) + + CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with array", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with array", typename Json::type_error&); + } + + SECTION("number (integer)") + { + Json j_nonobject(Json::value_t::number_integer); + const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) + + CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); + } + + SECTION("number (unsigned)") + { + Json j_nonobject(Json::value_t::number_unsigned); + const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) + + CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); + } + + SECTION("number (floating-point)") + { + Json j_nonobject(Json::value_t::number_float); + const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) + + CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); + } + } + } + + SECTION("access specified element with default value") + { + SECTION("given a key") + { + SECTION("access existing value") + { + CHECK(j.value(std::string_view("integer"), 2) == 1); + CHECK(j.value(std::string_view("integer"), 1.0) == Approx(1)); + CHECK(j.value(std::string_view("unsigned"), 2) == 1u); + CHECK(j.value(std::string_view("unsigned"), 1.0) == Approx(1u)); + CHECK(j.value(std::string_view("null"), Json(1)) == Json()); + CHECK(j.value(std::string_view("boolean"), false) == true); + CHECK(j.value(std::string_view("string"), "bar") == "hello world"); + CHECK(j.value(std::string_view("string"), std::string("bar")) == "hello world"); + CHECK(j.value(std::string_view("floating"), 12.34) == Approx(42.23)); + CHECK(j.value(std::string_view("floating"), 12) == 42); + CHECK(j.value(std::string_view("object"), Json({{"foo", "bar"}})) == Json::object()); + CHECK(j.value(std::string_view("array"), Json({10, 100})) == Json({1, 2, 3})); + + CHECK(j_const.value(std::string_view("integer"), 2) == 1); + CHECK(j_const.value(std::string_view("integer"), 1.0) == Approx(1)); + CHECK(j_const.value(std::string_view("unsigned"), 2) == 1u); + CHECK(j_const.value(std::string_view("unsigned"), 1.0) == Approx(1u)); + CHECK(j_const.value(std::string_view("boolean"), false) == true); + CHECK(j_const.value(std::string_view("string"), "bar") == "hello world"); + CHECK(j_const.value(std::string_view("string"), std::string("bar")) == "hello world"); + CHECK(j_const.value(std::string_view("floating"), 12.34) == Approx(42.23)); + CHECK(j_const.value(std::string_view("floating"), 12) == 42); + CHECK(j_const.value(std::string_view("object"), Json({{"foo", "bar"}})) == Json::object()); + CHECK(j_const.value(std::string_view("array"), Json({10, 100})) == Json({1, 2, 3})); + } + + SECTION("access non-existing value") + { + CHECK(j.value(std::string_view("_"), 2) == 2); + CHECK(j.value(std::string_view("_"), 2u) == 2u); + CHECK(j.value(std::string_view("_"), false) == false); + CHECK(j.value(std::string_view("_"), "bar") == "bar"); + CHECK(j.value(std::string_view("_"), 12.34) == Approx(12.34)); + CHECK(j.value(std::string_view("_"), Json({{"foo", "bar"}})) == Json({{"foo", "bar"}})); + CHECK(j.value(std::string_view("_"), Json({10, 100})) == Json({10, 100})); + + CHECK(j_const.value(std::string_view("_"), 2) == 2); + CHECK(j_const.value(std::string_view("_"), 2u) == 2u); + CHECK(j_const.value(std::string_view("_"), false) == false); + CHECK(j_const.value(std::string_view("_"), "bar") == "bar"); + CHECK(j_const.value(std::string_view("_"), 12.34) == Approx(12.34)); + CHECK(j_const.value(std::string_view("_"), Json({{"foo", "bar"}})) == Json({{"foo", "bar"}})); + CHECK(j_const.value(std::string_view("_"), Json({10, 100})) == Json({10, 100})); + } + + SECTION("access on non-object type") + { + SECTION("null") + { + Json j_nonobject(Json::value_t::null); + const Json j_nonobject_const(Json::value_t::null); + + CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); + } + + SECTION("boolean") + { + Json j_nonobject(Json::value_t::boolean); + const Json j_nonobject_const(Json::value_t::boolean); + + CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with boolean", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with boolean", typename Json::type_error&); + } + + SECTION("string") + { + Json j_nonobject(Json::value_t::string); + const Json j_nonobject_const(Json::value_t::string); + + CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with string", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with string", typename Json::type_error&); + } + + SECTION("array") + { + Json j_nonobject(Json::value_t::array); + const Json j_nonobject_const(Json::value_t::array); + + CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); + } + + SECTION("number (integer)") + { + Json j_nonobject(Json::value_t::number_integer); + const Json j_nonobject_const(Json::value_t::number_integer); + + CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); + } + + SECTION("number (unsigned)") + { + Json j_nonobject(Json::value_t::number_unsigned); + const Json j_nonobject_const(Json::value_t::number_unsigned); + + CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); + } + + SECTION("number (floating-point)") + { + Json j_nonobject(Json::value_t::number_float); + const Json j_nonobject_const(Json::value_t::number_float); + + CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); + } + } + } + } + + SECTION("non-const operator[]") + { + { + std::string_view const key = "key"; + Json j_null; + CHECK(j_null.is_null()); + j_null[key] = 1; + CHECK(j_null.is_object()); + CHECK(j_null.size() == 1); + j_null[key] = 2; + CHECK(j_null.size() == 1); + } + } + + SECTION("access specified element") + { + SECTION("access within bounds (string_view)") + { + CHECK(j["integer"] == Json(1)); + CHECK(j[std::string_view("integer")] == j["integer"]); + + CHECK(j["unsigned"] == Json(1u)); + CHECK(j[std::string_view("unsigned")] == j["unsigned"]); + + CHECK(j["boolean"] == Json(true)); + CHECK(j[std::string_view("boolean")] == j["boolean"]); + + CHECK(j["null"] == Json(nullptr)); + CHECK(j[std::string_view("null")] == j["null"]); + + CHECK(j["string"] == Json("hello world")); + CHECK(j[std::string_view("string")] == j["string"]); + + CHECK(j["floating"] == Json(42.23)); + CHECK(j[std::string_view("floating")] == j["floating"]); + + CHECK(j["object"] == Json::object()); + CHECK(j[std::string_view("object")] == j["object"]); + + CHECK(j["array"] == Json({1, 2, 3})); + CHECK(j[std::string_view("array")] == j["array"]); + + CHECK(j_const["integer"] == Json(1)); + CHECK(j_const[std::string_view("integer")] == j["integer"]); + + CHECK(j_const["boolean"] == Json(true)); + CHECK(j_const[std::string_view("boolean")] == j["boolean"]); + + CHECK(j_const["null"] == Json(nullptr)); + CHECK(j_const[std::string_view("null")] == j["null"]); + + CHECK(j_const["string"] == Json("hello world")); + CHECK(j_const[std::string_view("string")] == j["string"]); + + CHECK(j_const["floating"] == Json(42.23)); + CHECK(j_const[std::string_view("floating")] == j["floating"]); + + CHECK(j_const["object"] == Json::object()); + CHECK(j_const[std::string_view("object")] == j["object"]); + + CHECK(j_const["array"] == Json({1, 2, 3})); + CHECK(j_const[std::string_view("array")] == j["array"]); + } + + SECTION("access on non-object type") + { + SECTION("null") + { + Json j_nonobject(Json::value_t::null); + Json j_nonobject2(Json::value_t::null); + const Json j_const_nonobject(j_nonobject); + + CHECK_NOTHROW(j_nonobject2[std::string_view("foo")]); + CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with null", typename Json::type_error&); + } + + SECTION("boolean") + { + Json j_nonobject(Json::value_t::boolean); + const Json j_const_nonobject(j_nonobject); + + CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with boolean", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with boolean", typename Json::type_error&); + } + + SECTION("string") + { + Json j_nonobject(Json::value_t::string); + const Json j_const_nonobject(j_nonobject); + + CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", typename Json::type_error&); + } + + SECTION("array") + { + Json j_nonobject(Json::value_t::array); + const Json j_const_nonobject(j_nonobject); + + CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", typename Json::type_error&); + } + + SECTION("number (integer)") + { + Json j_nonobject(Json::value_t::number_integer); + const Json j_const_nonobject(j_nonobject); + + CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); + } + + SECTION("number (unsigned)") + { + Json j_nonobject(Json::value_t::number_unsigned); + const Json j_const_nonobject(j_nonobject); + + CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); + } + + SECTION("number (floating-point)") + { + Json j_nonobject(Json::value_t::number_float); + const Json j_const_nonobject(j_nonobject); + + CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); + CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); + } + } + } + + SECTION("remove specified element") + { + SECTION("remove element by key (string_view)") + { + CHECK(j.find(std::string_view("integer")) != j.end()); + CHECK(j.erase(std::string_view("integer")) == 1); + CHECK(j.find(std::string_view("integer")) == j.end()); + CHECK(j.erase(std::string_view("integer")) == 0); + + CHECK(j.find(std::string_view("unsigned")) != j.end()); + CHECK(j.erase(std::string_view("unsigned")) == 1); + CHECK(j.find(std::string_view("unsigned")) == j.end()); + CHECK(j.erase(std::string_view("unsigned")) == 0); + + CHECK(j.find(std::string_view("boolean")) != j.end()); + CHECK(j.erase(std::string_view("boolean")) == 1); + CHECK(j.find(std::string_view("boolean")) == j.end()); + CHECK(j.erase(std::string_view("boolean")) == 0); + + CHECK(j.find(std::string_view("null")) != j.end()); + CHECK(j.erase(std::string_view("null")) == 1); + CHECK(j.find(std::string_view("null")) == j.end()); + CHECK(j.erase(std::string_view("null")) == 0); + + CHECK(j.find(std::string_view("string")) != j.end()); + CHECK(j.erase(std::string_view("string")) == 1); + CHECK(j.find(std::string_view("string")) == j.end()); + CHECK(j.erase(std::string_view("string")) == 0); + + CHECK(j.find(std::string_view("floating")) != j.end()); + CHECK(j.erase(std::string_view("floating")) == 1); + CHECK(j.find(std::string_view("floating")) == j.end()); + CHECK(j.erase(std::string_view("floating")) == 0); + + CHECK(j.find(std::string_view("object")) != j.end()); + CHECK(j.erase(std::string_view("object")) == 1); + CHECK(j.find(std::string_view("object")) == j.end()); + CHECK(j.erase(std::string_view("object")) == 0); + + CHECK(j.find(std::string_view("array")) != j.end()); + CHECK(j.erase(std::string_view("array")) == 1); + CHECK(j.find(std::string_view("array")) == j.end()); + CHECK(j.erase(std::string_view("array")) == 0); + } + + SECTION("remove element by key in non-object type") + { + SECTION("null") + { + Json j_nonobject(Json::value_t::null); + + CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with null", typename Json::type_error&); + } + + SECTION("boolean") + { + Json j_nonobject(Json::value_t::boolean); + + CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with boolean", typename Json::type_error&); + } + + SECTION("string") + { + Json j_nonobject(Json::value_t::string); + + CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with string", typename Json::type_error&); + } + + SECTION("array") + { + Json j_nonobject(Json::value_t::array); + + CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with array", typename Json::type_error&); + } + + SECTION("number (integer)") + { + Json j_nonobject(Json::value_t::number_integer); + + CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with number", typename Json::type_error&); + } + + SECTION("number (floating-point)") + { + Json j_nonobject(Json::value_t::number_float); + + CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with number", typename Json::type_error&); + } + } + } + + SECTION("find an element in an object") + { + SECTION("existing element") + { + for (const std::string_view key : + {"integer", "unsigned", "floating", "null", "string", "boolean", "object", "array" + }) + { + CHECK(j.find(key) != j.end()); + CHECK(*j.find(key) == j.at(key)); + CHECK(j_const.find(key) != j_const.end()); + CHECK(*j_const.find(key) == j_const.at(key)); + } + } + + SECTION("nonexisting element") + { + CHECK(j.find(std::string_view("foo")) == j.end()); + CHECK(j_const.find(std::string_view("foo")) == j_const.end()); + } + + SECTION("all types") + { + SECTION("null") + { + Json j_nonarray(Json::value_t::null); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + + SECTION("string") + { + Json j_nonarray(Json::value_t::string); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + + SECTION("object") + { + Json j_nonarray(Json::value_t::object); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + + SECTION("array") + { + Json j_nonarray(Json::value_t::array); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + + SECTION("boolean") + { + Json j_nonarray(Json::value_t::boolean); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + + SECTION("number (integer)") + { + Json j_nonarray(Json::value_t::number_integer); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + + SECTION("number (unsigned)") + { + Json j_nonarray(Json::value_t::number_unsigned); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + + SECTION("number (floating-point)") + { + Json j_nonarray(Json::value_t::number_float); + const Json j_nonarray_const(j_nonarray); + + CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); + CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); + } + } + } + + SECTION("count keys in an object") + { + SECTION("existing element") + { + for (const std::string_view key : + {"integer", "unsigned", "floating", "null", "string", "boolean", "object", "array" + }) + { + CHECK(j.count(key) == 1); + CHECK(j_const.count(key) == 1); + } + } + + SECTION("nonexisting element") + { + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("all types") + { + SECTION("null") + { + Json j_nonobject(Json::value_t::null); + const Json j_nonobject_const(Json::value_t::null); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("string") + { + Json j_nonobject(Json::value_t::string); + const Json j_nonobject_const(Json::value_t::string); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("object") + { + Json j_nonobject(Json::value_t::object); + const Json j_nonobject_const(Json::value_t::object); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("array") + { + Json j_nonobject(Json::value_t::array); + const Json j_nonobject_const(Json::value_t::array); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("boolean") + { + Json j_nonobject(Json::value_t::boolean); + const Json j_nonobject_const(Json::value_t::boolean); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("number (integer)") + { + Json j_nonobject(Json::value_t::number_integer); + const Json j_nonobject_const(Json::value_t::number_integer); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("number (unsigned)") + { + Json j_nonobject(Json::value_t::number_unsigned); + const Json j_nonobject_const(Json::value_t::number_unsigned); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + + SECTION("number (floating-point)") + { + Json j_nonobject(Json::value_t::number_float); + const Json j_nonobject_const(Json::value_t::number_float); + + CHECK(j.count(std::string_view("foo")) == 0); + CHECK(j_const.count(std::string_view("foo")) == 0); + } + } + } + + SECTION("check existence of key in an object") + { + SECTION("existing element") + { + for (const std::string_view key : + {"integer", "unsigned", "floating", "null", "string", "boolean", "object", "array" + }) + { + CHECK(j.contains(key) == true); + CHECK(j_const.contains(key) == true); + } + } + + SECTION("nonexisting element") + { + CHECK(j.contains(std::string_view("foo")) == false); + CHECK(j_const.contains(std::string_view("foo")) == false); + } + + SECTION("all types") + { + SECTION("null") + { + Json j_nonobject(Json::value_t::null); + const Json j_nonobject_const(Json::value_t::null); + + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + + SECTION("string") + { + Json j_nonobject(Json::value_t::string); + const Json j_nonobject_const(Json::value_t::string); + + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + + SECTION("object") + { + Json j_nonobject(Json::value_t::object); + const Json j_nonobject_const(Json::value_t::object); + + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + + SECTION("array") + { + Json j_nonobject(Json::value_t::array); + const Json j_nonobject_const(Json::value_t::array); + + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + + SECTION("boolean") + { + Json j_nonobject(Json::value_t::boolean); + const Json j_nonobject_const(Json::value_t::boolean); + + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + + SECTION("number (integer)") + { + Json j_nonobject(Json::value_t::number_integer); + const Json j_nonobject_const(Json::value_t::number_integer); + + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + + SECTION("number (unsigned)") + { + Json j_nonobject(Json::value_t::number_unsigned); + const Json j_nonobject_const(Json::value_t::number_unsigned); + + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + + SECTION("number (floating-point)") + { + Json j_nonobject(Json::value_t::number_float); + const Json j_nonobject_const(Json::value_t::number_float); + CHECK(j_nonobject.contains(std::string_view("foo")) == false); + CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); + } + } + } + } + + SECTION("integral keys for object lookup are rejected at compile time") + { + // https://github.com/nlohmann/json/issues/5657: an integer literal like 0 is a null pointer + // constant, which used to convert to a null const char* and from there--via undefined + // behavior in the std::string constructor--to key_type, so contains(0), find(0), and + // count(0) used to compile and then crash instead of failing to compile + using nlohmann::detail::is_detected; + + CHECK(is_detected::value); + CHECK(is_detected::value); + CHECK(is_detected::value); + + // the neighboring size_type overloads for array access are unaffected by the new + // integral-key overloads above (at(), operator[](), and erase() take a size_type) + Json arr = {10, 20, 30}; + const Json arr_const = arr; + arr.erase(0); + } +} + +TEST_CASE_TEMPLATE("element access 2 (additional value() tests) (C++17)", Json, nlohmann::json, nlohmann::ordered_json) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) +{ + using string_t = typename Json::string_t; + using number_integer_t = typename Json::number_integer_t; + + // test assumes string_t and object_t::key_type are the same + REQUIRE(std::is_same::value); + + Json j + { + {"foo", "bar"}, + {"baz", 42} + }; + + const char* cpstr = "default"; + const char castr[] = "default"; // NOLINT(hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-avoid-c-arrays) + string_t const str = "default"; + + number_integer_t integer = 69; + std::size_t size = 69; + + SECTION("deduced ValueType") + { + SECTION("std::string_view key") + { + std::string_view const key = "foo"; + std::string_view const key2 = "baz"; + std::string_view const key_notfound = "bar"; + + CHECK(j.value(key, "default") == "bar"); + CHECK(j.value(key, cpstr) == "bar"); + CHECK(j.value(key, castr) == "bar"); + CHECK(j.value(key, str) == "bar"); + CHECK(j.value(key2, 0) == 42); + CHECK(j.value(key2, 47) == 42); + CHECK(j.value(key2, integer) == 42); + CHECK(j.value(key2, size) == 42); + + CHECK(j.value(key_notfound, "default") == "default"); + CHECK(j.value(key_notfound, 0) == 0); + CHECK(j.value(key_notfound, 47) == 47); + CHECK(j.value(key_notfound, integer) == integer); + CHECK(j.value(key_notfound, size) == size); + + CHECK_THROWS_WITH_AS(Json().value(key, "default"), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); + CHECK_THROWS_WITH_AS(Json().value(key, str), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); + } + } + + SECTION("explicit ValueType") + { + SECTION("std::string_view key") + { + std::string_view const key = "foo"; + std::string_view const key2 = "baz"; + std::string_view const key_notfound = "bar"; + + CHECK(j.template value(key, "default") == "bar"); + CHECK(j.template value(key, cpstr) == "bar"); + CHECK(j.template value(key, castr) == "bar"); + CHECK(j.template value(key, str) == "bar"); + CHECK(j.template value(key2, 0) == 42); + CHECK(j.template value(key2, 47) == 42); + CHECK(j.template value(key2, integer) == 42); + CHECK(j.template value(key2, 0) == 42); + CHECK(j.template value(key2, 47) == 42); + CHECK(j.template value(key2, size) == 42); + + CHECK(j.template value(key_notfound, "default") == "default"); + CHECK(j.template value(key_notfound, 0) == 0); + CHECK(j.template value(key_notfound, 47) == 47); + CHECK(j.template value(key_notfound, integer) == integer); + CHECK(j.template value(key_notfound, 0) == 0); + CHECK(j.template value(key_notfound, 47) == 47); + CHECK(j.template value(key_notfound, size) == size); + + CHECK(j.template value(key, "default") == "bar"); + CHECK(j.template value(key, cpstr) == "bar"); + CHECK(j.template value(key, castr) == "bar"); + CHECK(j.template value(key, str) == "bar"); + + CHECK(j.template value(key_notfound, "default") == "default"); + + CHECK_THROWS_WITH_AS(Json().template value(key, "default"), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); + CHECK_THROWS_WITH_AS(Json().template value(key, str), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); + } + } +} + +TEST_CASE("operator[] with user-defined std::string_view-convertible types") +{ + using json = nlohmann::json; + + class TestClass + { + std::string key_data_ = "foo"; + + public: + operator std::string_view() const + { + return key_data_; + } + }; + + struct TestStruct + { + operator std::string_view() const + { + return "bar"; + } + }; + + json j = {{"foo", "from_class"}, {"bar", "from_struct"}}; + const TestClass foo_obj; + const TestStruct bar_obj; + + SECTION("read access") + { + CHECK(j[foo_obj] == "from_class"); + CHECK(j[TestClass{}] == "from_class"); + CHECK(j[bar_obj] == "from_struct"); + CHECK(j[TestStruct{}] == "from_struct"); + } + + SECTION("write access") + { + j[TestClass{}] = "updated_class"; + j[TestStruct{}] = "updated_struct"; + CHECK(j["foo"] == "updated_class"); + CHECK(j["bar"] == "updated_struct"); + + SECTION("direct std::string_view access") + { + CHECK(j[std::string_view{"foo"}] == "updated_class"); + CHECK(j[std::string_view{"bar"}] == "updated_struct"); + } + } +} + +TEST_CASE("keys convertible to std::string_view work with all lookup functions (regression test for #5663)") +{ + // a key type convertible only to std::string_view: the case #4958 added + // support for, but only the non-const operator[] compiled with it + struct ViewKey + { + operator std::string_view() const + { + return "a"; + } + }; + + // a key type convertible to both std::string and std::string_view: with + // 3.12.0, such a key worked with at, the const operator[], find, count and + // contains via the conversion to std::string; #4958 made the KeyType&& + // templates win overload resolution for it instead, and those then failed + // the lookups pick the conversion to std::string_view, which leaves the one + // to std::string unused; it has to exist to reproduce the ambiguity + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") + struct DualKey + { + operator std::string() const + { + return "a"; + } + operator std::string_view() const + { + return "a"; + } + }; + DOCTEST_CLANG_SUPPRESS_WARNING_POP + + SECTION("nlohmann::json") + { + using json = nlohmann::json; + + SECTION("ViewKey") + { + json j = {{"a", 1}}; + const json& cj = j; + + CHECK(j[ViewKey{}] == 1); + CHECK(cj[ViewKey{}] == 1); + CHECK(j.at(ViewKey{}) == 1); + CHECK(cj.at(ViewKey{}) == 1); + CHECK(j.find(ViewKey{}) != j.end()); + CHECK(cj.find(ViewKey{}) != cj.end()); + CHECK(j.count(ViewKey{}) == 1); + CHECK(j.contains(ViewKey{})); + CHECK(j.value(ViewKey{}, 0) == 1); + CHECK(j.erase(ViewKey{}) == 1); + CHECK(!j.contains("a")); + } + + SECTION("DualKey") + { + json j = {{"a", 1}}; + const json& cj = j; + + CHECK(j[DualKey{}] == 1); + CHECK(cj[DualKey{}] == 1); + CHECK(j.at(DualKey{}) == 1); + CHECK(cj.at(DualKey{}) == 1); + CHECK(j.find(DualKey{}) != j.end()); + CHECK(cj.find(DualKey{}) != cj.end()); + CHECK(j.count(DualKey{}) == 1); + CHECK(j.contains(DualKey{})); + CHECK(j.value(DualKey{}, 0) == 1); + CHECK(j.erase(DualKey{}) == 1); + CHECK(!j.contains("a")); + } + } + + SECTION("nlohmann::ordered_json") + { + using ordered_json = nlohmann::ordered_json; + + SECTION("ViewKey") + { + ordered_json j = {{"a", 1}}; + const ordered_json& cj = j; + + CHECK(j[ViewKey{}] == 1); + CHECK(cj[ViewKey{}] == 1); + CHECK(j.at(ViewKey{}) == 1); + CHECK(cj.at(ViewKey{}) == 1); + CHECK(j.find(ViewKey{}) != j.end()); + CHECK(cj.find(ViewKey{}) != cj.end()); + CHECK(j.count(ViewKey{}) == 1); + CHECK(j.contains(ViewKey{})); + CHECK(j.value(ViewKey{}, 0) == 1); + CHECK(j.erase(ViewKey{}) == 1); + CHECK(!j.contains("a")); + } + + SECTION("DualKey") + { + ordered_json j = {{"a", 1}}; + const ordered_json& cj = j; + + CHECK(j[DualKey{}] == 1); + CHECK(cj[DualKey{}] == 1); + CHECK(j.at(DualKey{}) == 1); + CHECK(cj.at(DualKey{}) == 1); + CHECK(j.find(DualKey{}) != j.end()); + CHECK(cj.find(DualKey{}) != cj.end()); + CHECK(j.count(DualKey{}) == 1); + CHECK(j.contains(DualKey{})); + CHECK(j.value(DualKey{}, 0) == 1); + CHECK(j.erase(DualKey{}) == 1); + CHECK(!j.contains("a")); + } + } +} + +#endif diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 1b59d8add..60bf2e784 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -13,9 +13,6 @@ using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif -// build test with C++14 -// JSON_HAS_CPP_14 - // used to check at compile time (via is_detected) whether a call is well-formed; see // https://github.com/nlohmann/json/issues/5657 // @@ -78,37 +75,12 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_const.at("floating") == Json(42.23)); CHECK(j_const.at("object") == Json::object()); CHECK(j_const.at("array") == Json({1, 2, 3})); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.at(std::string_view("integer")) == Json(1)); - CHECK(j.at(std::string_view("unsigned")) == Json(1u)); - CHECK(j.at(std::string_view("boolean")) == Json(true)); - CHECK(j.at(std::string_view("null")) == Json(nullptr)); - CHECK(j.at(std::string_view("string")) == Json("hello world")); - CHECK(j.at(std::string_view("floating")) == Json(42.23)); - CHECK(j.at(std::string_view("object")) == Json::object()); - CHECK(j.at(std::string_view("array")) == Json({1, 2, 3})); - - CHECK(j_const.at(std::string_view("integer")) == Json(1)); - CHECK(j_const.at(std::string_view("unsigned")) == Json(1u)); - CHECK(j_const.at(std::string_view("boolean")) == Json(true)); - CHECK(j_const.at(std::string_view("null")) == Json(nullptr)); - CHECK(j_const.at(std::string_view("string")) == Json("hello world")); - CHECK(j_const.at(std::string_view("floating")) == Json(42.23)); - CHECK(j_const.at(std::string_view("object")) == Json::object()); - CHECK(j_const.at(std::string_view("array")) == Json({1, 2, 3})); -#endif } SECTION("access outside bounds") { CHECK_THROWS_WITH_AS(j.at("foo"), "[json.exception.out_of_range.403] key 'foo' not found", typename Json::out_of_range&); CHECK_THROWS_WITH_AS(j_const.at("foo"), "[json.exception.out_of_range.403] key 'foo' not found", typename Json::out_of_range&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j.at(std::string_view("foo")), "[json.exception.out_of_range.403] key 'foo' not found", typename Json::out_of_range&); - CHECK_THROWS_WITH_AS(j_const.at(std::string_view("foo")), "[json.exception.out_of_range.403] key 'foo' not found", typename Json::out_of_range&); -#endif } SECTION("access on non-object type") @@ -119,11 +91,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) CHECK_THROWS_WITH_AS(j_nonobject.at("foo"), "[json.exception.type_error.304] cannot use at() with null", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.at("foo"), "[json.exception.type_error.304] cannot use at() with null", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view(std::string_view("foo"))), "[json.exception.type_error.304] cannot use at() with null", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view(std::string_view("foo"))), "[json.exception.type_error.304] cannot use at() with null", typename Json::type_error&); -#endif } SECTION("boolean") @@ -132,11 +99,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) CHECK_THROWS_WITH_AS(j_nonobject.at("foo"), "[json.exception.type_error.304] cannot use at() with boolean", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.at("foo"), "[json.exception.type_error.304] cannot use at() with boolean", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with boolean", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with boolean", typename Json::type_error&); -#endif } SECTION("string") @@ -145,11 +107,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) CHECK_THROWS_WITH_AS(j_nonobject.at("foo"), "[json.exception.type_error.304] cannot use at() with string", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.at("foo"), "[json.exception.type_error.304] cannot use at() with string", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with string", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with string", typename Json::type_error&); -#endif } SECTION("array") @@ -158,11 +115,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) CHECK_THROWS_WITH_AS(j_nonobject.at("foo"), "[json.exception.type_error.304] cannot use at() with array", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.at("foo"), "[json.exception.type_error.304] cannot use at() with array", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with array", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with array", typename Json::type_error&); -#endif } SECTION("number (integer)") @@ -171,11 +123,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) CHECK_THROWS_WITH_AS(j_nonobject.at("foo"), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.at("foo"), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); -#endif } SECTION("number (unsigned)") @@ -184,11 +131,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) CHECK_THROWS_WITH_AS(j_nonobject.at("foo"), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.at("foo"), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); -#endif } SECTION("number (floating-point)") @@ -197,11 +139,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(j_nonobject); // NOLINT(performance-unnecessary-copy-initialization) CHECK_THROWS_WITH_AS(j_nonobject.at("foo"), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.at("foo"), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.at(std::string_view("foo")), "[json.exception.type_error.304] cannot use at() with number", typename Json::type_error&); -#endif } } } @@ -236,33 +173,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_const.value("floating", 12) == 42); CHECK(j_const.value("object", Json({{"foo", "bar"}})) == Json::object()); CHECK(j_const.value("array", Json({10, 100})) == Json({1, 2, 3})); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.value(std::string_view("integer"), 2) == 1); - CHECK(j.value(std::string_view("integer"), 1.0) == Approx(1)); - CHECK(j.value(std::string_view("unsigned"), 2) == 1u); - CHECK(j.value(std::string_view("unsigned"), 1.0) == Approx(1u)); - CHECK(j.value(std::string_view("null"), Json(1)) == Json()); - CHECK(j.value(std::string_view("boolean"), false) == true); - CHECK(j.value(std::string_view("string"), "bar") == "hello world"); - CHECK(j.value(std::string_view("string"), std::string("bar")) == "hello world"); - CHECK(j.value(std::string_view("floating"), 12.34) == Approx(42.23)); - CHECK(j.value(std::string_view("floating"), 12) == 42); - CHECK(j.value(std::string_view("object"), Json({{"foo", "bar"}})) == Json::object()); - CHECK(j.value(std::string_view("array"), Json({10, 100})) == Json({1, 2, 3})); - - CHECK(j_const.value(std::string_view("integer"), 2) == 1); - CHECK(j_const.value(std::string_view("integer"), 1.0) == Approx(1)); - CHECK(j_const.value(std::string_view("unsigned"), 2) == 1u); - CHECK(j_const.value(std::string_view("unsigned"), 1.0) == Approx(1u)); - CHECK(j_const.value(std::string_view("boolean"), false) == true); - CHECK(j_const.value(std::string_view("string"), "bar") == "hello world"); - CHECK(j_const.value(std::string_view("string"), std::string("bar")) == "hello world"); - CHECK(j_const.value(std::string_view("floating"), 12.34) == Approx(42.23)); - CHECK(j_const.value(std::string_view("floating"), 12) == 42); - CHECK(j_const.value(std::string_view("object"), Json({{"foo", "bar"}})) == Json::object()); - CHECK(j_const.value(std::string_view("array"), Json({10, 100})) == Json({1, 2, 3})); -#endif } SECTION("access non-existing value") @@ -282,24 +192,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_const.value("_", 12.34) == Approx(12.34)); CHECK(j_const.value("_", Json({{"foo", "bar"}})) == Json({{"foo", "bar"}})); CHECK(j_const.value("_", Json({10, 100})) == Json({10, 100})); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.value(std::string_view("_"), 2) == 2); - CHECK(j.value(std::string_view("_"), 2u) == 2u); - CHECK(j.value(std::string_view("_"), false) == false); - CHECK(j.value(std::string_view("_"), "bar") == "bar"); - CHECK(j.value(std::string_view("_"), 12.34) == Approx(12.34)); - CHECK(j.value(std::string_view("_"), Json({{"foo", "bar"}})) == Json({{"foo", "bar"}})); - CHECK(j.value(std::string_view("_"), Json({10, 100})) == Json({10, 100})); - - CHECK(j_const.value(std::string_view("_"), 2) == 2); - CHECK(j_const.value(std::string_view("_"), 2u) == 2u); - CHECK(j_const.value(std::string_view("_"), false) == false); - CHECK(j_const.value(std::string_view("_"), "bar") == "bar"); - CHECK(j_const.value(std::string_view("_"), 12.34) == Approx(12.34)); - CHECK(j_const.value(std::string_view("_"), Json({{"foo", "bar"}})) == Json({{"foo", "bar"}})); - CHECK(j_const.value(std::string_view("_"), Json({10, 100})) == Json({10, 100})); -#endif } SECTION("access on non-object type") @@ -310,11 +202,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::null); CHECK_THROWS_WITH_AS(j_nonobject.value("foo", 1), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.value("foo", 1), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); -#endif } SECTION("boolean") @@ -323,11 +210,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::boolean); CHECK_THROWS_WITH_AS(j_nonobject.value("foo", 1), "[json.exception.type_error.306] cannot use value() with boolean", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.value("foo", 1), "[json.exception.type_error.306] cannot use value() with boolean", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with boolean", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with boolean", typename Json::type_error&); -#endif } SECTION("string") @@ -336,11 +218,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::string); CHECK_THROWS_WITH_AS(j_nonobject.value("foo", 1), "[json.exception.type_error.306] cannot use value() with string", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.value("foo", 1), "[json.exception.type_error.306] cannot use value() with string", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with string", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with string", typename Json::type_error&); -#endif } SECTION("array") @@ -349,11 +226,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::array); CHECK_THROWS_WITH_AS(j_nonobject.value("foo", 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.value("foo", 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with array", typename Json::type_error&); -#endif } SECTION("number (integer)") @@ -362,11 +234,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::number_integer); CHECK_THROWS_WITH_AS(j_nonobject.value("foo", 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.value("foo", 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); -#endif } SECTION("number (unsigned)") @@ -375,11 +242,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::number_unsigned); CHECK_THROWS_WITH_AS(j_nonobject.value("foo", 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.value("foo", 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); -#endif } SECTION("number (floating-point)") @@ -388,11 +250,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::number_float); CHECK_THROWS_WITH_AS(j_nonobject.value("foo", 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_nonobject_const.value("foo", 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_nonobject_const.value(std::string_view("foo"), 1), "[json.exception.type_error.306] cannot use value() with number", typename Json::type_error&); -#endif } } } @@ -555,18 +412,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j j_null["key"] = 2; CHECK(j_null.size() == 1); } -#ifdef JSON_HAS_CPP_17 - { - std::string_view const key = "key"; - Json j_null; - CHECK(j_null.is_null()); - j_null[key] = 1; - CHECK(j_null.is_object()); - CHECK(j_null.size() == 1); - j_null[key] = 2; - CHECK(j_null.size() == 1); - } -#endif } SECTION("front and back") @@ -641,56 +486,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_const[typename Json::object_t::key_type("array")] == j["array"]); } -#ifdef JSON_HAS_CPP_17 - SECTION("access within bounds (string_view)") - { - CHECK(j["integer"] == Json(1)); - CHECK(j[std::string_view("integer")] == j["integer"]); - - CHECK(j["unsigned"] == Json(1u)); - CHECK(j[std::string_view("unsigned")] == j["unsigned"]); - - CHECK(j["boolean"] == Json(true)); - CHECK(j[std::string_view("boolean")] == j["boolean"]); - - CHECK(j["null"] == Json(nullptr)); - CHECK(j[std::string_view("null")] == j["null"]); - - CHECK(j["string"] == Json("hello world")); - CHECK(j[std::string_view("string")] == j["string"]); - - CHECK(j["floating"] == Json(42.23)); - CHECK(j[std::string_view("floating")] == j["floating"]); - - CHECK(j["object"] == Json::object()); - CHECK(j[std::string_view("object")] == j["object"]); - - CHECK(j["array"] == Json({1, 2, 3})); - CHECK(j[std::string_view("array")] == j["array"]); - - CHECK(j_const["integer"] == Json(1)); - CHECK(j_const[std::string_view("integer")] == j["integer"]); - - CHECK(j_const["boolean"] == Json(true)); - CHECK(j_const[std::string_view("boolean")] == j["boolean"]); - - CHECK(j_const["null"] == Json(nullptr)); - CHECK(j_const[std::string_view("null")] == j["null"]); - - CHECK(j_const["string"] == Json("hello world")); - CHECK(j_const[std::string_view("string")] == j["string"]); - - CHECK(j_const["floating"] == Json(42.23)); - CHECK(j_const[std::string_view("floating")] == j["floating"]); - - CHECK(j_const["object"] == Json::object()); - CHECK(j_const[std::string_view("object")] == j["object"]); - - CHECK(j_const["array"] == Json({1, 2, 3})); - CHECK(j_const[std::string_view("array")] == j["array"]); - } -#endif - SECTION("access on non-object type") { SECTION("null") @@ -703,11 +498,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK_NOTHROW(j_nonobject2[typename Json::object_t::key_type("foo")]); CHECK_THROWS_WITH_AS(j_const_nonobject["foo"], "[json.exception.type_error.305] cannot use operator[] with a string argument with null", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_const_nonobject[typename Json::object_t::key_type("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with null", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_NOTHROW(j_nonobject2[std::string_view("foo")]); - CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with null", typename Json::type_error&); -#endif } SECTION("boolean") @@ -722,11 +512,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j "[json.exception.type_error.305] cannot use operator[] with a string argument with boolean", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_const_nonobject[typename Json::object_t::key_type("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with boolean", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with boolean", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with boolean", typename Json::type_error&); -#endif } SECTION("string") @@ -741,11 +526,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j "[json.exception.type_error.305] cannot use operator[] with a string argument with string", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_const_nonobject[typename Json::object_t::key_type("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", typename Json::type_error&); -#endif } SECTION("array") @@ -759,11 +539,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j "[json.exception.type_error.305] cannot use operator[] with a string argument with array", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_const_nonobject[typename Json::object_t::key_type("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", typename Json::type_error&); -#endif } SECTION("number (integer)") @@ -778,11 +553,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_const_nonobject[typename Json::object_t::key_type("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); -#endif } SECTION("number (unsigned)") @@ -797,11 +567,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_const_nonobject[typename Json::object_t::key_type("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); -#endif } SECTION("number (floating-point)") @@ -816,11 +581,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); CHECK_THROWS_WITH_AS(j_const_nonobject[typename Json::object_t::key_type("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); - CHECK_THROWS_WITH_AS(j_const_nonobject[std::string_view("foo")], "[json.exception.type_error.305] cannot use operator[] with a string argument with number", typename Json::type_error&); -#endif } } } @@ -870,51 +630,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j.erase("array") == 0); } -#ifdef JSON_HAS_CPP_17 - SECTION("remove element by key (string_view)") - { - CHECK(j.find(std::string_view("integer")) != j.end()); - CHECK(j.erase(std::string_view("integer")) == 1); - CHECK(j.find(std::string_view("integer")) == j.end()); - CHECK(j.erase(std::string_view("integer")) == 0); - - CHECK(j.find(std::string_view("unsigned")) != j.end()); - CHECK(j.erase(std::string_view("unsigned")) == 1); - CHECK(j.find(std::string_view("unsigned")) == j.end()); - CHECK(j.erase(std::string_view("unsigned")) == 0); - - CHECK(j.find(std::string_view("boolean")) != j.end()); - CHECK(j.erase(std::string_view("boolean")) == 1); - CHECK(j.find(std::string_view("boolean")) == j.end()); - CHECK(j.erase(std::string_view("boolean")) == 0); - - CHECK(j.find(std::string_view("null")) != j.end()); - CHECK(j.erase(std::string_view("null")) == 1); - CHECK(j.find(std::string_view("null")) == j.end()); - CHECK(j.erase(std::string_view("null")) == 0); - - CHECK(j.find(std::string_view("string")) != j.end()); - CHECK(j.erase(std::string_view("string")) == 1); - CHECK(j.find(std::string_view("string")) == j.end()); - CHECK(j.erase(std::string_view("string")) == 0); - - CHECK(j.find(std::string_view("floating")) != j.end()); - CHECK(j.erase(std::string_view("floating")) == 1); - CHECK(j.find(std::string_view("floating")) == j.end()); - CHECK(j.erase(std::string_view("floating")) == 0); - - CHECK(j.find(std::string_view("object")) != j.end()); - CHECK(j.erase(std::string_view("object")) == 1); - CHECK(j.find(std::string_view("object")) == j.end()); - CHECK(j.erase(std::string_view("object")) == 0); - - CHECK(j.find(std::string_view("array")) != j.end()); - CHECK(j.erase(std::string_view("array")) == 1); - CHECK(j.find(std::string_view("array")) == j.end()); - CHECK(j.erase(std::string_view("array")) == 0); - } -#endif - SECTION("remove element by iterator") { SECTION("erase(begin())") @@ -1034,60 +749,36 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j { Json j_nonobject(Json::value_t::null); CHECK_THROWS_WITH_AS(j_nonobject.erase("foo"), "[json.exception.type_error.307] cannot use erase() with null", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with null", typename Json::type_error&); -#endif } SECTION("boolean") { Json j_nonobject(Json::value_t::boolean); CHECK_THROWS_WITH_AS(j_nonobject.erase("foo"), "[json.exception.type_error.307] cannot use erase() with boolean", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with boolean", typename Json::type_error&); -#endif } SECTION("string") { Json j_nonobject(Json::value_t::string); CHECK_THROWS_WITH_AS(j_nonobject.erase("foo"), "[json.exception.type_error.307] cannot use erase() with string", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with string", typename Json::type_error&); -#endif } SECTION("array") { Json j_nonobject(Json::value_t::array); CHECK_THROWS_WITH_AS(j_nonobject.erase("foo"), "[json.exception.type_error.307] cannot use erase() with array", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with array", typename Json::type_error&); -#endif } SECTION("number (integer)") { Json j_nonobject(Json::value_t::number_integer); CHECK_THROWS_WITH_AS(j_nonobject.erase("foo"), "[json.exception.type_error.307] cannot use erase() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with number", typename Json::type_error&); -#endif } SECTION("number (floating-point)") { Json j_nonobject(Json::value_t::number_float); CHECK_THROWS_WITH_AS(j_nonobject.erase("foo"), "[json.exception.type_error.307] cannot use erase() with number", typename Json::type_error&); - -#ifdef JSON_HAS_CPP_17 - CHECK_THROWS_WITH_AS(j_nonobject.erase(std::string_view("foo")), "[json.exception.type_error.307] cannot use erase() with number", typename Json::type_error&); -#endif } } } @@ -1105,28 +796,12 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_const.find(key) != j_const.end()); CHECK(*j_const.find(key) == j_const.at(key)); } -#ifdef JSON_HAS_CPP_17 - for (const std::string_view key : - {"integer", "unsigned", "floating", "null", "string", "boolean", "object", "array" - }) - { - CHECK(j.find(key) != j.end()); - CHECK(*j.find(key) == j.at(key)); - CHECK(j_const.find(key) != j_const.end()); - CHECK(*j_const.find(key) == j_const.at(key)); - } -#endif } SECTION("nonexisting element") { CHECK(j.find("foo") == j.end()); CHECK(j_const.find("foo") == j_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.find(std::string_view("foo")) == j.end()); - CHECK(j_const.find(std::string_view("foo")) == j_const.end()); -#endif } SECTION("all types") @@ -1138,11 +813,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } SECTION("string") @@ -1152,11 +822,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } SECTION("object") @@ -1166,11 +831,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } SECTION("array") @@ -1180,11 +840,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } SECTION("boolean") @@ -1194,11 +849,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } SECTION("number (integer)") @@ -1208,11 +858,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } SECTION("number (unsigned)") @@ -1222,11 +867,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } SECTION("number (floating-point)") @@ -1236,11 +876,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonarray.find("foo") == j_nonarray.end()); CHECK(j_nonarray_const.find("foo") == j_nonarray_const.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonarray.find(std::string_view("foo")) == j_nonarray.end()); - CHECK(j_nonarray_const.find(std::string_view("foo")) == j_nonarray_const.end()); -#endif } } } @@ -1256,26 +891,12 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j.count(key) == 1); CHECK(j_const.count(key) == 1); } -#ifdef JSON_HAS_CPP_17 - for (const std::string_view key : - {"integer", "unsigned", "floating", "null", "string", "boolean", "object", "array" - }) - { - CHECK(j.count(key) == 1); - CHECK(j_const.count(key) == 1); - } -#endif } SECTION("nonexisting element") { CHECK(j.count("foo") == 0); CHECK(j_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("all types") @@ -1287,11 +908,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("string") @@ -1301,11 +917,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("object") @@ -1315,11 +926,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("array") @@ -1329,11 +935,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("boolean") @@ -1343,11 +944,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("number (integer)") @@ -1357,11 +953,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("number (unsigned)") @@ -1371,11 +962,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } SECTION("number (floating-point)") @@ -1385,11 +971,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.count("foo") == 0); CHECK(j_nonobject_const.count("foo") == 0); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.count(std::string_view("foo")) == 0); - CHECK(j_const.count(std::string_view("foo")) == 0); -#endif } } } @@ -1405,27 +986,12 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j.contains(key) == true); CHECK(j_const.contains(key) == true); } - -#ifdef JSON_HAS_CPP_17 - for (const std::string_view key : - {"integer", "unsigned", "floating", "null", "string", "boolean", "object", "array" - }) - { - CHECK(j.contains(key) == true); - CHECK(j_const.contains(key) == true); - } -#endif } SECTION("nonexisting element") { CHECK(j.contains("foo") == false); CHECK(j_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j.contains(std::string_view("foo")) == false); - CHECK(j_const.contains(std::string_view("foo")) == false); -#endif } SECTION("all types") @@ -1437,11 +1003,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } SECTION("string") @@ -1451,11 +1012,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } SECTION("object") @@ -1465,11 +1021,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } SECTION("array") @@ -1479,11 +1030,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } SECTION("boolean") @@ -1493,11 +1039,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } SECTION("number (integer)") @@ -1507,11 +1048,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } SECTION("number (unsigned)") @@ -1521,11 +1057,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); - -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } SECTION("number (floating-point)") @@ -1534,10 +1065,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j const Json j_nonobject_const(Json::value_t::number_float); CHECK(j_nonobject.contains("foo") == false); CHECK(j_nonobject_const.contains("foo") == false); -#ifdef JSON_HAS_CPP_17 - CHECK(j_nonobject.contains(std::string_view("foo")) == false); - CHECK(j_nonobject_const.contains(std::string_view("foo")) == false); -#endif } } } @@ -1577,12 +1104,6 @@ TEST_CASE_TEMPLATE("element access 2", Json, nlohmann::json, nlohmann::ordered_j CHECK(is_detected::value); CHECK(is_detected::value); -#ifdef JSON_HAS_CPP_17 - CHECK(is_detected::value); - CHECK(is_detected::value); - CHECK(is_detected::value); -#endif - // the neighboring size_type overloads for array access are unaffected by the new // integral-key overloads above (at(), operator[](), and erase() take a size_type) Json arr = {10, 20, 30}; @@ -1764,33 +1285,6 @@ TEST_CASE_TEMPLATE("element access 2 (additional value() tests)", Json, nlohmann CHECK_THROWS_WITH_AS(Json().value(key, "default"), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); CHECK_THROWS_WITH_AS(Json().value(key, str), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); } - -#ifdef JSON_HAS_CPP_17 - SECTION("std::string_view key") - { - std::string_view const key = "foo"; - std::string_view const key2 = "baz"; - std::string_view const key_notfound = "bar"; - - CHECK(j.value(key, "default") == "bar"); - CHECK(j.value(key, cpstr) == "bar"); - CHECK(j.value(key, castr) == "bar"); - CHECK(j.value(key, str) == "bar"); - CHECK(j.value(key2, 0) == 42); - CHECK(j.value(key2, 47) == 42); - CHECK(j.value(key2, integer) == 42); - CHECK(j.value(key2, size) == 42); - - CHECK(j.value(key_notfound, "default") == "default"); - CHECK(j.value(key_notfound, 0) == 0); - CHECK(j.value(key_notfound, 47) == 47); - CHECK(j.value(key_notfound, integer) == integer); - CHECK(j.value(key_notfound, size) == size); - - CHECK_THROWS_WITH_AS(Json().value(key, "default"), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); - CHECK_THROWS_WITH_AS(Json().value(key, str), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); - } -#endif } SECTION("explicit ValueType") @@ -1902,211 +1396,6 @@ TEST_CASE_TEMPLATE("element access 2 (additional value() tests)", Json, nlohmann CHECK_THROWS_WITH_AS(Json().template value(key, "default"), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); CHECK_THROWS_WITH_AS(Json().template value(key, str), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); } - -#ifdef JSON_HAS_CPP_17 - SECTION("std::string_view key") - { - std::string_view const key = "foo"; - std::string_view const key2 = "baz"; - std::string_view const key_notfound = "bar"; - - CHECK(j.template value(key, "default") == "bar"); - CHECK(j.template value(key, cpstr) == "bar"); - CHECK(j.template value(key, castr) == "bar"); - CHECK(j.template value(key, str) == "bar"); - CHECK(j.template value(key2, 0) == 42); - CHECK(j.template value(key2, 47) == 42); - CHECK(j.template value(key2, integer) == 42); - CHECK(j.template value(key2, 0) == 42); - CHECK(j.template value(key2, 47) == 42); - CHECK(j.template value(key2, size) == 42); - - CHECK(j.template value(key_notfound, "default") == "default"); - CHECK(j.template value(key_notfound, 0) == 0); - CHECK(j.template value(key_notfound, 47) == 47); - CHECK(j.template value(key_notfound, integer) == integer); - CHECK(j.template value(key_notfound, 0) == 0); - CHECK(j.template value(key_notfound, 47) == 47); - CHECK(j.template value(key_notfound, size) == size); - - CHECK(j.template value(key, "default") == "bar"); - CHECK(j.template value(key, cpstr) == "bar"); - CHECK(j.template value(key, castr) == "bar"); - CHECK(j.template value(key, str) == "bar"); - - CHECK(j.template value(key_notfound, "default") == "default"); - - CHECK_THROWS_WITH_AS(Json().template value(key, "default"), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); - CHECK_THROWS_WITH_AS(Json().template value(key, str), "[json.exception.type_error.306] cannot use value() with null", typename Json::type_error&); - } -#endif } } -#ifdef JSON_HAS_CPP_17 -TEST_CASE("operator[] with user-defined std::string_view-convertible types") -{ - using json = nlohmann::json; - - class TestClass - { - std::string key_data_ = "foo"; - - public: - operator std::string_view() const - { - return key_data_; - } - }; - - struct TestStruct - { - operator std::string_view() const - { - return "bar"; - } - }; - - json j = {{"foo", "from_class"}, {"bar", "from_struct"}}; - const TestClass foo_obj; - const TestStruct bar_obj; - - SECTION("read access") - { - CHECK(j[foo_obj] == "from_class"); - CHECK(j[TestClass{}] == "from_class"); - CHECK(j[bar_obj] == "from_struct"); - CHECK(j[TestStruct{}] == "from_struct"); - } - - SECTION("write access") - { - j[TestClass{}] = "updated_class"; - j[TestStruct{}] = "updated_struct"; - CHECK(j["foo"] == "updated_class"); - CHECK(j["bar"] == "updated_struct"); - - SECTION("direct std::string_view access") - { - CHECK(j[std::string_view{"foo"}] == "updated_class"); - CHECK(j[std::string_view{"bar"}] == "updated_struct"); - } - } -} - -TEST_CASE("keys convertible to std::string_view work with all lookup functions (regression test for #5663)") -{ - // a key type convertible only to std::string_view: the case #4958 added - // support for, but only the non-const operator[] compiled with it - struct ViewKey - { - operator std::string_view() const - { - return "a"; - } - }; - - // a key type convertible to both std::string and std::string_view: with - // 3.12.0, such a key worked with at, the const operator[], find, count and - // contains via the conversion to std::string; #4958 made the KeyType&& - // templates win overload resolution for it instead, and those then failed - // the lookups pick the conversion to std::string_view, which leaves the one - // to std::string unused; it has to exist to reproduce the ambiguity - DOCTEST_CLANG_SUPPRESS_WARNING_PUSH - DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") - struct DualKey - { - operator std::string() const - { - return "a"; - } - operator std::string_view() const - { - return "a"; - } - }; - DOCTEST_CLANG_SUPPRESS_WARNING_POP - - SECTION("nlohmann::json") - { - using json = nlohmann::json; - - SECTION("ViewKey") - { - json j = {{"a", 1}}; - const json& cj = j; - - CHECK(j[ViewKey{}] == 1); - CHECK(cj[ViewKey{}] == 1); - CHECK(j.at(ViewKey{}) == 1); - CHECK(cj.at(ViewKey{}) == 1); - CHECK(j.find(ViewKey{}) != j.end()); - CHECK(cj.find(ViewKey{}) != cj.end()); - CHECK(j.count(ViewKey{}) == 1); - CHECK(j.contains(ViewKey{})); - CHECK(j.value(ViewKey{}, 0) == 1); - CHECK(j.erase(ViewKey{}) == 1); - CHECK(!j.contains("a")); - } - - SECTION("DualKey") - { - json j = {{"a", 1}}; - const json& cj = j; - - CHECK(j[DualKey{}] == 1); - CHECK(cj[DualKey{}] == 1); - CHECK(j.at(DualKey{}) == 1); - CHECK(cj.at(DualKey{}) == 1); - CHECK(j.find(DualKey{}) != j.end()); - CHECK(cj.find(DualKey{}) != cj.end()); - CHECK(j.count(DualKey{}) == 1); - CHECK(j.contains(DualKey{})); - CHECK(j.value(DualKey{}, 0) == 1); - CHECK(j.erase(DualKey{}) == 1); - CHECK(!j.contains("a")); - } - } - - SECTION("nlohmann::ordered_json") - { - using ordered_json = nlohmann::ordered_json; - - SECTION("ViewKey") - { - ordered_json j = {{"a", 1}}; - const ordered_json& cj = j; - - CHECK(j[ViewKey{}] == 1); - CHECK(cj[ViewKey{}] == 1); - CHECK(j.at(ViewKey{}) == 1); - CHECK(cj.at(ViewKey{}) == 1); - CHECK(j.find(ViewKey{}) != j.end()); - CHECK(cj.find(ViewKey{}) != cj.end()); - CHECK(j.count(ViewKey{}) == 1); - CHECK(j.contains(ViewKey{})); - CHECK(j.value(ViewKey{}, 0) == 1); - CHECK(j.erase(ViewKey{}) == 1); - CHECK(!j.contains("a")); - } - - SECTION("DualKey") - { - ordered_json j = {{"a", 1}}; - const ordered_json& cj = j; - - CHECK(j[DualKey{}] == 1); - CHECK(cj[DualKey{}] == 1); - CHECK(j.at(DualKey{}) == 1); - CHECK(cj.at(DualKey{}) == 1); - CHECK(j.find(DualKey{}) != j.end()); - CHECK(cj.find(DualKey{}) != cj.end()); - CHECK(j.count(DualKey{}) == 1); - CHECK(j.contains(DualKey{})); - CHECK(j.value(DualKey{}, 0) == 1); - CHECK(j.erase(DualKey{}) == 1); - CHECK(!j.contains("a")); - } - } -} -#endif diff --git a/tests/src/unit-enum_keyed_maps_default.cpp b/tests/src/unit-enum_keyed_maps_default.cpp index 5638b37e1..590f8c622 100644 --- a/tests/src/unit-enum_keyed_maps_default.cpp +++ b/tests/src/unit-enum_keyed_maps_default.cpp @@ -27,6 +27,9 @@ using nlohmann::json; DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") +// the types live in a namespace, because the file may be compiled together with other test files +namespace enum_keyed_maps_default +{ enum class cards {kreuz, pik, herz, karo}; // NOLINTNEXTLINE(misc-use-internal-linkage,misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive @@ -65,6 +68,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(strict_cards, {strict_cards::herz, "herz"}, {strict_cards::karo, "karo"} }) +} // namespace enum_keyed_maps_default namespace { @@ -82,6 +86,15 @@ struct enum_hash // see unit-enum_keyed_maps.cpp for JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS=1 TEST_CASE("maps with enum keys") { + // block-scope using-declarations take precedence over same-named types of other files in the same translation unit + using enum_keyed_maps_default::cards; + using enum_keyed_maps_default::strict_cards; + using enum_keyed_maps_default::TaskState; + using enum_keyed_maps_default::TS_COMPLETED; + using enum_keyed_maps_default::TS_INVALID; + using enum_keyed_maps_default::TS_RUNNING; + using enum_keyed_maps_default::TS_STOPPED; + using task_map = std::map; using task_umap = std::unordered_map; using task_gmap = std::map>; diff --git a/tests/src/unit-explicit_instantiation.cpp b/tests/src/unit-explicit_instantiation.cpp index 71132a59f..80c9593d4 100644 --- a/tests/src/unit-explicit_instantiation.cpp +++ b/tests/src/unit-explicit_instantiation.cpp @@ -6,11 +6,11 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// The regression below only showed on C++17, so build this file for every -// standard like the other regression tests: -// JSON_HAS_CPP_17 JSON_HAS_CPP_20 (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. The regression below only +// showed on C++17, and the whole file is just this explicit instantiation, so it +// cannot be split: build this file for every standard like the other regression tests: +// JSON_HAS_CPP_17 JSON_HAS_CPP_20 (do not remove) #include "doctest_compatibility.h" diff --git a/tests/src/unit-iterators2-cpp20.cpp b/tests/src/unit-iterators2-cpp20.cpp new file mode 100644 index 000000000..9c2a0d171 --- /dev/null +++ b/tests/src/unit-iterators2-cpp20.cpp @@ -0,0 +1,122 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-iterators2.cpp (iterators and +// std::ranges). It is kept in a separate translation unit so the (much larger) unit- +// iterators2.cpp is built for C++11 only and not rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#ifdef JSON_HAS_CPP_20 +#include +#include +#include +#include +#include + +#if JSON_HAS_RANGES + #include + #include +#endif + +TEST_CASE("iterators 2 (C++20)") +{ +#if JSON_HAS_RANGES + SECTION("ranges") + { + SECTION("concepts") + { + using nlohmann::detail::iteration_proxy_value; + CHECK(std::bidirectional_iterator); + CHECK(std::input_iterator>); + + CHECK(std::is_same>::value); + CHECK(std::ranges::bidirectional_range); + + using nlohmann::detail::iteration_proxy; + using items_type = decltype(std::declval().items()); + CHECK(std::is_same>::value); + CHECK(std::is_same, std::ranges::iterator_t>::value); + CHECK(std::ranges::input_range); + } + + SECTION("algorithms") + { + SECTION("copy") + { + json j{"foo", "bar"}; + auto j_copied = json::array(); + + std::ranges::copy(j, std::back_inserter(j_copied)); + + CHECK(j == j_copied); + } + + SECTION("find_if") + { + json j{1, 3, 2, 4}; + auto j_even = json::array(); + +#if JSON_USE_IMPLICIT_CONVERSIONS + auto it = std::ranges::find_if(j, [](int v) noexcept + { + return (v % 2) == 0; + }); +#else + auto it = std::ranges::find_if(j, [](const json & j) noexcept + { + int v; + j.get_to(v); + return (v % 2) == 0; + }); +#endif + + CHECK(*it == 2); + } + } + + SECTION("views") + { + SECTION("reverse") + { + json j{1, 2, 3, 4, 5}; + json j_expected{5, 4, 3, 2, 1}; + + auto reversed = j | std::views::reverse; + CHECK(reversed == j_expected); + } + + SECTION("transform") + { + json j + { + { "a_key", "a_value"}, + { "b_key", "b_value"}, + { "c_key", "c_value"}, + }; + json j_expected{"a_key", "b_key", "c_key"}; + + // NOLINTNEXTLINE(fuchsia-trailing-return) + auto transformed = j.items() | std::views::transform([](const auto & item) -> std::string_view + { + return item.key(); + }); + auto j_transformed = json::array(); + std::ranges::copy(transformed, std::back_inserter(j_transformed)); + + CHECK(j_transformed == j_expected); + } + } + } +#endif +} + +#endif diff --git a/tests/src/unit-iterators2.cpp b/tests/src/unit-iterators2.cpp index 5bc738e21..54c2fb310 100644 --- a/tests/src/unit-iterators2.cpp +++ b/tests/src/unit-iterators2.cpp @@ -6,23 +6,17 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. To avoid rebuilding this +// large file for every standard, tests that depend on the standard version (e.g., +// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +// go into a separate file unit-iterators2-cpp.cpp. This file stays C++11-only. #include "doctest_compatibility.h" #include using nlohmann::json; -#if JSON_HAS_RANGES - #include - #include -#endif - TEST_CASE("iterators 2") { SECTION("iterator comparisons") @@ -872,94 +866,4 @@ TEST_CASE("iterators 2") } } } - -#if JSON_HAS_RANGES - // JSON_HAS_CPP_20 (do not remove; see note at top of file) - SECTION("ranges") - { - SECTION("concepts") - { - using nlohmann::detail::iteration_proxy_value; - CHECK(std::bidirectional_iterator); - CHECK(std::input_iterator>); - - CHECK(std::is_same>::value); - CHECK(std::ranges::bidirectional_range); - - using nlohmann::detail::iteration_proxy; - using items_type = decltype(std::declval().items()); - CHECK(std::is_same>::value); - CHECK(std::is_same, std::ranges::iterator_t>::value); - CHECK(std::ranges::input_range); - } - - SECTION("algorithms") - { - SECTION("copy") - { - json j{"foo", "bar"}; - auto j_copied = json::array(); - - std::ranges::copy(j, std::back_inserter(j_copied)); - - CHECK(j == j_copied); - } - - SECTION("find_if") - { - json j{1, 3, 2, 4}; - auto j_even = json::array(); - -#if JSON_USE_IMPLICIT_CONVERSIONS - auto it = std::ranges::find_if(j, [](int v) noexcept - { - return (v % 2) == 0; - }); -#else - auto it = std::ranges::find_if(j, [](const json & j) noexcept - { - int v; - j.get_to(v); - return (v % 2) == 0; - }); -#endif - - CHECK(*it == 2); - } - } - - SECTION("views") - { - SECTION("reverse") - { - json j{1, 2, 3, 4, 5}; - json j_expected{5, 4, 3, 2, 1}; - - auto reversed = j | std::views::reverse; - CHECK(reversed == j_expected); - } - - SECTION("transform") - { - json j - { - { "a_key", "a_value"}, - { "b_key", "b_value"}, - { "c_key", "c_value"}, - }; - json j_expected{"a_key", "b_key", "c_key"}; - - // NOLINTNEXTLINE(fuchsia-trailing-return) - auto transformed = j.items() | std::views::transform([](const auto & item) -> std::string_view - { - return item.key(); - }); - auto j_transformed = json::array(); - std::ranges::copy(transformed, std::back_inserter(j_transformed)); - - CHECK(j_transformed == j_expected); - } - } - } -#endif } diff --git a/tests/src/unit-json_pointer-cpp20.cpp b/tests/src/unit-json_pointer-cpp20.cpp new file mode 100644 index 000000000..75d5f8c76 --- /dev/null +++ b/tests/src/unit-json_pointer-cpp20.cpp @@ -0,0 +1,145 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-json_pointer.cpp (JSON pointer comparison +// with C++20 rewritten candidates and operator<=>, and char8_t literals). It is kept in a +// separate translation unit so the (much larger) unit-json_pointer.cpp is built for C++11 +// only and not rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + +#include +using nlohmann::json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#ifdef JSON_HAS_CPP_20 +#include +#include +#include + +TEST_CASE("JSON pointers (C++20)") +{ + SECTION("equality comparison") + { + std::string ptr_string{"/foo/bar"}; + auto ptr1 = json::json_pointer(ptr_string); + auto ptr2 = json::json_pointer(ptr_string); + + CHECK(ptr1 == ptr2); + + CHECK_FALSE(ptr1 != ptr2); + +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED + const char* ptr_cpstring = "/foo/bar"; + const char ptr_castring[] = "/foo/bar"; // NOLINT(misc-const-correctness,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-avoid-c-arrays) + + CHECK(ptr1 == "/foo/bar"); + CHECK(ptr1 == ptr_cpstring); + CHECK(ptr1 == ptr_castring); + CHECK(ptr1 == ptr_string); + + CHECK("/foo/bar" == ptr1); + CHECK(ptr_cpstring == ptr1); + CHECK(ptr_castring == ptr1); + CHECK(ptr_string == ptr1); + + CHECK_FALSE(ptr1 != "/foo/bar"); + CHECK_FALSE(ptr1 != ptr_cpstring); + CHECK_FALSE(ptr1 != ptr_castring); + CHECK_FALSE(ptr1 != ptr_string); + + CHECK_FALSE("/foo/bar" != ptr1); + CHECK_FALSE(ptr_cpstring != ptr1); + CHECK_FALSE(ptr_castring != ptr1); + CHECK_FALSE(ptr_string != ptr1); + + SECTION("exceptions") + { + CHECK_THROWS_WITH_AS(ptr1 == "foo", + "[json.exception.parse_error.107] parse error at byte 1: JSON pointer must be empty or begin with '/' - was: 'foo'", json::parse_error&); + CHECK_THROWS_WITH_AS("foo" == ptr1, + "[json.exception.parse_error.107] parse error at byte 1: JSON pointer must be empty or begin with '/' - was: 'foo'", json::parse_error&); + CHECK_THROWS_WITH_AS(ptr1 == "/~~", + "[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&); + CHECK_THROWS_WITH_AS("/~~" == ptr1, + "[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&); + } +#endif + } + + SECTION("less-than comparison") + { + auto ptr1 = json::json_pointer("/foo/a"); + auto ptr2 = json::json_pointer("/foo/b"); + +#if JSON_HAS_THREE_WAY_COMPARISON + CHECK((ptr1 <=> ptr2) == std::strong_ordering::less); // *NOPAD* + CHECK(ptr2 > ptr1); +#endif + } + + SECTION("backwards compatibility and mixing") + { + json j = R"( + { + "foo": ["bar", "baz"] + } + )"_json; + + using nlohmann::ordered_json; + using json_ptr_str = nlohmann::json_pointer; + using json_ptr_j = nlohmann::json_pointer; + using json_ptr_oj = nlohmann::json_pointer; + + std::string const ptr_string{"/foo/0"}; + json_ptr_str ptr{ptr_string}; + json_ptr_j ptr_j{ptr_string}; + json_ptr_oj ptr_oj{ptr_string}; + + SECTION("equality comparison") + { + CHECK(ptr == ptr_j); + CHECK(ptr == ptr_oj); + CHECK(ptr_j == ptr); + CHECK(ptr_j == ptr_oj); + CHECK(ptr_oj == ptr_j); + CHECK(ptr_oj == ptr); + + CHECK_FALSE(ptr != ptr_j); + CHECK_FALSE(ptr != ptr_oj); + CHECK_FALSE(ptr_j != ptr); + CHECK_FALSE(ptr_j != ptr_oj); + CHECK_FALSE(ptr_oj != ptr_j); + CHECK_FALSE(ptr_oj != ptr); + } + } + +#if defined(__cpp_char8_t) + SECTION("Using _json_pointer with char8_t literals #4945") + { + const json j = R"({"a": {"b": {"c": 123}}})"_json; + const auto p1 = "/a/b/c"_json_pointer; + CHECK(j[p1] == 123); + + const auto p2 = u8"/a/b/c"_json_pointer; + CHECK(j[p2] == 123); + } +#endif +} + +#endif diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index b4ed2c9cc..d525a88ae 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -752,14 +752,13 @@ TEST_CASE("JSON pointers") CHECK(ptr.to_string() == "/object/~1"); } +#if !JSON_HAS_THREE_WAY_COMPARISON // with three-way comparison, see unit-json_pointer-cpp20.cpp SECTION("equality comparison") { std::string ptr_string{"/foo/bar"}; auto ptr1 = json::json_pointer(ptr_string); auto ptr2 = json::json_pointer(ptr_string); - // build with C++20 to test rewritten candidates - // JSON_HAS_CPP_20 CHECK(ptr1 == ptr2); @@ -802,6 +801,7 @@ TEST_CASE("JSON pointers") } #endif } +#endif SECTION("less-than comparison") { @@ -811,12 +811,6 @@ TEST_CASE("JSON pointers") CHECK(ptr1 < ptr2); CHECK_FALSE(ptr2 < ptr1); - // build with C++20 - // JSON_HAS_CPP_20 -#if JSON_HAS_THREE_WAY_COMPARISON - CHECK((ptr1 <=> ptr2) == std::strong_ordering::less); // *NOPAD* - CHECK(ptr2 > ptr1); -#endif } SECTION("usable as map key") @@ -872,10 +866,9 @@ TEST_CASE("JSON pointers") CHECK_FALSE(ptr != ptr_j); CHECK_FALSE(ptr != ptr_oj); +#if !JSON_HAS_THREE_WAY_COMPARISON // with three-way comparison, see unit-json_pointer-cpp20.cpp SECTION("equality comparison") { - // build with C++20 to test rewritten candidates - // JSON_HAS_CPP_20 CHECK(ptr == ptr_j); CHECK(ptr == ptr_oj); @@ -891,6 +884,7 @@ TEST_CASE("JSON pointers") CHECK_FALSE(ptr_oj != ptr_j); CHECK_FALSE(ptr_oj != ptr); } +#endif } SECTION("value(json_pointer, default) with ordered_json #5664") @@ -912,19 +906,6 @@ TEST_CASE("JSON pointers") CHECK(j.value(ptr_missing, 42) == 42); } - // build with C++20 - // JSON_HAS_CPP_20 -#if defined(__cpp_char8_t) - SECTION("Using _json_pointer with char8_t literals #4945") - { - const json j = R"({"a": {"b": {"c": 123}}})"_json; - const auto p1 = "/a/b/c"_json_pointer; - CHECK(j[p1] == 123); - - const auto p2 = u8"/a/b/c"_json_pointer; - CHECK(j[p2] == 123); - } -#endif } TEST_CASE("unescaping keeps a '~' that does not start an escape sequence") diff --git a/tests/src/unit-merge_patch.cpp b/tests/src/unit-merge_patch.cpp index 927eb8773..d96cd2f4f 100644 --- a/tests/src/unit-merge_patch.cpp +++ b/tests/src/unit-merge_patch.cpp @@ -46,7 +46,7 @@ void reference_merge_patch(json& target, const json& patch) // objects nested `depth` levels deep under the key "a", with members that // differ by `variant` on the way down -std::string nested_objects(const std::size_t depth, const int variant) +std::string nested_objects_merge_patch(const std::size_t depth, const int variant) { std::string text; for (std::size_t i = 0; i < depth; ++i) @@ -309,9 +309,9 @@ TEST_CASE("JSON Merge Patch on deeply nested values") for (int variant = 0; variant < 3; ++variant) { CAPTURE(variant) - const json patch = json::parse(nested_objects(depth, variant)); + const json patch = json::parse(nested_objects_merge_patch(depth, variant)); - json result = json::parse(nested_objects(depth, (variant + 1) % 3)); + json result = json::parse(nested_objects_merge_patch(depth, (variant + 1) % 3)); json expected = result; result.merge_patch(patch); reference_merge_patch(expected, patch); @@ -332,8 +332,8 @@ TEST_CASE("JSON Merge Patch on deeply nested values") // applying a patch used to recurse once per nesting level. The result // is only walked, never copied or compared, since those recurse too. const std::size_t depth = 100000; - json target = json::parse(nested_objects(depth, 0)); - target.merge_patch(json::parse(nested_objects(depth, 1))); + json target = json::parse(nested_objects_merge_patch(depth, 0)); + target.merge_patch(json::parse(nested_objects_merge_patch(depth, 1))); const json* p = ⌖ for (std::size_t i = 0; i < depth; ++i) @@ -404,7 +404,7 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)") }) { CAPTURE(depth) - json j = json::parse(nested_objects(depth, 0)); + json j = json::parse(nested_objects_merge_patch(depth, 0)); const json expected = j; j.merge_patch(j); CHECK(j == expected); diff --git a/tests/src/unit-modifiers.cpp b/tests/src/unit-modifiers.cpp index 3377eace0..81cb5dc62 100644 --- a/tests/src/unit-modifiers.cpp +++ b/tests/src/unit-modifiers.cpp @@ -35,7 +35,7 @@ void reference_update(json& target, const json& source) // objects nested `depth` levels deep under the key "a", with members that // differ by `variant` on the way down -std::string nested_objects(const std::size_t depth, const int variant) +std::string nested_objects_update(const std::size_t depth, const int variant) { std::string text; for (std::size_t i = 0; i < depth; ++i) @@ -1131,8 +1131,8 @@ TEST_CASE("update() on deeply nested values") for (int variant = 0; variant < 3; ++variant) { CAPTURE(variant) - const json source = json::parse(nested_objects(depth, variant)); - json result = json::parse(nested_objects(depth, (variant + 1) % 3)); + const json source = json::parse(nested_objects_update(depth, variant)); + json result = json::parse(nested_objects_update(depth, (variant + 1) % 3)); json expected = result; result.update(source, true); reference_update(expected, source); @@ -1146,8 +1146,8 @@ TEST_CASE("update() on deeply nested values") // merging used to recurse once per nesting level. The result is only // walked, never copied or compared, since those recurse too. const std::size_t depth = 100000; - json target = json::parse(nested_objects(depth, 0)); - target.update(json::parse(nested_objects(depth, 1)), true); + json target = json::parse(nested_objects_update(depth, 0)); + target.update(json::parse(nested_objects_update(depth, 1)), true); const json* p = ⌖ for (std::size_t i = 0; i < depth; ++i) @@ -1223,7 +1223,7 @@ TEST_CASE("update() with an argument that aliases *this (#5641)") }) { CAPTURE(depth) - json j = json::parse(nested_objects(depth, 0)); + json j = json::parse(nested_objects_update(depth, 0)); const json expected = j; j.update(j, true); CHECK(j == expected); diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index d29769738..bc49dfc0e 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -2285,7 +2285,7 @@ TEST_CASE("MessagePack Size above uint32 for object") } #ifdef JSON_TEST_BEYOND_UINT32_STRING -struct huge_string : std::string +struct msgpack_huge_string : std::string { using std::string::string; @@ -2295,31 +2295,31 @@ struct huge_string : std::string } }; -using huge_string_json = nlohmann::basic_json < - std::map, - std::vector, - huge_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - void >; +using msgpack_huge_string_json = nlohmann::basic_json < + std::map, + std::vector, + msgpack_huge_string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + nlohmann::adl_serializer, + std::vector, + void >; TEST_CASE("MessagePack Size above uint32 for string") { - const huge_string_json j = "hello"; + const msgpack_huge_string_json j = "hello"; CHECK_THROWS_WITH_AS( - huge_string_json::to_msgpack(j), + msgpack_huge_string_json::to_msgpack(j), "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", json::out_of_range&); } #endif -struct huge_binary : std::vector +struct msgpack_huge_binary : std::vector { using std::vector::vector; @@ -2329,29 +2329,29 @@ struct huge_binary : std::vector } }; -using huge_binary_json = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - huge_binary, - void >; +using msgpack_huge_binary_json = nlohmann::basic_json < + std::map, + std::vector, + std::string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + nlohmann::adl_serializer, + msgpack_huge_binary, + void >; TEST_CASE("MessagePack Size above uint32 for binary") { - huge_binary_json j = huge_binary_json::binary(huge_binary{}); + msgpack_huge_binary_json j = msgpack_huge_binary_json::binary(msgpack_huge_binary{}); j.get_binary().push_back(0x01); j.get_binary().push_back(0x02); CHECK_THROWS_WITH_AS( - huge_binary_json::to_msgpack(j), + msgpack_huge_binary_json::to_msgpack(j), "[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295", json::out_of_range&); } diff --git a/tests/src/unit-ordered_json2.cpp b/tests/src/unit-ordered_json2.cpp index 653820628..e98cb46e1 100644 --- a/tests/src/unit-ordered_json2.cpp +++ b/tests/src/unit-ordered_json2.cpp @@ -44,6 +44,8 @@ using nlohmann::ordered_json; // this type, which is presumably why the gap was never noticed. ///////////////////////////////////////////////////////////////////////////// +namespace unit_ordered_json2_detail +{ class alt_string; bool operator<(const char* op1, const alt_string& op2) noexcept; // NOLINT(misc-use-internal-linkage) void int_to_string(alt_string& target, std::size_t value); // NOLINT(misc-use-internal-linkage) @@ -217,21 +219,24 @@ void int_to_string(alt_string& target, std::size_t value) target = std::to_string(value).c_str(); } -using alt_json = nlohmann::basic_json < - std::map, - std::vector, - alt_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer >; +using ordered_json2_alt_json = nlohmann::basic_json < + std::map, + std::vector, + alt_string, + bool, + std::int64_t, + std::uint64_t, + double, + std::allocator, + nlohmann::adl_serializer >; bool operator<(const char* op1, const alt_string& op2) noexcept { return op1 < op2.str_impl; } +} // namespace unit_ordered_json2_detail + +using unit_ordered_json2_detail::ordered_json2_alt_json; namespace { @@ -262,11 +267,11 @@ ordered_json make_rich_ordered_json() return j; } -alt_json make_rich_alt_json() +ordered_json2_alt_json make_rich_alt_json() { - alt_json j; + ordered_json2_alt_json j; j["zebra"] = 1; - j["apple"] = alt_json::array({1, 2, 3}); + j["apple"] = ordered_json2_alt_json::array({1, 2, 3}); j["mango"]["z_nested"] = true; j["mango"]["a_nested"] = nullptr; j["banana"] = "some text"; @@ -339,47 +344,47 @@ TEST_CASE("ordered_json across binary formats") TEST_CASE("alt_json (custom string_t) across binary formats") { - const alt_json original = make_rich_alt_json(); + const ordered_json2_alt_json original = make_rich_alt_json(); SECTION("CBOR") { - const auto bytes = alt_json::to_cbor(original); - const auto restored = alt_json::from_cbor(bytes); + const auto bytes = ordered_json2_alt_json::to_cbor(original); + const auto restored = ordered_json2_alt_json::from_cbor(bytes); CHECK(restored == original); } SECTION("MessagePack") { - const auto bytes = alt_json::to_msgpack(original); - const auto restored = alt_json::from_msgpack(bytes); + const auto bytes = ordered_json2_alt_json::to_msgpack(original); + const auto restored = ordered_json2_alt_json::from_msgpack(bytes); CHECK(restored == original); } SECTION("UBJSON") { - const auto bytes = alt_json::to_ubjson(original); - const auto restored = alt_json::from_ubjson(bytes); + const auto bytes = ordered_json2_alt_json::to_ubjson(original); + const auto restored = ordered_json2_alt_json::from_ubjson(bytes); CHECK(restored == original); } SECTION("BON8") { - const auto bytes = alt_json::to_bon8(original); - const auto restored = alt_json::from_bon8(bytes); + const auto bytes = ordered_json2_alt_json::to_bon8(original); + const auto restored = ordered_json2_alt_json::from_bon8(bytes); CHECK(restored == original); } SECTION("BSON") { - const auto bytes = alt_json::to_bson(original); - const auto restored = alt_json::from_bson(bytes); + const auto bytes = ordered_json2_alt_json::to_bson(original); + const auto restored = ordered_json2_alt_json::from_bson(bytes); CHECK(restored == original); } SECTION("BJData") { - const auto bytes = alt_json::to_bjdata(original); - const auto restored = alt_json::from_bjdata(bytes); + const auto bytes = ordered_json2_alt_json::to_bjdata(original); + const auto restored = ordered_json2_alt_json::from_bjdata(bytes); CHECK(restored == original); } } diff --git a/tests/src/unit-ordered_map-cpp17.cpp b/tests/src/unit-ordered_map-cpp17.cpp new file mode 100644 index 000000000..4411db15a --- /dev/null +++ b/tests/src/unit-ordered_map-cpp17.cpp @@ -0,0 +1,40 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-ordered_map.cpp (ordered_map::find with +// std::string_view keys). It is kept in a separate translation unit so the (much larger) +// unit-ordered_map.cpp is built for C++11 only and not rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::ordered_map; + +#ifdef JSON_HAS_CPP_17 +#include +#include + +TEST_CASE("ordered_map (C++17)") +{ + SECTION("find") + { + ordered_map om; + om["eins"] = "one"; + om["zwei"] = "two"; + om["drei"] = "three"; + const auto com = om; + + const std::string eins("eins"); + const std::string vier("vier"); + + CHECK(om.find(std::string_view("eins")) == om.begin()); + CHECK(com.find(std::string_view("eins")) == com.begin()); + } +} + +#endif diff --git a/tests/src/unit-ordered_map.cpp b/tests/src/unit-ordered_map.cpp index 0d0f7730c..20a5f48bb 100644 --- a/tests/src/unit-ordered_map.cpp +++ b/tests/src/unit-ordered_map.cpp @@ -360,11 +360,6 @@ TEST_CASE("ordered_map") CHECK(com.find("vier") == com.end()); CHECK(com.find(std::string("vier")) == com.end()); CHECK(com.find(vier) == com.end()); - -#ifdef JSON_HAS_CPP_17 - CHECK(om.find(std::string_view("eins")) == om.begin()); - CHECK(com.find(std::string_view("eins")) == com.begin()); -#endif } SECTION("insert") diff --git a/tests/src/unit-regression2-cpp17.cpp b/tests/src/unit-regression2-cpp17.cpp new file mode 100644 index 000000000..1325a10dc --- /dev/null +++ b/tests/src/unit-regression2-cpp17.cpp @@ -0,0 +1,67 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-regression2.cpp (std::variant, std::any +// and std::optional regression tests). It is kept in a separate translation unit so the +// (much larger) unit-regression2.cpp is built for C++11 only and not rebuilt for every C++ +// standard. + +#include "doctest_compatibility.h" + +#include +using json = nlohmann::json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#ifdef JSON_HAS_CPP_17 +#include +#include +#include + +#include +#include + +#if __has_include() + #include +#elif __has_include() + #include +#endif + +///////////////////////////////////////////////////////////////////// +// for #4804 +///////////////////////////////////////////////////////////////////// +using json_4804 = nlohmann::json::with_binary_t>; + +TEST_CASE("regression tests 2 (C++17)") +{ + SECTION("issue #1292 - Serializing std::variant causes stack overflow") + { + static_assert(!std::is_constructible>::value, "unexpected value"); + } + + SECTION("issue #5066 - MSVC converts json to std::variant via the conversion operator") + { + // std::variant must not be retrievable via get<>(), because otherwise the + // implicit conversion operator becomes a candidate that MSVC picks over the variant's + // converting constructor, routing a number through the string from_json overload + static_assert(!nlohmann::detail::is_detected>::value, + "std::variant must not be retrievable via get<>()"); + + // clang before 7 cannot instantiate libstdc++'s std::variant +#if !(defined(__clang__) && __clang_major__ < 7) + // push_back, not emplace_back: #5066 needs the implicit conversion + // from json to the vector's value type + std::vector> v; + v.push_back(json(1)); // NOLINT(hicpp-use-emplace,modernize-use-emplace) + CHECK(std::get<0>(v[0]) == 1); +#endif + } +} + +#endif diff --git a/tests/src/unit-regression2-cpp20.cpp b/tests/src/unit-regression2-cpp20.cpp new file mode 100644 index 000000000..f7ad28592 --- /dev/null +++ b/tests/src/unit-regression2-cpp20.cpp @@ -0,0 +1,57 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-regression2.cpp (std::span and ranges +// regression tests). It is kept in a separate translation unit so the (much larger) unit- +// regression2.cpp is built for C++11 only and not rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using json = nlohmann::json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#ifdef JSON_HAS_CPP_20 +#include + +#if __has_include() + #include +#endif + +///////////////////////////////////////////////////////////////////// +// for #4440 +///////////////////////////////////////////////////////////////////// +#if JSON_HAS_RANGES == 1 + #include +#endif + +TEST_CASE("regression tests 2 (C++20)") +{ +#ifndef _LIBCPP_VERSION // see https://github.com/nlohmann/json/issues/4490 + // classic Intel ICC reports as includable but cannot actually compile + // std::span/std::as_bytes usage below +#if __has_include() && !defined(__ICC) && !defined(__INTEL_COMPILER) + SECTION("issue #2546 - parsing containers of std::byte") + { + const char DATA[] = R"("Hello, world!")"; // NOLINT(misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + // exclude the trailing '\0' that string-literal initialization adds to + // DATA: std::span(DATA) would span the full array extent (including + // that NUL), which is only silently accepted as end-of-input by default + // and would fail under JSON_STRICT_NUL_HANDLING + const auto s = std::as_bytes(std::span(DATA, sizeof(DATA) - 1)); + const json j = json::parse(s); + CHECK(j.dump() == "\"Hello, world!\""); + } +#endif +#endif + +} + +#endif diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index b532e75ae..db7b393ee 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -6,12 +6,11 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. To avoid rebuilding this +// large file for every standard, tests that depend on the standard version (e.g., +// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +// go into a separate file unit-regression2-cpp.cpp. This file stays C++11-only. #include "doctest_compatibility.h" @@ -49,30 +48,6 @@ using ordered_json = nlohmann::ordered_json; #include "test_utils.hpp" -#ifdef JSON_HAS_CPP_17 - #include - #include -#endif - -#ifdef JSON_HAS_CPP_17 - #if __has_include() - #include - #elif __has_include() - #include - #endif - - ///////////////////////////////////////////////////////////////////// - // for #4804 - ///////////////////////////////////////////////////////////////////// - using json_4804 = nlohmann::json::with_binary_t>; -#endif - -#ifdef JSON_HAS_CPP_20 - #if __has_include() - #include - #endif -#endif - ///////////////////////////////////////////////////////////////////// // for #4825 - explicitly instantiating basic_json must compile; this // forces instantiation of binary_writer::write_bjdata_ndarray, whose @@ -81,13 +56,6 @@ using ordered_json = nlohmann::ordered_json; ///////////////////////////////////////////////////////////////////// template class nlohmann::basic_json<>; -///////////////////////////////////////////////////////////////////// -// for #4440 -///////////////////////////////////////////////////////////////////// -#if JSON_HAS_RANGES == 1 - #include -#endif - // NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") @@ -285,18 +253,18 @@ struct adl_serializer ///////////////////////////////////////////////////////////////////// template -class my_allocator : public std::allocator +class my_allocator_2982 : public std::allocator { public: using std::allocator::allocator; - my_allocator() = default; - template my_allocator(const my_allocator& /*unused*/) { } + my_allocator_2982() = default; + template my_allocator_2982(const my_allocator_2982& /*unused*/) { } template struct rebind { - using other = my_allocator; + using other = my_allocator_2982; }; }; @@ -468,13 +436,6 @@ TEST_CASE("regression tests 2") CHECK(diffs.size() == 1); // Note the change here, was 2 } -#ifdef JSON_HAS_CPP_17 - SECTION("issue #1292 - Serializing std::variant causes stack overflow") - { - static_assert(!std::is_constructible>::value, "unexpected value"); - } -#endif - SECTION("issue #1299 - compile error in from_json converting to container " "with std::pair") { @@ -718,26 +679,6 @@ TEST_CASE("regression tests 2") CHECK(j.dump() == "{}"); } -#ifdef JSON_HAS_CPP_20 -#ifndef _LIBCPP_VERSION // see https://github.com/nlohmann/json/issues/4490 - // classic Intel ICC reports as includable but cannot actually compile - // std::span/std::as_bytes usage below -#if __has_include() && !defined(__ICC) && !defined(__INTEL_COMPILER) - SECTION("issue #2546 - parsing containers of std::byte") - { - const char DATA[] = R"("Hello, world!")"; // NOLINT(misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - // exclude the trailing '\0' that string-literal initialization adds to - // DATA: std::span(DATA) would span the full array extent (including - // that NUL), which is only silently accepted as end-of-input by default - // and would fail under JSON_STRICT_NUL_HANDLING - const auto s = std::as_bytes(std::span(DATA, sizeof(DATA) - 1)); - const json j = json::parse(s); - CHECK(j.dump() == "\"Hello, world!\""); - } -#endif -#endif -#endif - SECTION("issue #2574 - Deserialization to std::array, std::pair, and std::tuple with non-default constructable types fails") { SECTION("std::array") @@ -868,7 +809,7 @@ TEST_CASE("regression tests 2") SECTION("issue #2982 - to_{binary format} does not provide a mechanism for specifying a custom allocator for the returned type") { - std::vector> my_vector; + std::vector> my_vector; const json j = {1, 2, 3, 4}; json::to_cbor(j, my_vector); json k = json::from_cbor(my_vector); @@ -884,26 +825,6 @@ TEST_CASE("regression tests 2") CHECK(node.dump(-1, ' ', true, json::error_handler_t::keep) == "{\"test\":\"test\334\\u0005\"}"); } -#ifdef JSON_HAS_CPP_17 - SECTION("issue #5066 - MSVC converts json to std::variant via the conversion operator") - { - // std::variant must not be retrievable via get<>(), because otherwise the - // implicit conversion operator becomes a candidate that MSVC picks over the variant's - // converting constructor, routing a number through the string from_json overload - static_assert(!nlohmann::detail::is_detected>::value, - "std::variant must not be retrievable via get<>()"); - - // clang before 7 cannot instantiate libstdc++'s std::variant -#if !(defined(__clang__) && __clang_major__ < 7) - // push_back, not emplace_back: #5066 needs the implicit conversion - // from json to the vector's value type - std::vector> v; - v.push_back(json(1)); // NOLINT(hicpp-use-emplace,modernize-use-emplace) - CHECK(std::get<0>(v[0]) == 1); -#endif - } -#endif - SECTION("issue #3669 - invalid use of incomplete type with optional member and to_json") { const Issue3669Holder h{}; diff --git a/tests/src/unit-regression3-cpp17.cpp b/tests/src/unit-regression3-cpp17.cpp new file mode 100644 index 000000000..2e7bdecff --- /dev/null +++ b/tests/src/unit-regression3-cpp17.cpp @@ -0,0 +1,171 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++17-only part of unit-regression3.cpp (std::any, std::optional, +// std::variant and std::filesystem regression tests). It is kept in a separate translation +// unit so the (much larger) unit-regression3.cpp is built for C++11 only and not rebuilt +// for every C++ standard. + +#include "doctest_compatibility.h" + +// skip tests if JSON_DisableEnumSerialization=ON (#4384): std::byte is a +// scoped enum, so get() (needed below to get>() +// from a plain JSON array, not just from an already-binary value) relies on +// enum serialization being enabled +#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1) + #define SKIP_TESTS_FOR_ENUM_SERIALIZATION +#endif + +#include +using json = nlohmann::json; +using ordered_json = nlohmann::ordered_json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#ifdef JSON_HAS_CPP_17 +#include +#include +#include +#include + +#include +#include + +#if __has_include() + #include +#elif __has_include() +#endif + +///////////////////////////////////////////////////////////////////// +// for #4804 +///////////////////////////////////////////////////////////////////// +using json_4804 = nlohmann::json::with_binary_t>; + +///////////////////////////////////////////////////////////////////// +// for #4740 +///////////////////////////////////////////////////////////////////// + +struct Example_4740 +{ + std::optional host = std::nullopt; + std::optional port = std::nullopt; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Example_4740, host, port) +}; + +TEST_CASE("regression tests 3 (C++17)") +{ +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM + SECTION("issue #3070 - Version 3.10.3 breaks backward-compatibility with 3.10.2 ") + { + nlohmann::detail::std_fs::path text_path("/tmp/text.txt"); + const json j(text_path); + + const auto j_path = j.get(); + CHECK(j_path == text_path); + +#if DOCTEST_CLANG || DOCTEST_GCC >= DOCTEST_COMPILER(8, 4, 0) + // only known to work on Clang and GCC >=8.4 + CHECK_THROWS_WITH_AS(nlohmann::detail::std_fs::path(json(1)), "[json.exception.type_error.302] type must be string, but is number", json::type_error); +#endif + } +#endif + +#if JSON_USE_IMPLICIT_CONVERSIONS + SECTION("issue #3428 - Error occurred when converting nlohmann::json to std::any") + { + const json j; + const std::any a1 = j; + std::any&& a2 = j; + + CHECK(a1.type() == typeid(j)); + CHECK(a2.type() == typeid(j)); + } +#endif + + SECTION("issue #4740 - build issue with std::optional") + { + const auto t1 = Example_4740(); + const auto j1 = nlohmann::json(t1); + CHECK(j1.dump() == "{\"host\":null,\"port\":null}"); + const auto t2 = j1.get(); + CHECK(!t2.host.has_value()); + CHECK(!t2.port.has_value()); + + // improve coverage + auto t3 = Example_4740(); + t3.port = 80; + t3.host = "example.com"; + const auto j2 = nlohmann::json(t3); + CHECK(j2.dump() == "{\"host\":\"example.com\",\"port\":80}"); + const auto t4 = j2.get(); + CHECK(t4.host.has_value()); + CHECK(t4.port.has_value()); + } + + SECTION("issue #4804: from_cbor incompatible with std::vector as binary_t") + { + const std::vector data = {0x80}; + const auto decoded = json_4804::from_cbor(data); + CHECK((decoded == json_4804::array())); + } + +#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION + SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping") + { + // Test that assigning a custom BinaryType directly creates a binary value, not an array + const std::vector original{std::byte{1}, std::byte{2}, std::byte{3}}; + const json_4804 j = original; + CHECK(j.is_binary()); + CHECK(!j.is_array()); + + // Test round-tripping: extracting the binary value back as the custom container type + const auto extracted = j.get>(); + CHECK(extracted == original); + + // Test that the default json alias behavior is unchanged: std::vector -> array + const json default_json = std::vector {1, 2, 3}; + CHECK(default_json.is_array()); + CHECK(!default_json.is_binary()); + } + + SECTION("discussion #4209 - custom BinaryType extraction from parsed array") + { + // Test that extracting a custom BinaryType from a parsed JSON array still works + // (not just from a binary-typed node) + const auto j = json_4804::parse("[1,2,3]"); + CHECK(j.is_array()); + CHECK(!j.is_binary()); + + // Extracting as custom BinaryType should work from arrays + const auto extracted = j.get>(); + CHECK(extracted.size() == 3); + CHECK(extracted[0] == std::byte{1}); + CHECK(extracted[1] == std::byte{2}); + CHECK(extracted[2] == std::byte{3}); + } +#endif + + SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit") + { + const json jval{}; + auto GetValue = [](const json & valRoot) -> std::optional + { + if (valRoot.contains("default")) + { + return valRoot.at("default"); + } + return std::nullopt; + }; + auto result = GetValue(jval); + CHECK(!result.has_value()); + } + +} + +#endif diff --git a/tests/src/unit-regression3-cpp20.cpp b/tests/src/unit-regression3-cpp20.cpp new file mode 100644 index 000000000..144476cbd --- /dev/null +++ b/tests/src/unit-regression3-cpp20.cpp @@ -0,0 +1,127 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-regression3.cpp (C++20 regression tests +// (operator<=> related aggregates, ranges)). It is kept in a separate translation unit so +// the (much larger) unit-regression3.cpp is built for C++11 only and not rebuilt for every +// C++ standard. + +#include "doctest_compatibility.h" + +// skip tests if JSON_DisableEnumSerialization=ON (#4384): std::byte is a +// scoped enum, so get() (needed below to get>() +// from a plain JSON array, not just from an already-binary value) relies on +// enum serialization being enabled +#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1) + #define SKIP_TESTS_FOR_ENUM_SERIALIZATION +#endif + +#include +using json = nlohmann::json; +using ordered_json = nlohmann::ordered_json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#ifdef JSON_HAS_CPP_20 +#include + +#if __has_include() + #include +#endif + +///////////////////////////////////////////////////////////////////// +// for #4440 +///////////////////////////////////////////////////////////////////// +#if JSON_HAS_RANGES == 1 + #include +#endif + +///////////////////////////////////////////////////////////////////// +// for #3312 +///////////////////////////////////////////////////////////////////// + +struct for_3312 +{ + std::string name; +}; + +inline void from_json(const json& j, for_3312& obj) // NOLINT(misc-use-internal-linkage) +{ + j.at("name").get_to(obj.name); +} + +TEST_CASE("regression tests 3 (C++20)") +{ + SECTION("issue #3312 - Parse to custom class from unordered_json breaks on G++11.2.0 with C++20") + { + // see test for #3171 + const ordered_json j = {{"name", "class"}}; + for_3312 obj{}; + + j.get_to(obj); + + CHECK(obj.name == "class"); + } + +#if JSON_HAS_RANGES == 1 + SECTION("issue #4440 - assert when using std::views::filter and GCC 10") + { + auto noOpFilter = std::views::filter([](auto&&) noexcept + { + return true; + }); + json j = {1, 2, 3}; + auto filtered = j | noOpFilter; + CHECK(*filtered.begin() == 1); + } +#endif + +#if JSON_HAS_RANGE_VIEW_CONVERSION + SECTION("issue #4916 - constructing array from C++20 ranges view does not work") + { + std::vector nums{1, 2, 37, 42, 21}; + auto filteredNums = nums | std::views::filter([](int i) + { + return i > 10; + }); + json const j(filteredNums); + CHECK(j.type() == json::value_t::array); + CHECK(j == json({37, 42, 21})); + } +#endif + + // owning_view is not available in libstdc++ < 12 +#if JSON_HAS_RANGE_VIEW_CONVERSION && !(defined(__GLIBCXX__) && _GLIBCXX_RELEASE < 12) + SECTION("issue #4916 - constructing array from prvalue C++20 ranges view (owning_view)") + { + json const j(std::vector {1, 2, 37, 42, 21} | std::views::filter([](int i) + { + return i > 10; + })); + CHECK(j.type() == json::value_t::array); + CHECK(j == json({37, 42, 21})); + } +#endif + +#if JSON_HAS_RANGE_VIEW_CONVERSION + SECTION("issue #4916 - constructing array from C++20 transform view (prvalue elements)") + { + std::vector nums{1, 2, 3}; + auto t = nums | std::views::transform([](int i) noexcept + { + return i * 2; + }); + json const j(t); + CHECK(j.type() == json::value_t::array); + CHECK(j == json({2, 4, 6})); + } +#endif +} + +#endif diff --git a/tests/src/unit-regression3.cpp b/tests/src/unit-regression3.cpp index f8d9740fa..36ac30fbc 100644 --- a/tests/src/unit-regression3.cpp +++ b/tests/src/unit-regression3.cpp @@ -6,12 +6,11 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. To avoid rebuilding this +// large file for every standard, tests that depend on the standard version (e.g., +// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +// go into a separate file unit-regression3-cpp.cpp. This file stays C++11-only. #include "doctest_compatibility.h" @@ -48,38 +47,8 @@ using ordered_json = nlohmann::ordered_json; #include #include -#ifdef JSON_HAS_CPP_17 - #include - #include -#endif - -#ifdef JSON_HAS_CPP_17 - #if __has_include() - #include - #elif __has_include() - #endif - - ///////////////////////////////////////////////////////////////////// - // for #4804 - ///////////////////////////////////////////////////////////////////// - using json_4804 = nlohmann::json::with_binary_t>; -#endif - -#ifdef JSON_HAS_CPP_20 - #if __has_include() - #include - #endif -#endif - // the explicit instantiation for #4825 is in unit-explicit_instantiation.cpp -///////////////////////////////////////////////////////////////////// -// for #4440 -///////////////////////////////////////////////////////////////////// -#if JSON_HAS_RANGES == 1 - #include -#endif - // NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") @@ -194,22 +163,6 @@ inline void from_json(const json& j, for_3171_base& tb) // NOLINT(misc-use-inter tb._from_json(j); } -///////////////////////////////////////////////////////////////////// -// for #3312 -///////////////////////////////////////////////////////////////////// - -#ifdef JSON_HAS_CPP_20 -struct for_3312 -{ - std::string name; -}; - -inline void from_json(const json& j, for_3312& obj) // NOLINT(misc-use-internal-linkage) -{ - j.at("name").get_to(obj.name); -} -#endif - ///////////////////////////////////////////////////////////////////// // for #3204 ///////////////////////////////////////////////////////////////////// @@ -273,37 +226,8 @@ struct Example_3810 NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Example_3810, bla) // NOLINT(misc-use-internal-linkage) -///////////////////////////////////////////////////////////////////// -// for #4740 -///////////////////////////////////////////////////////////////////// - -#ifdef JSON_HAS_CPP_17 -struct Example_4740 -{ - std::optional host = std::nullopt; - std::optional port = std::nullopt; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Example_4740, host, port) -}; -#endif - TEST_CASE("regression tests 3") { -#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM - // JSON_HAS_CPP_17 (do not remove; see note at top of file) - SECTION("issue #3070 - Version 3.10.3 breaks backward-compatibility with 3.10.2 ") - { - nlohmann::detail::std_fs::path text_path("/tmp/text.txt"); - const json j(text_path); - - const auto j_path = j.get(); - CHECK(j_path == text_path); - -#if DOCTEST_CLANG || DOCTEST_GCC >= DOCTEST_COMPILER(8, 4, 0) - // only known to work on Clang and GCC >=8.4 - CHECK_THROWS_WITH_AS(nlohmann::detail::std_fs::path(json(1)), "[json.exception.type_error.302] type must be string, but is number", json::type_error); -#endif - } -#endif SECTION("issue #3077 - explicit constructor with default does not compile") { @@ -354,31 +278,6 @@ TEST_CASE("regression tests 3") CHECK(td.str == "value"); } -#ifdef JSON_HAS_CPP_20 - SECTION("issue #3312 - Parse to custom class from unordered_json breaks on G++11.2.0 with C++20") - { - // see test for #3171 - const ordered_json j = {{"name", "class"}}; - for_3312 obj{}; - - j.get_to(obj); - - CHECK(obj.name == "class"); - } -#endif - -#if defined(JSON_HAS_CPP_17) && JSON_USE_IMPLICIT_CONVERSIONS - SECTION("issue #3428 - Error occurred when converting nlohmann::json to std::any") - { - const json j; - const std::any a1 = j; - std::any&& a2 = j; - - CHECK(a1.type() == typeid(j)); - CHECK(a2.type() == typeid(j)); - } -#endif - SECTION("issue #3204 - ambiguous regression") { const for_3204_bar bar_from_foo([](for_3204_foo) noexcept {}); // NOLINT(performance-unnecessary-value-param) @@ -421,28 +320,6 @@ TEST_CASE("regression tests 3") CHECK(oj["test"].dump() == expected); } -#ifdef JSON_HAS_CPP_17 - SECTION("issue #4740 - build issue with std::optional") - { - const auto t1 = Example_4740(); - const auto j1 = nlohmann::json(t1); - CHECK(j1.dump() == "{\"host\":null,\"port\":null}"); - const auto t2 = j1.get(); - CHECK(!t2.host.has_value()); - CHECK(!t2.port.has_value()); - - // improve coverage - auto t3 = Example_4740(); - t3.port = 80; - t3.host = "example.com"; - const auto j2 = nlohmann::json(t3); - CHECK(j2.dump() == "{\"host\":\"example.com\",\"port\":80}"); - const auto t4 = j2.get(); - CHECK(t4.host.has_value()); - CHECK(t4.port.has_value()); - } -#endif - #if !defined(_MSVC_LANG) // MSVC returns garbage on invalid enum values, so this test is excluded // there. @@ -457,119 +334,7 @@ TEST_CASE("regression tests 3") } #endif -#ifdef JSON_HAS_CPP_17 - SECTION("issue #4804: from_cbor incompatible with std::vector as binary_t") - { - const std::vector data = {0x80}; - const auto decoded = json_4804::from_cbor(data); - CHECK((decoded == json_4804::array())); - } - -#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION - SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping") - { - // Test that assigning a custom BinaryType directly creates a binary value, not an array - const std::vector original{std::byte{1}, std::byte{2}, std::byte{3}}; - const json_4804 j = original; - CHECK(j.is_binary()); - CHECK(!j.is_array()); - - // Test round-tripping: extracting the binary value back as the custom container type - const auto extracted = j.get>(); - CHECK(extracted == original); - - // Test that the default json alias behavior is unchanged: std::vector -> array - const json default_json = std::vector {1, 2, 3}; - CHECK(default_json.is_array()); - CHECK(!default_json.is_binary()); - } - - SECTION("discussion #4209 - custom BinaryType extraction from parsed array") - { - // Test that extracting a custom BinaryType from a parsed JSON array still works - // (not just from a binary-typed node) - const auto j = json_4804::parse("[1,2,3]"); - CHECK(j.is_array()); - CHECK(!j.is_binary()); - - // Extracting as custom BinaryType should work from arrays - const auto extracted = j.get>(); - CHECK(extracted.size() == 3); - CHECK(extracted[0] == std::byte{1}); - CHECK(extracted[1] == std::byte{2}); - CHECK(extracted[2] == std::byte{3}); - } -#endif - - SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit") - { - const json jval{}; - auto GetValue = [](const json & valRoot) -> std::optional - { - if (valRoot.contains("default")) - { - return valRoot.at("default"); - } - return std::nullopt; - }; - auto result = GetValue(jval); - CHECK(!result.has_value()); - } -#endif - -#if JSON_HAS_RANGES == 1 - SECTION("issue #4440 - assert when using std::views::filter and GCC 10") - { - auto noOpFilter = std::views::filter([](auto&&) noexcept - { - return true; - }); - json j = {1, 2, 3}; - auto filtered = j | noOpFilter; - CHECK(*filtered.begin() == 1); - } -#endif - -#if JSON_HAS_RANGE_VIEW_CONVERSION - SECTION("issue #4916 - constructing array from C++20 ranges view does not work") - { - std::vector nums{1, 2, 37, 42, 21}; - auto filteredNums = nums | std::views::filter([](int i) - { - return i > 10; - }); - json const j(filteredNums); - CHECK(j.type() == json::value_t::array); - CHECK(j == json({37, 42, 21})); - } -#endif - // owning_view is not available in libstdc++ < 12 -#if JSON_HAS_RANGE_VIEW_CONVERSION && !(defined(__GLIBCXX__) && _GLIBCXX_RELEASE < 12) - SECTION("issue #4916 - constructing array from prvalue C++20 ranges view (owning_view)") - { - json const j(std::vector {1, 2, 37, 42, 21} | std::views::filter([](int i) - { - return i > 10; - })); - CHECK(j.type() == json::value_t::array); - CHECK(j == json({37, 42, 21})); - } -#endif - -#if JSON_HAS_RANGE_VIEW_CONVERSION - SECTION("issue #4916 - constructing array from C++20 transform view (prvalue elements)") - { - std::vector nums{1, 2, 3}; - auto t = nums | std::views::transform([](int i) noexcept - { - return i * 2; - }); - json const j(t); - CHECK(j.type() == json::value_t::array); - CHECK(j == json({2, 4, 6})); - } -#endif } TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN as double", T, double, float) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) @@ -914,7 +679,6 @@ TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with CHECK(mixed == json({{"keep", {{"a", 1}, {"b", 2}}}, {"replace", {{"x", 2}}}})); } - TEST_CASE("regression test #5476 - array type without reserve()") { // the capacity reserved for definite-length arrays must not require the diff --git a/tests/src/unit-std-format.cpp b/tests/src/unit-std-format.cpp index f2be8d5ec..080668aa7 100644 --- a/tests/src/unit-std-format.cpp +++ b/tests/src/unit-std-format.cpp @@ -6,12 +6,10 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. This whole file tests +// std::format (C++20), so it cannot be split: it is built for C++20 as well: +// JSON_HAS_CPP_20 (do not remove) #include "doctest_compatibility.h" @@ -19,7 +17,6 @@ using json = nlohmann::json; using ordered_json = nlohmann::ordered_json; -// JSON_HAS_CPP_20 (do not remove; see note at top of file) #if JSON_HAS_STD_FORMAT #include diff --git a/tests/src/unit-user_defined_input-cpp20.cpp b/tests/src/unit-user_defined_input-cpp20.cpp new file mode 100644 index 000000000..4e60ee8b9 --- /dev/null +++ b/tests/src/unit-user_defined_input-cpp20.cpp @@ -0,0 +1,219 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +// This file contains the C++20-only part of unit-user_defined_input.cpp (parsing with +// std::counted_iterator and std::default_sentinel_t). It is kept in a separate translation +// unit so the (much larger) unit-user_defined_input.cpp is built for C++11 only and not +// rebuilt for every C++ standard. + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#ifdef JSON_HAS_CPP_20 +#include +#include +#include + +#if defined(__cpp_lib_concepts) + #include +#endif + +#if defined(__cpp_lib_concepts) +TEST_CASE("Parse with std::counted_iterator and std::default_sentinel_t") +{ + using iterator_type = std::string::const_iterator; + const std::string json_str = R"({"key":"value","array":[1,2,3]})"; + const auto len = static_cast>(json_str.size()); + + const std::counted_iterator first(json_str.begin(), len); + const json j = json::parse(first, std::default_sentinel); + CHECK(j["key"] == "value"); + CHECK(j["array"].size() == 3); + + const std::counted_iterator first2(json_str.begin(), len); + CHECK(json::accept(first2, std::default_sentinel)); +} + +TEST_CASE("std::counted_iterator reaches the contiguous fast paths") +{ + // A sized sentinel makes the remaining element count computable in O(1), so + // std::counted_iterator over a contiguous iterator must reach the same bulk + // string/number scanners as a plain pointer - not just the byte-at-a-time + // fallback (see #5268 for the equivalent memcpy fast path). +#if JSON_HAS_RANGES + // JSON_HAS_RANGES is 0 on standard libraries with an incomplete + // (libstdc++ < 11, libc++ < 16), where the adapter deliberately falls back + // to the byte-at-a-time scanner; everything below still has to work there. + using adapter_type = nlohmann::detail::iterator_input_adapter, std::default_sentinel_t>; + CHECK(adapter_type::supports_bulk_scan); + CHECK(adapter_type::supports_seek); +#endif + + // exercise every fast path: long ASCII run, multibyte UTF-8, escapes, and + // integer/floating-point numbers + const std::string json_str = + R"({"ascii":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",)" + "\"utf8\":\"\xe4\xb8\xad\xe6\x96\x87\xf0\x9f\x98\x80\xc3\xa9\"," + R"("escaped":"aéb\n\\","ints":[0,-1,18446744073709551615,-9223372036854775808],)" + R"("floats":[1.5,-2.25e3,0.30000000000000004]})"; + const auto len = static_cast>(json_str.size()); + + const std::counted_iterator first(json_str.data(), len); + const json j = json::parse(first, std::default_sentinel); + + // parsing through the pointer adapter must give exactly the same result + CHECK(j == json::parse(json_str)); + +#if !defined(JSON_NOEXCEPTION) + // Diagnostics that quote the offending token are reconstructed from the + // already-consumed input (supports_seek), a path a sized sentinel only + // reaches now; check a few that include the "last read" text. Parsing + // invalid input aborts when exceptions are off, hence the guard. + // Raw strings and explicit bytes: an escaped literal and two literals + // written next to each other both read as mistakes to static analysis. + const auto byte = [](int value) + { + return std::string(1, static_cast(value)); + }; + const std::vector diagnostic_docs = + { + "1\nx", + "truX", + "[tru]", + R"("abc)", + R"(["\ud834"])", + R"(["a)" + byte(0x01) + R"(b"])", + R"([")" + byte(0xC3) + byte(0x28) + R"("])", + "[1e]", + R"(["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX)" + }; + + for (const auto& text : diagnostic_docs) + { + CAPTURE(text) + const std::counted_iterator it(text.data(), static_cast>(text.size())); + std::string counted_message; + std::string string_message; + try + { + const json counted_result = json::parse(it, std::default_sentinel); + static_cast(counted_result); + } + catch (const json::parse_error& e) + { + counted_message = e.what(); + } + try + { + const json string_result = json::parse(text); + static_cast(string_result); + } + catch (const json::parse_error& e) + { + string_message = e.what(); + } + CHECK_FALSE(counted_message.empty()); + CHECK(counted_message == string_message); + } + + // and errors must still be reported identically + const std::string bad = "[01\n]"; + const std::counted_iterator bad_first(bad.data(), static_cast>(bad.size())); + std::string counted_what; + std::string string_what; + try + { + const json counted_result = json::parse(bad_first, std::default_sentinel); + static_cast(counted_result); + } + catch (const json::parse_error& e) + { + counted_what = e.what(); + } + try + { + const json string_result = json::parse(bad); + static_cast(string_result); + } + catch (const json::parse_error& e) + { + string_what = e.what(); + } + CHECK_FALSE(counted_what.empty()); + CHECK(counted_what == string_what); +#endif +} + +#if !defined(JSON_NOEXCEPTION) +// several cases below are truncated on purpose, and parsing invalid input +// aborts when exceptions are off +TEST_CASE("std::counted_iterator bulk scanning stops at the counted end") +{ + // The count, not the size of the underlying buffer, is the end of the + // input: the bulk scanners must never look at the bytes behind it, even + // though they are readable. Each case is compared against parsing the + // equivalent prefix as a std::string. + const auto via_counted = [](const std::string & buf, std::size_t n) -> std::string + { + const std::counted_iterator first(buf.data(), static_cast>(n)); + try + { + const json j = json::parse(first, std::default_sentinel); + return "OK|" + j.dump(); + } + catch (const json::parse_error& e) + { + return {e.what()}; + } + }; + const auto via_prefix = [](const std::string & buf, std::size_t n) -> std::string + { + try + { + const json j = json::parse(buf.substr(0, n)); + return "OK|" + j.dump(); + } + catch (const json::parse_error& e) + { + return {e.what()}; + } + }; + + struct testcase // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init) + { + const char* buffer; + std::size_t count; + }; + const std::vector cases = + { + {"[\"abc\"]____TRAILING____", 7}, // exact fit, tail hidden + {"[\"abcdefghijklmnop\"]____", 8}, // cut inside a string + {"[\"abc\"]____", 6}, // cut just before the closing quote + {"[12345]xxxxx", 4}, // cut inside a number + {"[123]999999", 5}, // number ends exactly at the count + {"[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]", 12}, // closing quote only behind the count + {"[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]", 19}, // cut inside an 8-byte SWAR stride + {"[\"\xe4\xb8\xad\xe6\x96\x87\"]", 5}, // cut inside a UTF-8 sequence + {"[\"\xe4\xb8\xad\xe6\x96\x87\"]____", 10}, // complete UTF-8, tail hidden + {"[1.25e3]TRAILINGDIGITS999", 7}, // number token reaches the count + }; + + for (const auto& tc : cases) + { + CAPTURE(tc.buffer) + CAPTURE(tc.count) + const std::string buffer = tc.buffer; + CHECK(via_counted(buffer, tc.count) == via_prefix(buffer, tc.count)); + } +} +#endif +#endif + +#endif diff --git a/tests/src/unit-user_defined_input.cpp b/tests/src/unit-user_defined_input.cpp index 0c5cb231e..0b1c920b7 100644 --- a/tests/src/unit-user_defined_input.cpp +++ b/tests/src/unit-user_defined_input.cpp @@ -6,12 +6,11 @@ // SPDX-FileCopyrightText: 2013-2026 Niels Lohmann // SPDX-License-Identifier: MIT -// cmake/test.cmake selects the C++ standard versions with which to build a -// unit test based on the presence of JSON_HAS_CPP_ macros. -// When using macros that are only defined for particular versions of the standard -// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding -// version macro in a comment close by, like this: -// JSON_HAS_CPP_ (do not remove; see note at top of file) +// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the +// source file mentions the corresponding version macro. To avoid rebuilding this +// large file for every standard, tests that depend on the standard version (e.g., +// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON) +// go into a separate file unit-user_defined_input-cpp.cpp. This file stays C++11-only. #include "doctest_compatibility.h" @@ -25,10 +24,6 @@ using nlohmann::json; #include // string #include // vector -#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) - #include -#endif - namespace { TEST_CASE("Use arbitrary stdlib container") @@ -277,196 +272,4 @@ TEST_CASE("Contiguous byte containers take the pointer adapter") CHECK(json::parse(buffer) == json({1, 2, 3, 4, 5})); } -#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) -// JSON_HAS_CPP_20 (do not remove; see note at top of file) -TEST_CASE("Parse with std::counted_iterator and std::default_sentinel_t") -{ - using iterator_type = std::string::const_iterator; - const std::string json_str = R"({"key":"value","array":[1,2,3]})"; - const auto len = static_cast>(json_str.size()); - - const std::counted_iterator first(json_str.begin(), len); - const json j = json::parse(first, std::default_sentinel); - CHECK(j["key"] == "value"); - CHECK(j["array"].size() == 3); - - const std::counted_iterator first2(json_str.begin(), len); - CHECK(json::accept(first2, std::default_sentinel)); -} - -TEST_CASE("std::counted_iterator reaches the contiguous fast paths") -{ - // A sized sentinel makes the remaining element count computable in O(1), so - // std::counted_iterator over a contiguous iterator must reach the same bulk - // string/number scanners as a plain pointer - not just the byte-at-a-time - // fallback (see #5268 for the equivalent memcpy fast path). -#if JSON_HAS_RANGES - // JSON_HAS_RANGES is 0 on standard libraries with an incomplete - // (libstdc++ < 11, libc++ < 16), where the adapter deliberately falls back - // to the byte-at-a-time scanner; everything below still has to work there. - using adapter_type = nlohmann::detail::iterator_input_adapter, std::default_sentinel_t>; - CHECK(adapter_type::supports_bulk_scan); - CHECK(adapter_type::supports_seek); -#endif - - // exercise every fast path: long ASCII run, multibyte UTF-8, escapes, and - // integer/floating-point numbers - const std::string json_str = - R"({"ascii":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",)" - "\"utf8\":\"\xe4\xb8\xad\xe6\x96\x87\xf0\x9f\x98\x80\xc3\xa9\"," - R"("escaped":"aéb\n\\","ints":[0,-1,18446744073709551615,-9223372036854775808],)" - R"("floats":[1.5,-2.25e3,0.30000000000000004]})"; - const auto len = static_cast>(json_str.size()); - - const std::counted_iterator first(json_str.data(), len); - const json j = json::parse(first, std::default_sentinel); - - // parsing through the pointer adapter must give exactly the same result - CHECK(j == json::parse(json_str)); - -#if !defined(JSON_NOEXCEPTION) - // Diagnostics that quote the offending token are reconstructed from the - // already-consumed input (supports_seek), a path a sized sentinel only - // reaches now; check a few that include the "last read" text. Parsing - // invalid input aborts when exceptions are off, hence the guard. - // Raw strings and explicit bytes: an escaped literal and two literals - // written next to each other both read as mistakes to static analysis. - const auto byte = [](int value) - { - return std::string(1, static_cast(value)); - }; - const std::vector diagnostic_docs = - { - "1\nx", - "truX", - "[tru]", - R"("abc)", - R"(["\ud834"])", - R"(["a)" + byte(0x01) + R"(b"])", - R"([")" + byte(0xC3) + byte(0x28) + R"("])", - "[1e]", - R"(["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX)" - }; - - for (const auto& text : diagnostic_docs) - { - CAPTURE(text) - const std::counted_iterator it(text.data(), static_cast>(text.size())); - std::string counted_message; - std::string string_message; - try - { - const json counted_result = json::parse(it, std::default_sentinel); - static_cast(counted_result); - } - catch (const json::parse_error& e) - { - counted_message = e.what(); - } - try - { - const json string_result = json::parse(text); - static_cast(string_result); - } - catch (const json::parse_error& e) - { - string_message = e.what(); - } - CHECK_FALSE(counted_message.empty()); - CHECK(counted_message == string_message); - } - - // and errors must still be reported identically - const std::string bad = "[01\n]"; - const std::counted_iterator bad_first(bad.data(), static_cast>(bad.size())); - std::string counted_what; - std::string string_what; - try - { - const json counted_result = json::parse(bad_first, std::default_sentinel); - static_cast(counted_result); - } - catch (const json::parse_error& e) - { - counted_what = e.what(); - } - try - { - const json string_result = json::parse(bad); - static_cast(string_result); - } - catch (const json::parse_error& e) - { - string_what = e.what(); - } - CHECK_FALSE(counted_what.empty()); - CHECK(counted_what == string_what); -#endif -} - -#if !defined(JSON_NOEXCEPTION) -// several cases below are truncated on purpose, and parsing invalid input -// aborts when exceptions are off -TEST_CASE("std::counted_iterator bulk scanning stops at the counted end") -{ - // The count, not the size of the underlying buffer, is the end of the - // input: the bulk scanners must never look at the bytes behind it, even - // though they are readable. Each case is compared against parsing the - // equivalent prefix as a std::string. - const auto via_counted = [](const std::string & buf, std::size_t n) -> std::string - { - const std::counted_iterator first(buf.data(), static_cast>(n)); - try - { - const json j = json::parse(first, std::default_sentinel); - return "OK|" + j.dump(); - } - catch (const json::parse_error& e) - { - return {e.what()}; - } - }; - const auto via_prefix = [](const std::string & buf, std::size_t n) -> std::string - { - try - { - const json j = json::parse(buf.substr(0, n)); - return "OK|" + j.dump(); - } - catch (const json::parse_error& e) - { - return {e.what()}; - } - }; - - struct testcase // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init) - { - const char* buffer; - std::size_t count; - }; - const std::vector cases = - { - {"[\"abc\"]____TRAILING____", 7}, // exact fit, tail hidden - {"[\"abcdefghijklmnop\"]____", 8}, // cut inside a string - {"[\"abc\"]____", 6}, // cut just before the closing quote - {"[12345]xxxxx", 4}, // cut inside a number - {"[123]999999", 5}, // number ends exactly at the count - {"[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]", 12}, // closing quote only behind the count - {"[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]", 19}, // cut inside an 8-byte SWAR stride - {"[\"\xe4\xb8\xad\xe6\x96\x87\"]", 5}, // cut inside a UTF-8 sequence - {"[\"\xe4\xb8\xad\xe6\x96\x87\"]____", 10}, // complete UTF-8, tail hidden - {"[1.25e3]TRAILINGDIGITS999", 7}, // number token reaches the count - }; - - for (const auto& tc : cases) - { - CAPTURE(tc.buffer) - CAPTURE(tc.count) - const std::string buffer = tc.buffer; - CHECK(via_counted(buffer, tc.count) == via_prefix(buffer, tc.count)); - } -} -#endif -#endif - } // namespace