diff --git a/src/ifcviewer-wgpu/CMakeLists.txt b/src/ifcviewer-wgpu/CMakeLists.txt index 899fac90f3..3c21817fe0 100644 --- a/src/ifcviewer-wgpu/CMakeLists.txt +++ b/src/ifcviewer-wgpu/CMakeLists.txt @@ -156,3 +156,7 @@ if(NOT WIN32) else() install(FILES "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_runtime}" DESTINATION bin) endif() + +if(BUILD_BONSAIVIEWER_TESTS) + add_subdirectory(tests) +endif() diff --git a/src/ifcviewer-wgpu/tests/CMakeLists.txt b/src/ifcviewer-wgpu/tests/CMakeLists.txt new file mode 100644 index 0000000000..65de72e9d9 --- /dev/null +++ b/src/ifcviewer-wgpu/tests/CMakeLists.txt @@ -0,0 +1,35 @@ +################################################################################ +# # +# This file is part of IfcOpenShell. # +# # +# IfcOpenShell 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.0 of the License, or # +# (at your option) any later version. # +# # +# IfcOpenShell 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 this program. If not, see . # +# # +################################################################################ + +# Tier-1 unit tests for the wgpu backend's pure-CPU state machines (selection, +# visibility). Header-only subjects — the test binaries compile against the +# class definitions directly, no link to IfcViewerWgpu needed and no +# wgpu-native runtime involvement. + +set(IFCVIEWER_WGPU_SRC ${CMAKE_CURRENT_SOURCE_DIR}/..) + +function(add_ifcviewer_wgpu_unit_test name) + add_executable(${name} ${name}.cpp) + target_include_directories(${name} PRIVATE ${IFCVIEWER_WGPU_SRC}) + target_link_libraries(${name} PRIVATE Catch2::Catch2WithMain) + catch_discover_tests(${name}) +endfunction() + +add_ifcviewer_wgpu_unit_test(test_wgpu_selection) +add_ifcviewer_wgpu_unit_test(test_wgpu_visibility) diff --git a/src/ifcviewer-wgpu/tests/test_wgpu_selection.cpp b/src/ifcviewer-wgpu/tests/test_wgpu_selection.cpp new file mode 100644 index 0000000000..a53ce0132c --- /dev/null +++ b/src/ifcviewer-wgpu/tests/test_wgpu_selection.cpp @@ -0,0 +1,256 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell 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.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell 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 this program. If not, see . * + * * + ********************************************************************************/ + +// Tier-1 coverage of WgpuSelectionState — the CPU-side selection set + active +// id used by the wgpu viewport. The class is pure stdlib (no Qt, no QObject), +// so the test exercises the state machine directly. The GPU-side flags SSBO +// is filled via fillFlagsArray; that pure-function path is also covered. + +#include "WgpuSelectionState.h" + +#include + +#include + +TEST_CASE("WgpuSelectionState starts empty with no active id", "[wgpu-selection]") { + WgpuSelectionState sel; + REQUIRE(sel.count() == 0); + REQUIRE(sel.activeId() == 0); + REQUIRE_FALSE(sel.contains(1)); + REQUIRE_FALSE(sel.dirty()); +} + +TEST_CASE("replace(id) selects a single id and makes it active", + "[wgpu-selection]") { + WgpuSelectionState sel; + + sel.replace(5); + REQUIRE(sel.count() == 1); + REQUIRE(sel.contains(5)); + REQUIRE(sel.activeId() == 5); + REQUIRE(sel.dirty()); +} + +TEST_CASE("replace(0) clears the selection", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(5); + sel.markClean(); + + sel.replace(0); + REQUIRE(sel.count() == 0); + REQUIRE(sel.activeId() == 0); + REQUIRE(sel.dirty()); +} + +TEST_CASE("add(id) appends to the set and steals active", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(1); + sel.markClean(); + + sel.add(2); + REQUIRE(sel.count() == 2); + REQUIRE(sel.contains(1)); + REQUIRE(sel.contains(2)); + // Each click should drive the properties panel to the most recently + // touched object, so active follows the last add — distinct from GL's + // addToSelection (which kept the prior active). + REQUIRE(sel.activeId() == 2); + REQUIRE(sel.dirty()); +} + +TEST_CASE("add(0) is ignored", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(1); + sel.markClean(); + + sel.add(0); + REQUIRE(sel.count() == 1); + REQUIRE(sel.activeId() == 1); + REQUIRE_FALSE(sel.dirty()); // no-op didn't flip the flag +} + +TEST_CASE("remove(non-active) keeps active", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(1); + sel.add(2); + sel.add(3); + REQUIRE(sel.activeId() == 3); + sel.markClean(); + + sel.remove(1); + REQUIRE(sel.count() == 2); + REQUIRE_FALSE(sel.contains(1)); + REQUIRE(sel.activeId() == 3); // still the most recently touched + REQUIRE(sel.dirty()); +} + +TEST_CASE("remove(active) falls back to some remaining id", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(1); + sel.add(2); + sel.add(3); + REQUIRE(sel.activeId() == 3); + + sel.remove(3); + REQUIRE(sel.count() == 2); + REQUIRE_FALSE(sel.contains(3)); + // Active falls back to *some* remaining id (implementation picks the + // unordered_set's first element; documenting non-determinism rather + // than the specific choice). + const uint32_t a = sel.activeId(); + REQUIRE((a == 1 || a == 2)); + REQUIRE(sel.contains(a)); +} + +TEST_CASE("remove(last id) clears active", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(7); + REQUIRE(sel.activeId() == 7); + + sel.remove(7); + REQUIRE(sel.count() == 0); + REQUIRE(sel.activeId() == 0); +} + +TEST_CASE("remove(non-existent) is a no-op for state, no dirty flag", + "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(1); + sel.markClean(); + + sel.remove(99); + REQUIRE(sel.count() == 1); + REQUIRE(sel.activeId() == 1); + REQUIRE_FALSE(sel.dirty()); +} + +TEST_CASE("remove(0) is ignored", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(1); + sel.markClean(); + + sel.remove(0); + REQUIRE(sel.count() == 1); + REQUIRE_FALSE(sel.dirty()); +} + +TEST_CASE("toggle adds when absent, removes when present", "[wgpu-selection]") { + WgpuSelectionState sel; + + sel.toggle(5); + REQUIRE(sel.contains(5)); + REQUIRE(sel.activeId() == 5); + + sel.toggle(6); + REQUIRE(sel.contains(6)); + REQUIRE(sel.activeId() == 6); // last add steals active + + sel.toggle(5); // remove non-active — active unchanged + REQUIRE_FALSE(sel.contains(5)); + REQUIRE(sel.activeId() == 6); + + sel.toggle(6); // remove active — fallback (set empty → 0) + REQUIRE(sel.count() == 0); + REQUIRE(sel.activeId() == 0); +} + +TEST_CASE("toggle(0) is ignored", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.markClean(); + + sel.toggle(0); + REQUIRE(sel.count() == 0); + REQUIRE_FALSE(sel.dirty()); +} + +TEST_CASE("clear empties; no-op when already empty", "[wgpu-selection]") { + WgpuSelectionState sel; + + sel.clear(); // already empty + REQUIRE_FALSE(sel.dirty()); + + sel.replace(1); + sel.markClean(); + sel.clear(); + REQUIRE(sel.count() == 0); + REQUIRE(sel.activeId() == 0); + REQUIRE(sel.dirty()); +} + +TEST_CASE("markClean clears the dirty flag", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.replace(5); + REQUIRE(sel.dirty()); + + sel.markClean(); + REQUIRE_FALSE(sel.dirty()); + + // Subsequent mutation re-arms the flag. + sel.add(6); + REQUIRE(sel.dirty()); +} + +TEST_CASE("selectionIds returns the live set", "[wgpu-selection]") { + WgpuSelectionState sel; + sel.add(1); + sel.add(2); + sel.add(3); + + const auto& ids = sel.selectionIds(); + REQUIRE(ids.size() == 3); + REQUIRE(ids.count(1) == 1); + REQUIRE(ids.count(2) == 1); + REQUIRE(ids.count(3) == 1); +} + +TEST_CASE("fillFlagsArray packs selected/active bits per object_id", + "[wgpu-selection]") { + WgpuSelectionState sel; + sel.add(1); + sel.add(3); // active is now 3 + + std::vector flags; + sel.fillFlagsArray(flags, 8); + + REQUIRE(flags.size() == 8); + REQUIRE(flags[0] == 0u); // sentinel + REQUIRE(flags[1] == 1u); // selected, not active + REQUIRE(flags[2] == 0u); + REQUIRE(flags[3] == (1u | 2u)); // selected + active + REQUIRE(flags[4] == 0u); + REQUIRE(flags[5] == 0u); + REQUIRE(flags[6] == 0u); + REQUIRE(flags[7] == 0u); +} + +TEST_CASE("fillFlagsArray drops ids past the entries cap", + "[wgpu-selection]") { + WgpuSelectionState sel; + sel.add(1); + sel.add(100); // active is 100 + + std::vector flags; + sel.fillFlagsArray(flags, 4); + + REQUIRE(flags.size() == 4); + REQUIRE(flags[1] == 1u); + // id 100 is out of range — should not write to flags[2]/[3]/etc. + REQUIRE(flags[2] == 0u); + REQUIRE(flags[3] == 0u); +} diff --git a/src/ifcviewer-wgpu/tests/test_wgpu_visibility.cpp b/src/ifcviewer-wgpu/tests/test_wgpu_visibility.cpp new file mode 100644 index 0000000000..39307ebf5a --- /dev/null +++ b/src/ifcviewer-wgpu/tests/test_wgpu_visibility.cpp @@ -0,0 +1,118 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell 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.0 of the License, or * + * (at your option) any later version. * + * * + * IfcOpenShell 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 this program. If not, see . * + * * + ********************************************************************************/ + +// Tier-1 coverage of WgpuVisibilityState — the per-element hidden-id set +// consulted in cull. The class is pure stdlib; this test exercises its +// primitives directly. Bulk hide/isolate/show-all semantics live in +// WgpuViewportWindow (which composes WgpuVisibilityState + the model +// instance lists) and would need an integration test, not a Tier-1 unit. + +#include "WgpuVisibilityState.h" + +#include + +TEST_CASE("WgpuVisibilityState starts empty", "[wgpu-visibility]") { + WgpuVisibilityState vis; + REQUIRE(vis.hiddenCount() == 0); + REQUIRE_FALSE(vis.isHidden(0)); // 0 is the "no object" sentinel + REQUIRE_FALSE(vis.isHidden(1)); + REQUIRE_FALSE(vis.isHidden(1u << 20)); +} + +TEST_CASE("hide(id) records the id; isHidden reflects it", + "[wgpu-visibility]") { + WgpuVisibilityState vis; + + vis.hide(1); + vis.hide(2); + vis.hide(3); + + REQUIRE(vis.hiddenCount() == 3); + REQUIRE(vis.isHidden(1)); + REQUIRE(vis.isHidden(2)); + REQUIRE(vis.isHidden(3)); + REQUIRE_FALSE(vis.isHidden(4)); +} + +TEST_CASE("hide is idempotent", "[wgpu-visibility]") { + WgpuVisibilityState vis; + + vis.hide(5); + vis.hide(5); + vis.hide(5); + + REQUIRE(vis.hiddenCount() == 1); + REQUIRE(vis.isHidden(5)); +} + +TEST_CASE("hide(0) is ignored", "[wgpu-visibility]") { + WgpuVisibilityState vis; + + vis.hide(0); + REQUIRE(vis.hiddenCount() == 0); + REQUIRE_FALSE(vis.isHidden(0)); +} + +TEST_CASE("show(id) removes a previously hidden id", + "[wgpu-visibility]") { + WgpuVisibilityState vis; + vis.hide(1); + vis.hide(2); + + vis.show(1); + REQUIRE(vis.hiddenCount() == 1); + REQUIRE_FALSE(vis.isHidden(1)); + REQUIRE(vis.isHidden(2)); +} + +TEST_CASE("show(non-hidden) is a no-op", "[wgpu-visibility]") { + WgpuVisibilityState vis; + vis.hide(1); + + vis.show(99); // never hidden + REQUIRE(vis.hiddenCount() == 1); + REQUIRE(vis.isHidden(1)); +} + +TEST_CASE("clear() drops every hidden id", "[wgpu-visibility]") { + WgpuVisibilityState vis; + vis.hide(1); + vis.hide(2); + vis.hide(3); + + vis.clear(); + REQUIRE(vis.hiddenCount() == 0); + REQUIRE_FALSE(vis.isHidden(1)); + REQUIRE_FALSE(vis.isHidden(2)); + REQUIRE_FALSE(vis.isHidden(3)); +} + +TEST_CASE("hiddenIds returns the live set", "[wgpu-visibility]") { + WgpuVisibilityState vis; + vis.hide(10); + vis.hide(20); + vis.hide(30); + + const auto& ids = vis.hiddenIds(); + REQUIRE(ids.size() == 3); + REQUIRE(ids.count(10) == 1); + REQUIRE(ids.count(20) == 1); + REQUIRE(ids.count(30) == 1); + REQUIRE(ids.count(40) == 0); +}