From fcc41e0efc356007c0052f658360b7717aecc7a5 Mon Sep 17 00:00:00 2001 From: Christopher Neal Date: Thu, 14 May 2026 00:20:17 -0400 Subject: [PATCH 1/4] Add SystemTests harness for map tests --- quickTest/Makefile | 13 ++++--- quickTest/SystemTests/Makefile | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 quickTest/SystemTests/Makefile diff --git a/quickTest/Makefile b/quickTest/Makefile index c1b6ecae..febd83e9 100644 --- a/quickTest/Makefile +++ b/quickTest/Makefile @@ -1,10 +1,10 @@ TEST_BASE = $(shell pwd) -.PHONY: FRC FVMModUnitTests FVMAdaptTest Containers Tools test default +.PHONY: FRC FVMModUnitTests FVMAdaptTest Containers Tools SystemTests test default -default: FVM FVMModUnitTests FVMAdaptTest Containers Tools - cat Containers/TestResults Tools/TestResults FVMModUnitTests/TestResults FVM/TestResults > TestResults - rm Containers/TestResults Tools/TestResults FVMModUnitTests/TestResults FVM/TestResults +default: FVM FVMModUnitTests FVMAdaptTest Containers Tools SystemTests + cat Containers/TestResults Tools/TestResults FVMModUnitTests/TestResults FVM/TestResults SystemTests/TestResults > TestResults + rm Containers/TestResults Tools/TestResults FVMModUnitTests/TestResults FVM/TestResults SystemTests/TestResults @grep PASS TestResults @grep FAIL TestResults || true @! grep -q FAIL TestResults @@ -26,6 +26,9 @@ Containers: FRC Tools: FRC $(MAKE) -C Tools LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) +SystemTests: FRC + $(MAKE) -C SystemTests LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) + clean: FRC rm -f TestResults $(MAKE) -C FVM LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) clean @@ -33,6 +36,7 @@ clean: FRC $(MAKE) -C Containers LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) clean $(MAKE) -C Tools LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) clean $(MAKE) -C FVMAdaptTest LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) clean + $(MAKE) -C SystemTests LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) clean distclean: rm -f TestResults @@ -41,3 +45,4 @@ distclean: $(MAKE) -C Containers LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) distclean $(MAKE) -C Tools LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) distclean $(MAKE) -C FVMAdaptTest LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) distclean + $(MAKE) -C SystemTests LOCI_BASE=$(LOCI_BASE) TEST_BASE=$(TEST_BASE) distclean diff --git a/quickTest/SystemTests/Makefile b/quickTest/SystemTests/Makefile new file mode 100644 index 00000000..9ae62fd1 --- /dev/null +++ b/quickTest/SystemTests/Makefile @@ -0,0 +1,66 @@ +############################################################################### +# +# Copyright 2008-2025, Mississippi State University +# +# This file is part of the Loci Framework. +# +# The Loci Framework is free software: you can redistribute it and/or modify +# it under the terms of the Lesser GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# The Loci Framework is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Lesser GNU General Public License for more details. +# +# You should have received a copy of the Lesser GNU General Public License +# along with the Loci Framework. If not, see +# +############################################################################### +# LOCI_BASE should be set before the Makefile +# Set TARGET to the name of your program +# Set FILES to list '.loci' files that will be compiled into your module, or + +########################################################################### +# No changes required below this line +########################################################################### + +include $(LOCI_BASE)/Loci.conf +include $(LOCI_BASE)/version.conf + +INCLUDES = -I../contrib/doctest -I$(LOCI_BASE)/include + +AUTOMATIC_FILES = $(call loci_compile_files,) +AUTOMATIC_OBJS = $(call loci_file2objs,$(AUTOMATIC_FILES)) +AUTOMATIC_TESTS= $(subst .o,.test, $(AUTOMATIC_OBJS)) +TESTS = $(AUTOMATIC_TESTS) + +TestResults: $(TESTS) + cat $(TESTS) > TestResults; rm $(TESTS) + +%.test: %.o + $(LD) -o $*.exe $^ $(LOCAL_LIBS) $(LIBS) $(LDFLAGS) + @(LD_LIBRARY_PATH=$(LD_LIBRARY_PATH) DYLD_LIBRARY_PATH=$(DYLD_LIBRARY_PATH) ./$*.exe > $*.log 2>&1 || true) + tail -n 1 $*.log > $*.log1 + echo -n SystemTests/$*":" > $@ + @if grep -q "SUCCESS!" $*.log1 ; then echo " PASSED!"; else echo " FAILED!"; fi >>$@ + rm $*.log1 $*.exe + +clean: + rm -fr *.o *.exe *.d *.log *.log1 + +# Junk files that are created while editing and running cases +JUNK = $(wildcard *~) $(wildcard crash_dump.*) core debug output $(wildcard *.o) +# ".cc" files created from .loci files +LOCI_LPP_FILES = $(LOCI_FILES:.loci=.$(LPP_I_SUFFIX)) + +distclean: clean + rm -fr TestResults $(JUNK) $(LOCI_LPP_FILES) $(DEPEND_FILES) $(TESTS) + +DEPEND_FILES=$(subst .o,.d,$(OBJS)) + +#include automatically generated dependencies +ifeq ($(filter $(MAKECMDGOALS),clean distclean ),) +-include $(DEPEND_FILES) +endif From 5b2744829f0845428172d269fc350f91b5d2b5e7 Mon Sep 17 00:00:00 2001 From: Christopher Neal Date: Thu, 14 May 2026 00:25:40 -0400 Subject: [PATCH 2/4] Add Map and multiMap SystemTests --- quickTest/SystemTests/test_map.cc | 181 ++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 quickTest/SystemTests/test_map.cc diff --git a/quickTest/SystemTests/test_map.cc b/quickTest/SystemTests/test_map.cc new file mode 100644 index 00000000..3c3123b1 --- /dev/null +++ b/quickTest/SystemTests/test_map.cc @@ -0,0 +1,181 @@ +#include + +#define DOCTEST_CONFIG_IMPLEMENT +#include + +#include +#include +#include + +using namespace Loci; + +namespace { + + entitySet set_of(std::initializer_list values) { + entitySet result; + for(std::initializer_list::const_iterator i = values.begin(); + i != values.end(); ++i) + result += *i; + return result; + } + + std::vector row_values(const multiMap &map, int row) { + return std::vector(map.begin(row), map.end(row)); + } + + void check_values(const std::vector &actual, + std::initializer_list expected) { + REQUIRE(actual.size() == expected.size()); + size_t idx = 0; + for(std::initializer_list::const_iterator i = expected.begin(); + i != expected.end(); ++i, ++idx) + CHECK(actual[idx] == *i); + } + + void check_row(const multiMap &map, int row, + std::initializer_list expected) { + check_values(row_values(map, row), expected); + } + +} // namespace + +TEST_CASE("Map image, preimage, inverse, and compose use the active domains") { + Map map; + map.allocate(interval(0, 2)); + map[0] = 10; + map[1] = 11; + map[2] = 10; + + CHECK(map.image(set_of({0, 1, 99})) == set_of({10, 11})); + + std::pair preimage = map.preimage(set_of({10})); + CHECK(preimage.first == set_of({0, 2})); + CHECK(preimage.second == set_of({0, 2})); + + multiMap inverse; + inverseMap(inverse, map, set_of({10, 11}), map.domain()); + + CHECK(inverse.domain() == set_of({10, 11})); + check_row(inverse, 10, {2, 0}); + check_row(inverse, 11, {1}); + + dMap codomain_remap; + codomain_remap[10] = 100; + codomain_remap[11] = 101; + + MapRepP(map.Rep())->compose(codomain_remap, map.domain()); + + CHECK(map[0] == 100); + CHECK(map[1] == 101); + CHECK(map[2] == 100); +} + +TEST_CASE("MapRemap moves a Map through separate domain and range remaps") { + Map map; + map.allocate(interval(0, 2)); + map[0] = 100; + map[1] = 101; + map[2] = 100; + + dMap domain_remap; + domain_remap[0] = 20; + domain_remap[1] = 21; + domain_remap[2] = 22; + + dMap range_remap; + range_remap[100] = 7; + range_remap[101] = 8; + + storeRepP remapped_rep = + MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); + Map remapped(remapped_rep); + + CHECK(remapped.domain() == set_of({20, 21, 22})); + CHECK(remapped[20] == 7); + CHECK(remapped[21] == 8); + CHECK(remapped[22] == 7); +} + +TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { + store sizes; + sizes.allocate(interval(0, 2)); + sizes[0] = 2; + sizes[1] = 1; + sizes[2] = 0; + + multiMap map(sizes); + map[0][0] = 10; + map[0][1] = 11; + map[1][0] = 10; + + CHECK(MapRepP(map.Rep())->image(interval(0, 2)) == set_of({10, 11})); + + std::pair preimage = + MapRepP(map.Rep())->preimage(set_of({10})); + CHECK(preimage.first == set_of({1, 2})); + CHECK(preimage.second == set_of({0, 1})); + + multiMap inverse; + inverseMap(inverse, map, set_of({10, 11}), map.domain()); + + CHECK(inverse.domain() == set_of({10, 11})); + check_row(inverse, 10, {1, 0}); + check_row(inverse, 11, {0}); + + dMap codomain_remap; + codomain_remap[10] = 100; + codomain_remap[11] = 101; + + MapRepP(map.Rep())->compose(codomain_remap, set_of({0, 1})); + check_row(map, 0, {100, 101}); + check_row(map, 1, {100}); + check_row(map, 2, {}); + + dMap partial_remap; + partial_remap[100] = 1000; + + MapRepP(map.Rep())->compose(partial_remap, set_of({0})); + check_row(map, 0, {1000, -1}); +} + +TEST_CASE("multiMap MapRemap preserves row sizes while remapping keys") { + store sizes; + sizes.allocate(interval(0, 2)); + sizes[0] = 2; + sizes[1] = 1; + sizes[2] = 0; + + multiMap map(sizes); + map[0][0] = 10; + map[0][1] = 11; + map[1][0] = 10; + + dMap domain_remap; + domain_remap[0] = 20; + domain_remap[1] = 21; + domain_remap[2] = 22; + + dMap range_remap; + range_remap[10] = 100; + range_remap[11] = 101; + + storeRepP remapped_rep = + MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); + multiMap remapped(remapped_rep); + + CHECK(remapped.domain() == set_of({20, 21, 22})); + check_row(remapped, 20, {100, 101}); + check_row(remapped, 21, {100}); + check_row(remapped, 22, {}); +} + +int main(int argc, char **argv) { + Loci::Init(&argc, &argv); + + doctest::Context context; + context.applyCommandLine(argc, argv); + const int result = context.run(); + + Loci::Finalize(); + return result; +} From 93bc481986cd14ee88c29bebdfd996f4c78271c6 Mon Sep 17 00:00:00 2001 From: Christopher Neal Date: Thu, 14 May 2026 01:50:28 -0400 Subject: [PATCH 3/4] Improve Map quick tests --- quickTest/SystemTests/test_map.cc | 240 +++++++++++++++++++++++++----- 1 file changed, 203 insertions(+), 37 deletions(-) diff --git a/quickTest/SystemTests/test_map.cc b/quickTest/SystemTests/test_map.cc index 3c3123b1..ef7b3f78 100644 --- a/quickTest/SystemTests/test_map.cc +++ b/quickTest/SystemTests/test_map.cc @@ -5,6 +5,8 @@ #include #include +#include +#include #include using namespace Loci; @@ -23,13 +25,26 @@ namespace { return std::vector(map.begin(row), map.end(row)); } + void check_vector_values(const std::vector &actual, + const std::vector &expected) { + REQUIRE(actual.size() == expected.size()); + for(size_t idx = 0; idx < expected.size(); ++idx) { + CAPTURE(idx); + CHECK(actual[idx] == expected[idx]); + } + } + void check_values(const std::vector &actual, std::initializer_list expected) { - REQUIRE(actual.size() == expected.size()); - size_t idx = 0; - for(std::initializer_list::const_iterator i = expected.begin(); - i != expected.end(); ++i, ++idx) - CHECK(actual[idx] == *i); + check_vector_values(actual, std::vector(expected)); + } + + void check_values_unordered(std::vector actual, + std::initializer_list expected) { + std::vector sorted_expected(expected); + std::sort(actual.begin(), actual.end()); + std::sort(sorted_expected.begin(), sorted_expected.end()); + check_vector_values(actual, sorted_expected); } void check_row(const multiMap &map, int row, @@ -37,14 +52,62 @@ namespace { check_values(row_values(map, row), expected); } + void check_row_unordered(const multiMap &map, int row, + std::initializer_list expected) { + check_values_unordered(row_values(map, row), expected); + } + + void fill_map(Map &map, + std::initializer_list > entries) { + entitySet domain; + for(std::initializer_list >::const_iterator i = + entries.begin(); i != entries.end(); ++i) + domain += i->first; + + map.allocate(domain); + for(std::initializer_list >::const_iterator i = + entries.begin(); i != entries.end(); ++i) + map[i->first] = i->second; + } + + void fill_row_sizes(store &sizes, + std::initializer_list > rows) { + entitySet domain; + for(std::initializer_list >::const_iterator i = + rows.begin(); i != rows.end(); ++i) + domain += i->first; + + sizes.allocate(domain); + for(std::initializer_list >::const_iterator i = + rows.begin(); i != rows.end(); ++i) + sizes[i->first] = i->second; + } + + void fill_sample_multimap(multiMap &map) { + store sizes; + fill_row_sizes(sizes, {{0, 2}, {1, 1}, {2, 0}}); + + // One shared fixture covers duplicate images and an intentionally empty row. + map.allocate(sizes); + map[0][0] = 10; + map[0][1] = 11; + map[1][0] = 10; + } + } // namespace +TEST_CASE("image_section returns unique mapped values") { + const int dense_values[] = {3, 5, 4, 5}; + const int sparse_values[] = {-10, 100, -10}; + + CHECK(image_section(dense_values, dense_values + 4) == set_of({3, 4, 5})); + CHECK(image_section(sparse_values, sparse_values + 3) == set_of({-10, 100})); + CHECK(image_section(dense_values, dense_values) == EMPTY); +} + TEST_CASE("Map image, preimage, inverse, and compose use the active domains") { Map map; - map.allocate(interval(0, 2)); - map[0] = 10; - map[1] = 11; - map[2] = 10; + fill_map(map, {{0, 10}, {1, 11}, {2, 10}}); CHECK(map.image(set_of({0, 1, 99})) == set_of({10, 11})); @@ -56,8 +119,8 @@ TEST_CASE("Map image, preimage, inverse, and compose use the active domains") { inverseMap(inverse, map, set_of({10, 11}), map.domain()); CHECK(inverse.domain() == set_of({10, 11})); - check_row(inverse, 10, {2, 0}); - check_row(inverse, 11, {1}); + check_row_unordered(inverse, 10, {0, 2}); + check_row_unordered(inverse, 11, {1}); dMap codomain_remap; codomain_remap[10] = 100; @@ -70,12 +133,70 @@ TEST_CASE("Map image, preimage, inverse, and compose use the active domains") { CHECK(map[2] == 100); } +TEST_CASE("Map and multiMap image handle sparse multi-interval domains") { + Map map; + fill_map(map, {{0, 10}, {2, 20}, {4, 10}, {6, 30}, {8, 20}}); + + CHECK(map.image(set_of({0, 2, 4, 6, 8})) == set_of({10, 20, 30})); + + store sizes; + fill_row_sizes(sizes, {{0, 1}, {2, 1}, {4, 1}, {6, 1}, {8, 1}}); + + multiMap row_map; + row_map.allocate(sizes); + row_map[0][0] = 10; + row_map[2][0] = 20; + row_map[4][0] = 10; + row_map[6][0] = 30; + row_map[8][0] = 20; + + CHECK(MapRepP(row_map.Rep())->image(set_of({0, 2, 4, 6, 8})) == + set_of({10, 20, 30})); +} + +TEST_CASE("inverseMap keeps requested image rows and filters preimage rows") { + Map map; + fill_map(map, {{0, 10}, {1, 10}, {2, 11}}); + + multiMap inverse; + inverseMap(inverse, map, set_of({10, 11, 12}), set_of({0, 2, 99})); + + CHECK(inverse.domain() == set_of({10, 11, 12})); + check_row_unordered(inverse, 10, {0}); + check_row_unordered(inverse, 11, {2}); + check_row_unordered(inverse, 12, {}); + + multiMap row_map; + fill_sample_multimap(row_map); + + multiMap row_inverse; + inverseMap(row_inverse, row_map, set_of({10, 11, 12}), set_of({0})); + + CHECK(row_inverse.domain() == set_of({10, 11, 12})); + check_row_unordered(row_inverse, 10, {0}); + check_row_unordered(row_inverse, 11, {0}); + check_row_unordered(row_inverse, 12, {}); +} + +TEST_CASE("Map stream round trip preserves sparse domains and values") { + Map map; + fill_map(map, {{-2, 20}, {0, 30}, {3, 20}}); + + std::stringstream stream; + stream << map; + + Map parsed; + stream >> parsed; + + CHECK(parsed.domain() == set_of({-2, 0, 3})); + CHECK(parsed[-2] == 20); + CHECK(parsed[0] == 30); + CHECK(parsed[3] == 20); +} + TEST_CASE("MapRemap moves a Map through separate domain and range remaps") { Map map; - map.allocate(interval(0, 2)); - map[0] = 100; - map[1] = 101; - map[2] = 100; + fill_map(map, {{0, 100}, {1, 101}, {2, 100}}); dMap domain_remap; domain_remap[0] = 20; @@ -96,22 +217,37 @@ TEST_CASE("MapRemap moves a Map through separate domain and range remaps") { CHECK(remapped[22] == 7); } -TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { - store sizes; - sizes.allocate(interval(0, 2)); - sizes[0] = 2; - sizes[1] = 1; - sizes[2] = 0; +TEST_CASE("MapRemap drops Map entries without a range remap") { + Map map; + fill_map(map, {{0, 100}, {1, 999}, {2, 101}}); - multiMap map(sizes); - map[0][0] = 10; - map[0][1] = 11; - map[1][0] = 10; + dMap domain_remap; + domain_remap[0] = 20; + domain_remap[1] = 21; + domain_remap[2] = 22; + + dMap range_remap; + range_remap[100] = 7; + range_remap[101] = 8; + + storeRepP remapped_rep = + MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); + Map remapped(remapped_rep); + + CHECK(remapped.domain() == set_of({20, 22})); + CHECK(remapped[20] == 7); + CHECK(remapped[22] == 8); +} + +TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { + multiMap map; + fill_sample_multimap(map); CHECK(MapRepP(map.Rep())->image(interval(0, 2)) == set_of({10, 11})); std::pair preimage = MapRepP(map.Rep())->preimage(set_of({10})); + // An empty row is vacuously contained in the codomain, but does not overlap it. CHECK(preimage.first == set_of({1, 2})); CHECK(preimage.second == set_of({0, 1})); @@ -119,8 +255,8 @@ TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { inverseMap(inverse, map, set_of({10, 11}), map.domain()); CHECK(inverse.domain() == set_of({10, 11})); - check_row(inverse, 10, {1, 0}); - check_row(inverse, 11, {0}); + check_row_unordered(inverse, 10, {0, 1}); + check_row_unordered(inverse, 11, {0}); dMap codomain_remap; codomain_remap[10] = 100; @@ -138,17 +274,25 @@ TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { check_row(map, 0, {1000, -1}); } -TEST_CASE("multiMap MapRemap preserves row sizes while remapping keys") { - store sizes; - sizes.allocate(interval(0, 2)); - sizes[0] = 2; - sizes[1] = 1; - sizes[2] = 0; +TEST_CASE("multiMap stream round trip preserves row sizes and values") { + multiMap map; + fill_sample_multimap(map); + + std::stringstream stream; + stream << map; + + multiMap parsed; + stream >> parsed; - multiMap map(sizes); - map[0][0] = 10; - map[0][1] = 11; - map[1][0] = 10; + CHECK(parsed.domain() == set_of({0, 1, 2})); + check_row(parsed, 0, {10, 11}); + check_row(parsed, 1, {10}); + check_row(parsed, 2, {}); +} + +TEST_CASE("multiMap MapRemap preserves row sizes while remapping keys") { + multiMap map; + fill_sample_multimap(map); dMap domain_remap; domain_remap[0] = 20; @@ -169,6 +313,28 @@ TEST_CASE("multiMap MapRemap preserves row sizes while remapping keys") { check_row(remapped, 22, {}); } +TEST_CASE("multiMap MapRemap marks row values missing from the range remap") { + multiMap map; + fill_sample_multimap(map); + + dMap domain_remap; + domain_remap[0] = 20; + domain_remap[1] = 21; + domain_remap[2] = 22; + + dMap range_remap; + range_remap[10] = 100; + + storeRepP remapped_rep = + MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); + multiMap remapped(remapped_rep); + + CHECK(remapped.domain() == set_of({20, 21, 22})); + check_row(remapped, 20, {100, -1}); + check_row(remapped, 21, {100}); + check_row(remapped, 22, {}); +} + int main(int argc, char **argv) { Loci::Init(&argc, &argv); From 15e06a31a3203bd45ef22f8a88594faaf915041b Mon Sep 17 00:00:00 2001 From: Christopher Neal Date: Thu, 21 May 2026 21:41:30 -0400 Subject: [PATCH 4/4] minor adjustments --- quickTest/SystemTests/test_map.cc | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/quickTest/SystemTests/test_map.cc b/quickTest/SystemTests/test_map.cc index ef7b3f78..db674baf 100644 --- a/quickTest/SystemTests/test_map.cc +++ b/quickTest/SystemTests/test_map.cc @@ -100,6 +100,8 @@ TEST_CASE("image_section returns unique mapped values") { const int dense_values[] = {3, 5, 4, 5}; const int sparse_values[] = {-10, 100, -10}; + // Map images are entity sets: duplicate mapped values collapse, sparse entity + // labels are ordinary values, and an empty section has no image. CHECK(image_section(dense_values, dense_values + 4) == set_of({3, 4, 5})); CHECK(image_section(sparse_values, sparse_values + 3) == set_of({-10, 100})); CHECK(image_section(dense_values, dense_values) == EMPTY); @@ -109,15 +111,20 @@ TEST_CASE("Map image, preimage, inverse, and compose use the active domains") { Map map; fill_map(map, {{0, 10}, {1, 11}, {2, 10}}); + // A queried image should only follow active source entities, ignoring labels + // outside the map domain. CHECK(map.image(set_of({0, 1, 99})) == set_of({10, 11})); std::pair preimage = map.preimage(set_of({10})); + // For one-to-one maps the contained and overlapping preimages match. CHECK(preimage.first == set_of({0, 2})); CHECK(preimage.second == set_of({0, 2})); multiMap inverse; inverseMap(inverse, map, set_of({10, 11}), map.domain()); + // Inverting turns range entities into rows and groups every active source + // entity that reaches that range value. CHECK(inverse.domain() == set_of({10, 11})); check_row_unordered(inverse, 10, {0, 2}); check_row_unordered(inverse, 11, {1}); @@ -126,6 +133,8 @@ TEST_CASE("Map image, preimage, inverse, and compose use the active domains") { codomain_remap[10] = 100; codomain_remap[11] = 101; + // Composition rewrites reached range values without changing the active + // source entities that define the map. MapRepP(map.Rep())->compose(codomain_remap, map.domain()); CHECK(map[0] == 100); @@ -137,6 +146,8 @@ TEST_CASE("Map and multiMap image handle sparse multi-interval domains") { Map map; fill_map(map, {{0, 10}, {2, 20}, {4, 10}, {6, 30}, {8, 20}}); + // Entity labels are not dense array positions; discontiguous domains should + // produce the same image semantics as contiguous domains. CHECK(map.image(set_of({0, 2, 4, 6, 8})) == set_of({10, 20, 30})); store sizes; @@ -161,6 +172,8 @@ TEST_CASE("inverseMap keeps requested image rows and filters preimage rows") { multiMap inverse; inverseMap(inverse, map, set_of({10, 11, 12}), set_of({0, 2, 99})); + // Requested image rows are part of the inverse topology even when no active + // source maps to them; inactive preimage rows do not contribute. CHECK(inverse.domain() == set_of({10, 11, 12})); check_row_unordered(inverse, 10, {0}); check_row_unordered(inverse, 11, {2}); @@ -172,6 +185,7 @@ TEST_CASE("inverseMap keeps requested image rows and filters preimage rows") { multiMap row_inverse; inverseMap(row_inverse, row_map, set_of({10, 11, 12}), set_of({0})); + // The same active-image rule applies to one-to-many rows. CHECK(row_inverse.domain() == set_of({10, 11, 12})); check_row_unordered(row_inverse, 10, {0}); check_row_unordered(row_inverse, 11, {0}); @@ -182,6 +196,8 @@ TEST_CASE("Map stream round trip preserves sparse domains and values") { Map map; fill_map(map, {{-2, 20}, {0, 30}, {3, 20}}); + // Serialized maps are used as data, so round trips must preserve the actual + // sparse entity domain and not just the value sequence. std::stringstream stream; stream << map; @@ -211,6 +227,8 @@ TEST_CASE("MapRemap moves a Map through separate domain and range remaps") { MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); Map remapped(remapped_rep); + // Domain and range labels are remapped independently; duplicate old range + // values should remain duplicate new range values. CHECK(remapped.domain() == set_of({20, 21, 22})); CHECK(remapped[20] == 7); CHECK(remapped[21] == 8); @@ -234,6 +252,8 @@ TEST_CASE("MapRemap drops Map entries without a range remap") { MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); Map remapped(remapped_rep); + // A scalar map row with no valid range remap is dropped rather than inventing + // a destination entity. CHECK(remapped.domain() == set_of({20, 22})); CHECK(remapped[20] == 7); CHECK(remapped[22] == 8); @@ -243,6 +263,8 @@ TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { multiMap map; fill_sample_multimap(map); + // Multi-map image follows every value in every active row and still collapses + // duplicate destinations into an entity set. CHECK(MapRepP(map.Rep())->image(interval(0, 2)) == set_of({10, 11})); std::pair preimage = @@ -262,6 +284,7 @@ TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { codomain_remap[10] = 100; codomain_remap[11] = 101; + // Composition rewrites each row value while preserving row shape. MapRepP(map.Rep())->compose(codomain_remap, set_of({0, 1})); check_row(map, 0, {100, 101}); check_row(map, 1, {100}); @@ -270,6 +293,8 @@ TEST_CASE("multiMap image, preimage, inverse, and compose handle row values") { dMap partial_remap; partial_remap[100] = 1000; + // Partial composition marks missing range remaps in-place, which is different + // from scalar MapRemap dropping invalid rows. MapRepP(map.Rep())->compose(partial_remap, set_of({0})); check_row(map, 0, {1000, -1}); } @@ -278,6 +303,7 @@ TEST_CASE("multiMap stream round trip preserves row sizes and values") { multiMap map; fill_sample_multimap(map); + // Empty rows are part of the topology and need to survive serialization. std::stringstream stream; stream << map; @@ -307,6 +333,8 @@ TEST_CASE("multiMap MapRemap preserves row sizes while remapping keys") { MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); multiMap remapped(remapped_rep); + // Remapping a one-to-many topology changes labels but keeps each row's + // contribution count. CHECK(remapped.domain() == set_of({20, 21, 22})); check_row(remapped, 20, {100, 101}); check_row(remapped, 21, {100}); @@ -329,6 +357,8 @@ TEST_CASE("multiMap MapRemap marks row values missing from the range remap") { MapRepP(map.Rep())->MapRemap(domain_remap, range_remap); multiMap remapped(remapped_rep); + // Multi-map remap preserves row shape, marking missing destinations so callers + // can still reason about the original one-to-many structure. CHECK(remapped.domain() == set_of({20, 21, 22})); check_row(remapped, 20, {100, -1}); check_row(remapped, 21, {100});