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 diff --git a/quickTest/SystemTests/test_map.cc b/quickTest/SystemTests/test_map.cc new file mode 100644 index 00000000..db674baf --- /dev/null +++ b/quickTest/SystemTests/test_map.cc @@ -0,0 +1,377 @@ +#include + +#define DOCTEST_CONFIG_IMPLEMENT +#include + +#include +#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_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) { + 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, + std::initializer_list expected) { + 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}; + + // 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); +} + +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}); + + dMap codomain_remap; + 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); + CHECK(map[1] == 101); + 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}}); + + // 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; + 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})); + + // 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}); + 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})); + + // 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}); + 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}}); + + // 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; + + 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; + fill_map(map, {{0, 100}, {1, 101}, {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); + + // 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); + CHECK(remapped[22] == 7); +} + +TEST_CASE("MapRemap drops Map entries without a range remap") { + Map map; + fill_map(map, {{0, 100}, {1, 999}, {2, 101}}); + + 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); + + // 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); +} + +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 = + 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})); + + multiMap inverse; + inverseMap(inverse, map, set_of({10, 11}), map.domain()); + + CHECK(inverse.domain() == set_of({10, 11})); + check_row_unordered(inverse, 10, {0, 1}); + check_row_unordered(inverse, 11, {0}); + + dMap codomain_remap; + 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}); + check_row(map, 2, {}); + + 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}); +} + +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; + + multiMap parsed; + stream >> parsed; + + 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; + 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); + + // 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}); + 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); + + // 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}); + 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; +}