From bf8868e3390ec638903ae44315eecae5ae27b3ed Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Fri, 8 May 2026 16:32:16 -0400 Subject: [PATCH 01/10] fix: add include for treeBoundBox in heat source model --- .../heatSourceModels/heatSourceModel/heatSourceModel.C | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/heatSourceModel/heatSourceModel.C b/applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/heatSourceModel/heatSourceModel.C index 708087e..8e81f5c 100644 --- a/applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/heatSourceModel/heatSourceModel.C +++ b/applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/heatSourceModel/heatSourceModel.C @@ -28,6 +28,7 @@ License #include "heatSourceModel.H" #include "labelVector.H" #include "hexMatcher.H" +#include "treeBoundBox.H" // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * // From 4c085aacd52e063313839c070fe5a9837848c54a Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Fri, 8 May 2026 16:57:11 -0400 Subject: [PATCH 02/10] feat: add a doctest test harness, update ci, and update docs --- .github/workflows/CI.yml | 10 +- CONTRIBUTING.md | 5 + README.md | 2 + TESTING.md | 41 ++++++++ tests/Allwmake | 11 ++ tests/run | 24 +++++ tests/segment/Make/files | 4 + tests/segment/Make/options | 11 ++ tests/segment/segmentTests.C | 29 ++++++ tests/shared/testMain.C | 6 ++ tests/vendor/doctest/LICENSE.txt | 21 ++++ tests/vendor/doctest/doctest.h | 174 +++++++++++++++++++++++++++++++ 12 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 TESTING.md create mode 100755 tests/Allwmake create mode 100755 tests/run create mode 100644 tests/segment/Make/files create mode 100644 tests/segment/Make/options create mode 100644 tests/segment/segmentTests.C create mode 100644 tests/shared/testMain.C create mode 100644 tests/vendor/doctest/LICENSE.txt create mode 100644 tests/vendor/doctest/doctest.h diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a8b0756..10dacb1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -21,18 +21,22 @@ jobs: runs-on: ubuntu-latest container: - image: docker.io/openfoam/openfoam10-paraview510 + image: microfluidica/openfoam:13 options: --user root steps: - name: Checkout AdditiveFOAM uses: actions/checkout@v2 - name: Build AdditiveFOAM run: | - . /opt/openfoam10/etc/bashrc ./Allwmake + - name: Build native tests + run: | + ./tests/Allwmake + - name: Run native tests + run: | + ./tests/run - name: Test AdditiveFOAM run: | - . /opt/openfoam10/etc/bashrc cp -r tutorials/AMB2018-02-B userCase cd userCase # FIXME: use built-in "additiveFoam" smaller case when created diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 642ff22..6bdf911 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,3 +8,8 @@ maintainers. Your pull request must work with all current AdditiveFOAM tutorial examples and be reviewed by at least one AdditiveFOAM developer. + +For local verification, build the code with `./Allwmake`, build the native +test harness with `./tests/Allwmake`, and run it with `./tests/run`. The test +workflow and instructions for adding new native tests are documented in +[TESTING.md](TESTING.md). diff --git a/README.md b/README.md index ae69d80..39434a1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ The documentation for `AdditiveFOAM` is hosted on [GitHub Pages](https://ornl.github.io/AdditiveFOAM/). +For local test commands and guidance on adding native C++ tests, see [TESTING.md](TESTING.md). + ### Repository Features | Link | Description | |-----------------------------------------------------------|------------------------------------------| diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..f2d7cd3 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,41 @@ +# Testing + +AdditiveFOAM has two test layers: + +- Native C++ unit-style tests under [`tests/`](tests), built with `wmake` and linked against the existing AdditiveFOAM/OpenFOAM libraries. +- The tutorial smoke run in GitHub Actions, which checks end-to-end integration. + +## Prerequisites + +- An OpenFOAM-13 environment must be sourced before building or running tests. +- AdditiveFOAM must be built first so the native tests can link against `libmovingBeamModels`. + +## Build And Run + +From the repository root: + +```bash +. /path/to/openfoam/etc/bashrc +./Allwmake +./tests/Allwmake +./tests/run +``` + +`./tests/Allwmake` builds the native test executables without changing the default production build path. `./tests/run` executes the complete native suite. + +## Current Coverage + +The first native executable is `additiveFoamSegmentTests`. It validates `Foam::segment` from `libmovingBeamModels` by checking: + +- default construction yields the documented zeroed point-source state +- construction from a space-delimited string populates mode, position, power, and parameter + +## Adding A New Native Test + +1. Create a new subdirectory under `tests/`. +2. Add a `Make/files` that includes `../shared/testMain.C`, your test source, and an `EXE` target name. +3. Add a `Make/options` file with the required include paths and linked AdditiveFOAM/OpenFOAM libraries. +4. Add the new directory to [`tests/Allwmake`](tests/Allwmake). +5. Add the produced executable to [`tests/run`](tests/run). + +The vendored header at [`tests/vendor/doctest/doctest.h`](tests/vendor/doctest/doctest.h) keeps the harness self-contained and avoids extra package dependencies. diff --git a/tests/Allwmake b/tests/Allwmake new file mode 100755 index 0000000..992c3bb --- /dev/null +++ b/tests/Allwmake @@ -0,0 +1,11 @@ +#!/bin/sh +cd "${0%/*}" || exit 1 + +if [ -z "${WM_PROJECT_DIR:-}" ]; then + echo "Source the OpenFOAM environment before running ./tests/Allwmake" >&2 + exit 1 +fi + +. "$WM_PROJECT_DIR/wmake/scripts/AllwmakeParseArguments" + +wmake $targetType segment diff --git a/tests/run b/tests/run new file mode 100755 index 0000000..3054a23 --- /dev/null +++ b/tests/run @@ -0,0 +1,24 @@ +#!/bin/sh +cd "${0%/*}" || exit 1 + +set -eu + +if [ -z "${FOAM_USER_APPBIN:-}" ]; then + echo "Source the OpenFOAM environment before running ./tests/run" >&2 + exit 1 +fi + +test_binaries=" +$FOAM_USER_APPBIN/additiveFoamSegmentTests +" + +for test_binary in $test_binaries +do + if [ ! -x "$test_binary" ]; then + echo "Missing test binary: $test_binary" >&2 + echo "Build the test suite first with ./tests/Allwmake" >&2 + exit 1 + fi + + "$test_binary" +done diff --git a/tests/segment/Make/files b/tests/segment/Make/files new file mode 100644 index 0000000..bd0c76e --- /dev/null +++ b/tests/segment/Make/files @@ -0,0 +1,4 @@ +../shared/testMain.C +segmentTests.C + +EXE = $(FOAM_USER_APPBIN)/additiveFoamSegmentTests diff --git a/tests/segment/Make/options b/tests/segment/Make/options new file mode 100644 index 0000000..911e830 --- /dev/null +++ b/tests/segment/Make/options @@ -0,0 +1,11 @@ +EXE_INC = \ + -I../vendor/doctest \ + -I../../applications/solvers/additiveFoam/movingHeatSource/segment \ + -I$(LIB_SRC)/OpenFOAM/lnInclude + +EXE_LIBS = \ + -L$(FOAM_USER_LIBBIN) \ + -lmovingBeamModels \ + -lOpenFOAM \ + -lfiniteVolume \ + -lmeshTools diff --git a/tests/segment/segmentTests.C b/tests/segment/segmentTests.C new file mode 100644 index 0000000..8f3c0ed --- /dev/null +++ b/tests/segment/segmentTests.C @@ -0,0 +1,29 @@ +#include + +#include "doctest.h" +#include "segment.H" + +TEST_CASE("Foam::segment default construction yields a zeroed point source") +{ + Foam::segment seg; + + CHECK_EQ(seg.mode(), Foam::scalar(1)); + CHECK_EQ(seg.position().x(), Foam::scalar(0)); + CHECK_EQ(seg.position().y(), Foam::scalar(0)); + CHECK_EQ(seg.position().z(), Foam::scalar(0)); + CHECK_EQ(seg.power(), Foam::scalar(0)); + CHECK_EQ(seg.parameter(), Foam::scalar(0)); + CHECK_EQ(seg.time(), Foam::scalar(0)); +} + +TEST_CASE("Foam::segment parses a space-delimited segment definition") +{ + Foam::segment seg(std::string("0 1 2 3 400 5")); + + CHECK_EQ(seg.mode(), Foam::scalar(0)); + CHECK_EQ(seg.position().x(), Foam::scalar(1)); + CHECK_EQ(seg.position().y(), Foam::scalar(2)); + CHECK_EQ(seg.position().z(), Foam::scalar(3)); + CHECK_EQ(seg.power(), Foam::scalar(400)); + CHECK_EQ(seg.parameter(), Foam::scalar(5)); +} diff --git a/tests/shared/testMain.C b/tests/shared/testMain.C new file mode 100644 index 0000000..9b5d602 --- /dev/null +++ b/tests/shared/testMain.C @@ -0,0 +1,6 @@ +#include "doctest.h" + +int main() +{ + return doctest::runTests(); +} diff --git a/tests/vendor/doctest/LICENSE.txt b/tests/vendor/doctest/LICENSE.txt new file mode 100644 index 0000000..5ae0eb1 --- /dev/null +++ b/tests/vendor/doctest/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2023 Viktor Kirilov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/vendor/doctest/doctest.h b/tests/vendor/doctest/doctest.h new file mode 100644 index 0000000..e4bc3b5 --- /dev/null +++ b/tests/vendor/doctest/doctest.h @@ -0,0 +1,174 @@ + +// ================================================================================================= +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2017 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying LICENSE.txt file or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/doctest/doctest +// +// ================================================================================================= + +#ifndef TESTS_VENDOR_DOCTEST_DOCTEST_H +#define TESTS_VENDOR_DOCTEST_DOCTEST_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace doctest +{ + +struct TestCase +{ + const char* name; + void (*func)(); +}; + +inline std::vector& registry() +{ + static std::vector tests; + return tests; +} + +struct Registrar +{ + Registrar(const char* name, void (*func)()) + { + registry().push_back({name, func}); + } +}; + +class AssertionFailure : public std::runtime_error +{ +public: + explicit AssertionFailure(const std::string& message) + : + std::runtime_error(message) + {} +}; + +template +[[noreturn]] inline void failEquality +( + const char* file, + int line, + const char* lhsExpr, + const char* rhsExpr, + const Left& lhs, + const Right& rhs +) +{ + std::ostringstream os; + os << file << ":" << line << ": CHECK_EQ(" << lhsExpr << ", " << rhsExpr + << ") failed with lhs=" << lhs << " rhs=" << rhs; + throw AssertionFailure(os.str()); +} + +[[noreturn]] inline void failCheck +( + const char* file, + int line, + const char* expression +) +{ + std::ostringstream os; + os << file << ":" << line << ": CHECK(" << expression << ") failed"; + throw AssertionFailure(os.str()); +} + +template +inline void checkEqual +( + const Left& lhs, + const Right& rhs, + const char* lhsExpr, + const char* rhsExpr, + const char* file, + int line +) +{ + if (!(lhs == rhs)) + { + failEquality(file, line, lhsExpr, rhsExpr, lhs, rhs); + } +} + +inline void check +( + bool condition, + const char* expression, + const char* file, + int line +) +{ + if (!condition) + { + failCheck(file, line, expression); + } +} + +inline int runTests() +{ + int failed = 0; + int passed = 0; + + for (const auto& test : registry()) + { + try + { + test.func(); + ++passed; + std::cout << "[pass] " << test.name << '\n'; + } + catch (const std::exception& err) + { + ++failed; + std::cerr << "[fail] " << test.name << '\n' + << err.what() << '\n'; + } + catch (...) + { + ++failed; + std::cerr << "[fail] " << test.name << '\n' + << "Unknown exception\n"; + } + } + + std::cout << "Executed " << (passed + failed) << " test case(s): " + << passed << " passed, " << failed << " failed\n"; + + return failed == 0 ? 0 : 1; +} + +} // namespace doctest + +#define DOCTEST_DETAIL_CONCAT_IMPL(lhs, rhs) lhs##rhs +#define DOCTEST_DETAIL_CONCAT(lhs, rhs) DOCTEST_DETAIL_CONCAT_IMPL(lhs, rhs) + +#define TEST_CASE(name) \ + static void DOCTEST_DETAIL_CONCAT(doctest_case_, __LINE__)(); \ + static doctest::Registrar DOCTEST_DETAIL_CONCAT \ + ( \ + doctest_registrar_, \ + __LINE__ \ + )(name, &DOCTEST_DETAIL_CONCAT(doctest_case_, __LINE__)); \ + static void DOCTEST_DETAIL_CONCAT(doctest_case_, __LINE__)() + +#define CHECK(expression) \ + doctest::check(static_cast(expression), #expression, __FILE__, __LINE__) + +#define CHECK_EQ(lhs, rhs) \ + doctest::checkEqual((lhs), (rhs), #lhs, #rhs, __FILE__, __LINE__) + +#endif From f9425b7cacd757ea635df774fd94115117a38cac Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Fri, 8 May 2026 17:19:23 -0400 Subject: [PATCH 03/10] add v2 candidate as target for CI --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 10dacb1..1516e7c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -6,6 +6,7 @@ on: pull_request: branches: - main + - candidate-v2.0.0 concurrency: group: ${ {github.event_name }}-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{github.event_name == 'pull_request'}} From a2a3edf45ce6311459a77efc575cd74ac2eda43b Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Mon, 11 May 2026 08:57:16 -0400 Subject: [PATCH 04/10] fix: source docker container's openfoam bashrc directly --- .github/workflows/CI.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1516e7c..51d9569 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,7 +14,7 @@ jobs: CI: defaults: run: - shell: bash -ileo pipefail {0} + shell: bash -leo pipefail {0} strategy: matrix: cxx: ['g++'] @@ -29,15 +29,19 @@ jobs: uses: actions/checkout@v2 - name: Build AdditiveFOAM run: | + . /openfoam/bash.rc ./Allwmake - name: Build native tests run: | + . /openfoam/bash.rc ./tests/Allwmake - name: Run native tests run: | + . /openfoam/bash.rc ./tests/run - name: Test AdditiveFOAM run: | + . /openfoam/bash.rc cp -r tutorials/AMB2018-02-B userCase cd userCase # FIXME: use built-in "additiveFoam" smaller case when created From 735f38ee9eee2a16439fbe7b2972255878331492 Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Mon, 11 May 2026 09:00:31 -0400 Subject: [PATCH 05/10] fix: change shell parameters --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 51d9569..778c4eb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,7 +14,7 @@ jobs: CI: defaults: run: - shell: bash -leo pipefail {0} + shell: bash -eo pipefail {0} strategy: matrix: cxx: ['g++'] From 2c1c363a451e7e0fc3dc8500cd5ecca3d5e0f82e Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Mon, 11 May 2026 09:36:11 -0400 Subject: [PATCH 06/10] fix: correct which file is sourced for activating openfoam env --- .github/workflows/CI.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 778c4eb..a8fd0b8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,19 +29,19 @@ jobs: uses: actions/checkout@v2 - name: Build AdditiveFOAM run: | - . /openfoam/bash.rc + . /openfoam/profile.rc ./Allwmake - name: Build native tests run: | - . /openfoam/bash.rc + . /openfoam/profile.rc ./tests/Allwmake - name: Run native tests run: | - . /openfoam/bash.rc + . /openfoam/profile.rc ./tests/run - name: Test AdditiveFOAM run: | - . /openfoam/bash.rc + . /openfoam/profile.rc cp -r tutorials/AMB2018-02-B userCase cd userCase # FIXME: use built-in "additiveFoam" smaller case when created From faa1fac857790cebc9f50d3d1441f2e2af72cfed Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Mon, 11 May 2026 09:44:32 -0400 Subject: [PATCH 07/10] fix: correct (again) which file is sourced for activating openfoam env --- .github/workflows/CI.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a8fd0b8..6ea16be 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,19 +29,19 @@ jobs: uses: actions/checkout@v2 - name: Build AdditiveFOAM run: | - . /openfoam/profile.rc + . /opt/openfoam13/etc/bashrc ./Allwmake - name: Build native tests run: | - . /openfoam/profile.rc + . /opt/openfoam13/etc/bashrc ./tests/Allwmake - name: Run native tests run: | - . /openfoam/profile.rc + . /opt/openfoam13/etc/bashrc ./tests/run - name: Test AdditiveFOAM run: | - . /openfoam/profile.rc + . /opt/openfoam13/etc/bashrc cp -r tutorials/AMB2018-02-B userCase cd userCase # FIXME: use built-in "additiveFoam" smaller case when created From 4b860b16de9159a26b3b887c198d4375a4cb6fa2 Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Mon, 11 May 2026 10:59:40 -0400 Subject: [PATCH 08/10] draft: ignore error on bashrc source to see if it runs on CI --- .github/workflows/CI.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6ea16be..8938d4d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,7 +14,7 @@ jobs: CI: defaults: run: - shell: bash -eo pipefail {0} + shell: bash {0} strategy: matrix: cxx: ['g++'] @@ -29,19 +29,20 @@ jobs: uses: actions/checkout@v2 - name: Build AdditiveFOAM run: | - . /opt/openfoam13/etc/bashrc + . /opt/openfoam13/etc/bashrc || true + test -n "$WM_PROJECT_DIR" ./Allwmake - name: Build native tests run: | - . /opt/openfoam13/etc/bashrc + . /opt/openfoam13/etc/bashrc || true ./tests/Allwmake - name: Run native tests run: | - . /opt/openfoam13/etc/bashrc + . /opt/openfoam13/etc/bashrc || true ./tests/run - name: Test AdditiveFOAM run: | - . /opt/openfoam13/etc/bashrc + . /opt/openfoam13/etc/bashrc || true cp -r tutorials/AMB2018-02-B userCase cd userCase # FIXME: use built-in "additiveFoam" smaller case when created From a3e57c32f5e6342536d82473d29009bbc4b51ddb Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Mon, 11 May 2026 11:33:09 -0400 Subject: [PATCH 09/10] feat: add fixtures and unit tests for heat source, scan path, and utilities --- TESTING.md | 10 +- .../additiveFoam/utilities/graph/graph.C | 6 +- tests/Allwmake | 3 + .../constant/beamPath.dat | 4 + .../constant/heatSourceDict | 164 +++++++++++++++ .../constant/skipPath.dat | 3 + .../movingHeatSourceCase/system/controlDict | 23 +++ tests/movingBeam/Make/files | 4 + tests/movingBeam/Make/options | 17 ++ tests/movingBeam/movingBeamTests.C | 100 ++++++++++ tests/movingHeatSourceModels/Make/files | 4 + tests/movingHeatSourceModels/Make/options | 23 +++ .../movingHeatSourceModelTests.C | 188 ++++++++++++++++++ tests/run | 3 + tests/shared/movingHeatSourceTestFixture.H | 111 +++++++++++ tests/utilities/Make/files | 4 + tests/utilities/Make/options | 13 ++ tests/utilities/utilityTests.C | 82 ++++++++ 18 files changed, 756 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/movingHeatSourceCase/constant/beamPath.dat create mode 100644 tests/fixtures/movingHeatSourceCase/constant/heatSourceDict create mode 100644 tests/fixtures/movingHeatSourceCase/constant/skipPath.dat create mode 100644 tests/fixtures/movingHeatSourceCase/system/controlDict create mode 100644 tests/movingBeam/Make/files create mode 100644 tests/movingBeam/Make/options create mode 100644 tests/movingBeam/movingBeamTests.C create mode 100644 tests/movingHeatSourceModels/Make/files create mode 100644 tests/movingHeatSourceModels/Make/options create mode 100644 tests/movingHeatSourceModels/movingHeatSourceModelTests.C create mode 100644 tests/shared/movingHeatSourceTestFixture.H create mode 100644 tests/utilities/Make/files create mode 100644 tests/utilities/Make/options create mode 100644 tests/utilities/utilityTests.C diff --git a/TESTING.md b/TESTING.md index f2d7cd3..4165fb6 100644 --- a/TESTING.md +++ b/TESTING.md @@ -25,10 +25,14 @@ From the repository root: ## Current Coverage -The first native executable is `additiveFoamSegmentTests`. It validates `Foam::segment` from `libmovingBeamModels` by checking: +The native suite currently builds four executables: -- default construction yields the documented zeroed point-source state -- construction from a space-delimited string populates mode, position, power, and parameter +- `additiveFoamSegmentTests` validates `Foam::segment` default construction and parsing. +- `additiveFoamMovingBeamTests` covers scan-path timing, index selection, interpolation, and timestep adjustment in `Foam::movingBeam`. +- `additiveFoamMovingHeatSourceModelTests` exercises absorption-model and heat-source-model math for the current beam model implementations. +- `additiveFoamUtilityTests` protects `interpolateXY` and the graph utilities used by solver setup and post-processing. + +The `movingBeam` and heat-source-model tests use a small file-backed fixture case under [`tests/fixtures/movingHeatSourceCase`](tests/fixtures/movingHeatSourceCase) so the constructors read real OpenFOAM dictionaries and scan-path files. ## Adding A New Native Test diff --git a/applications/solvers/additiveFoam/utilities/graph/graph.C b/applications/solvers/additiveFoam/utilities/graph/graph.C index c7e1abf..f904bc0 100644 --- a/applications/solvers/additiveFoam/utilities/graph/graph.C +++ b/applications/solvers/additiveFoam/utilities/graph/graph.C @@ -40,9 +40,9 @@ namespace Foam Foam::word Foam::graph::wordify(const Foam::string& sname) { string wname = sname; - wname.replace(' ', '_'); - wname.replace('(', '_'); - wname.replace(')', ""); + wname.replaceAll(' ', '_'); + wname.replaceAll('(', '_'); + wname.replaceAll(')', ""); return word(wname); } diff --git a/tests/Allwmake b/tests/Allwmake index 992c3bb..f0f33c6 100755 --- a/tests/Allwmake +++ b/tests/Allwmake @@ -9,3 +9,6 @@ fi . "$WM_PROJECT_DIR/wmake/scripts/AllwmakeParseArguments" wmake $targetType segment +wmake $targetType movingBeam +wmake $targetType movingHeatSourceModels +wmake $targetType utilities diff --git a/tests/fixtures/movingHeatSourceCase/constant/beamPath.dat b/tests/fixtures/movingHeatSourceCase/constant/beamPath.dat new file mode 100644 index 0000000..d1eaa5a --- /dev/null +++ b/tests/fixtures/movingHeatSourceCase/constant/beamPath.dat @@ -0,0 +1,4 @@ +mode x y z power parameter +1 1 0 0 100 1.5 +0 4 0 0 200 2.0 +1 4 0 0 50 0.5 diff --git a/tests/fixtures/movingHeatSourceCase/constant/heatSourceDict b/tests/fixtures/movingHeatSourceCase/constant/heatSourceDict new file mode 100644 index 0000000..d1eb2a7 --- /dev/null +++ b/tests/fixtures/movingHeatSourceCase/constant/heatSourceDict @@ -0,0 +1,164 @@ +FoamFile +{ + version 2.0; + format ascii; + class dictionary; + object heatSourceDict; +} + +sources +( + testBeam + skipBeam + noHitBeam + kellyConeBeam + kellyCylinderBeam + modifiedBeam + projectedBeam +); + +testBeam +{ + heatSourceModel superGaussian; + absorptionModel constant; + pathName beamPath.dat; + deltaT 0.4; + hitPathIntervals true; + + constantCoeffs + { + eta 0.35; + } + + superGaussianCoeffs + { + dimensions (2 2 4); + k 2; + } +} + +skipBeam +{ + heatSourceModel superGaussian; + absorptionModel constant; + pathName skipPath.dat; + deltaT 0.4; + hitPathIntervals true; + + constantCoeffs + { + eta 0.35; + } + + superGaussianCoeffs + { + dimensions (2 2 4); + k 2; + } +} + +noHitBeam +{ + heatSourceModel superGaussian; + absorptionModel constant; + pathName beamPath.dat; + deltaT 0.4; + hitPathIntervals false; + + constantCoeffs + { + eta 0.35; + } + + superGaussianCoeffs + { + dimensions (2 2 4); + k 2; + } +} + +kellyConeBeam +{ + heatSourceModel superGaussian; + absorptionModel Kelly; + pathName beamPath.dat; + deltaT 0.4; + hitPathIntervals true; + + KellyCoeffs + { + geometry cone; + eta0 0.45; + etaMin 0.15; + } + + superGaussianCoeffs + { + dimensions (2 2 4); + k 2; + } +} + +kellyCylinderBeam +{ + heatSourceModel superGaussian; + absorptionModel Kelly; + pathName beamPath.dat; + deltaT 0.4; + hitPathIntervals true; + + KellyCoeffs + { + geometry cylinder; + eta0 0.45; + etaMin 0.15; + } + + superGaussianCoeffs + { + dimensions (2 2 4); + k 2; + } +} + +modifiedBeam +{ + heatSourceModel modifiedSuperGaussian; + absorptionModel constant; + pathName beamPath.dat; + deltaT 0.4; + hitPathIntervals true; + + constantCoeffs + { + eta 0.35; + } + + modifiedSuperGaussianCoeffs + { + dimensions (2 2 4); + k 2; + m 2; + } +} + +projectedBeam +{ + heatSourceModel projectedGaussian; + absorptionModel constant; + pathName beamPath.dat; + deltaT 0.4; + hitPathIntervals true; + + constantCoeffs + { + eta 0.35; + } + + projectedGaussianCoeffs + { + dimensions (1 2 16); + A 3; + B 0; + } +} diff --git a/tests/fixtures/movingHeatSourceCase/constant/skipPath.dat b/tests/fixtures/movingHeatSourceCase/constant/skipPath.dat new file mode 100644 index 0000000..edbab3b --- /dev/null +++ b/tests/fixtures/movingHeatSourceCase/constant/skipPath.dat @@ -0,0 +1,3 @@ +mode x y z power parameter +1 0 0 0 100 0 +0 2 0 0 100 1 diff --git a/tests/fixtures/movingHeatSourceCase/system/controlDict b/tests/fixtures/movingHeatSourceCase/system/controlDict new file mode 100644 index 0000000..8c1b422 --- /dev/null +++ b/tests/fixtures/movingHeatSourceCase/system/controlDict @@ -0,0 +1,23 @@ +FoamFile +{ + version 2.0; + format ascii; + class dictionary; + object controlDict; +} + +application additiveFoamUnitTests; +startFrom startTime; +startTime 0; +stopAt endTime; +endTime 10; +deltaT 0.25; +writeControl timeStep; +writeInterval 1; +purgeWrite 0; +writeFormat ascii; +writePrecision 6; +writeCompression off; +timeFormat general; +timePrecision 6; +runTimeModifiable false; diff --git a/tests/movingBeam/Make/files b/tests/movingBeam/Make/files new file mode 100644 index 0000000..043836a --- /dev/null +++ b/tests/movingBeam/Make/files @@ -0,0 +1,4 @@ +../shared/testMain.C +movingBeamTests.C + +EXE = $(FOAM_USER_APPBIN)/additiveFoamMovingBeamTests diff --git a/tests/movingBeam/Make/options b/tests/movingBeam/Make/options new file mode 100644 index 0000000..ae40133 --- /dev/null +++ b/tests/movingBeam/Make/options @@ -0,0 +1,17 @@ +EXE_INC = \ + -I../vendor/doctest \ + -I../shared \ + -I../../applications/solvers/additiveFoam/movingHeatSource/absorptionModels/absorptionModel \ + -I../../applications/solvers/additiveFoam/movingHeatSource/movingBeam \ + -I../../applications/solvers/additiveFoam/movingHeatSource/segment \ + -I$(LIB_SRC)/meshTools/lnInclude \ + -I$(LIB_SRC)/meshTools/zoneGenerators/cell/generatedCellZone \ + -I$(LIB_SRC)/OpenFOAM/lnInclude \ + -I$(LIB_SRC)/finiteVolume/lnInclude + +EXE_LIBS = \ + -L$(FOAM_USER_LIBBIN) \ + -lmovingBeamModels \ + -lOpenFOAM \ + -lfiniteVolume \ + -lmeshTools diff --git a/tests/movingBeam/movingBeamTests.C b/tests/movingBeam/movingBeamTests.C new file mode 100644 index 0000000..f22cd0c --- /dev/null +++ b/tests/movingBeam/movingBeamTests.C @@ -0,0 +1,100 @@ +#include "doctest.h" + +#include "movingBeam.H" +#include "movingHeatSourceTestFixture.H" + +namespace +{ + +bool scalarClose +( + const Foam::scalar lhs, + const Foam::scalar rhs, + const Foam::scalar tol = 1e-9 +) +{ + return Foam::mag(lhs - rhs) <= tol; +} + +Foam::movingBeam makeBeam(Foam::Time& runTime, const char* sourceName) +{ + Foam::IOdictionary heatSourceDict(additiveFoamTest::makeHeatSourceDict(runTime)); + return additiveFoamTest::suppressStdout + ( + [&]() + { + return Foam::movingBeam(sourceName, heatSourceDict, runTime); + } + ); +} + +} // namespace + +TEST_CASE("Foam::movingBeam computes path times and activity from the fixture scan path") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); + + CHECK_EQ(beam.findIndex(0.0), 1); + CHECK_EQ(beam.findIndex(1.5), 1); + CHECK_EQ(beam.findIndex(1.500001), 2); + CHECK_EQ(beam.findIndex(3.000001), 3); + CHECK(beam.activePath()); + + runTime->setTime(3.6, 0); + CHECK(!beam.activePath()); +} + +TEST_CASE("Foam::movingBeam skips zero-duration point sources when locating the active segment") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "skipBeam")); + + CHECK_EQ(beam.findIndex(0.0), 2); + CHECK_EQ(beam.findIndex(0.5), 2); +} + +TEST_CASE("Foam::movingBeam move interpolates travel segments and switches power on boundaries") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); + + beam.move(0.0); + CHECK_EQ(beam.position().x(), Foam::scalar(1.0)); + CHECK_EQ(beam.power(), Foam::scalar(0.0)); + + beam.move(2.25); + CHECK(scalarClose(beam.position().x(), 2.5)); + CHECK(scalarClose(beam.position().y(), 0.0)); + CHECK(scalarClose(beam.power(), 200.0)); + + beam.move(3.0); + CHECK(scalarClose(beam.position().x(), 4.0)); + CHECK(scalarClose(beam.power(), 200.0)); + + beam.move(3.000001); + CHECK(scalarClose(beam.position().x(), 4.0)); + CHECK(scalarClose(beam.power(), 50.0)); +} + +TEST_CASE("Foam::movingBeam adjustDeltaT lands on the next path interval when enabled") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); + + Foam::scalar dt = 1.0; + beam.adjustDeltaT(dt); + + CHECK(scalarClose(dt, 0.75)); +} + +TEST_CASE("Foam::movingBeam adjustDeltaT leaves the timestep unchanged when path hits are disabled") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "noHitBeam")); + + Foam::scalar dt = 1.0; + beam.adjustDeltaT(dt); + + CHECK(scalarClose(dt, 1.0)); +} diff --git a/tests/movingHeatSourceModels/Make/files b/tests/movingHeatSourceModels/Make/files new file mode 100644 index 0000000..4aaaf17 --- /dev/null +++ b/tests/movingHeatSourceModels/Make/files @@ -0,0 +1,4 @@ +../shared/testMain.C +movingHeatSourceModelTests.C + +EXE = $(FOAM_USER_APPBIN)/additiveFoamMovingHeatSourceModelTests diff --git a/tests/movingHeatSourceModels/Make/options b/tests/movingHeatSourceModels/Make/options new file mode 100644 index 0000000..4ac60b9 --- /dev/null +++ b/tests/movingHeatSourceModels/Make/options @@ -0,0 +1,23 @@ +EXE_INC = \ + -I../vendor/doctest \ + -I../shared \ + -I../../applications/solvers/additiveFoam/movingHeatSource/absorptionModels/absorptionModel \ + -I../../applications/solvers/additiveFoam/movingHeatSource/absorptionModels/constant \ + -I../../applications/solvers/additiveFoam/movingHeatSource/absorptionModels/Kelly \ + -I../../applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/heatSourceModel \ + -I../../applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/superGaussian \ + -I../../applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/modifiedSuperGaussian \ + -I../../applications/solvers/additiveFoam/movingHeatSource/heatSourceModels/projectedGaussian \ + -I../../applications/solvers/additiveFoam/movingHeatSource/movingBeam \ + -I../../applications/solvers/additiveFoam/movingHeatSource/segment \ + -I$(LIB_SRC)/meshTools/lnInclude \ + -I$(LIB_SRC)/meshTools/zoneGenerators/cell/generatedCellZone \ + -I$(LIB_SRC)/OpenFOAM/lnInclude \ + -I$(LIB_SRC)/finiteVolume/lnInclude + +EXE_LIBS = \ + -L$(FOAM_USER_LIBBIN) \ + -lmovingBeamModels \ + -lOpenFOAM \ + -lfiniteVolume \ + -lmeshTools diff --git a/tests/movingHeatSourceModels/movingHeatSourceModelTests.C b/tests/movingHeatSourceModels/movingHeatSourceModelTests.C new file mode 100644 index 0000000..d8f2a6d --- /dev/null +++ b/tests/movingHeatSourceModels/movingHeatSourceModelTests.C @@ -0,0 +1,188 @@ +#include + +#include "doctest.h" + +#include "KellyAbsorption.H" +#include "constantAbsorption.H" +#include "modifiedSuperGaussian.H" +#include "movingHeatSourceTestFixture.H" +#include "projectedGaussian.H" +#include "superGaussian.H" + +namespace +{ + +bool scalarClose +( + const Foam::scalar lhs, + const Foam::scalar rhs, + const Foam::scalar tol = 1e-9 +) +{ + return Foam::mag(lhs - rhs) <= tol; +} + +Foam::IOdictionary makeHeatSourceDict(Foam::Time& runTime) +{ + return additiveFoamTest::makeHeatSourceDict(runTime); +} + +Foam::scalar expectedKellyEta +( + const Foam::word& geometry, + const Foam::scalar aspectRatio, + const Foam::scalar eta0, + const Foam::scalar etaMin +) +{ + if (aspectRatio <= 1.0) + { + return etaMin; + } + + const Foam::scalar theta = Foam::atan(1.0 / aspectRatio); + + Foam::scalar F = 0.0; + Foam::scalar G = 0.0; + + if (geometry == "cone") + { + F = 0.25 * (3.0 * Foam::sin(theta) - Foam::sin(3.0 * theta)); + G = 1.0 / (1.0 + Foam::sqrt(1.0 + Foam::pow(aspectRatio, 2))); + } + else + { + F = 0.5 * (1.0 - Foam::cos(2.0 * theta)); + G = 0.5 / (1.0 + aspectRatio); + } + + return eta0 * (1.0 + (1.0 - eta0) * (G - F)) + / (1.0 - (1.0 - eta0) * (1.0 - G)); +} + +Foam::scalar projectedK +( + const Foam::scalar aspectRatio, + const Foam::scalar A, + const Foam::scalar B +) +{ + const Foam::scalar n = Foam::min(Foam::max(A * std::log2(aspectRatio) + B, 0.0), 9.0); + return std::pow(2.0, n); +} + +} // namespace + +TEST_CASE("constant absorption returns the configured eta for any aspect ratio") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); + Foam::IOdictionary heatSourceDict(makeHeatSourceDict(*runTime)); + + Foam::absorptionModels::constant model("testBeam", heatSourceDict, mesh); + + CHECK(scalarClose(model.eta(0.5), 0.35)); + CHECK(scalarClose(model.eta(7.5), 0.35)); +} + +TEST_CASE("Kelly absorption matches the cone and cylinder formulas and respects etaMin") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); + Foam::IOdictionary heatSourceDict(makeHeatSourceDict(*runTime)); + + Foam::absorptionModels::Kelly cone("kellyConeBeam", heatSourceDict, mesh); + Foam::absorptionModels::Kelly cylinder("kellyCylinderBeam", heatSourceDict, mesh); + + const Foam::scalar aspectRatio = 2.0; + + CHECK(scalarClose(cone.eta(aspectRatio), expectedKellyEta("cone", aspectRatio, 0.45, 0.15))); + CHECK(scalarClose(cylinder.eta(aspectRatio), expectedKellyEta("cylinder", aspectRatio, 0.45, 0.15))); + CHECK(scalarClose(cone.eta(1.0), 0.15)); + CHECK(scalarClose(cylinder.eta(0.75), 0.15)); +} + +TEST_CASE("superGaussian weight is centered and symmetric and V0 matches the normalization formula") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); + Foam::IOdictionary heatSourceDict(makeHeatSourceDict(*runTime)); + + Foam::heatSourceModels::superGaussian model + ( + additiveFoamTest::suppressStdout + ( + [&]() + { + return Foam::heatSourceModels::superGaussian("testBeam", heatSourceDict, mesh); + } + ) + ); + + CHECK(scalarClose(model.weight(Foam::vector::zero), 1.0)); + CHECK(scalarClose(model.weight(Foam::vector(1.0, 0.0, 0.0)), model.weight(Foam::vector(-1.0, 0.0, 0.0)))); + CHECK(model.weight(Foam::vector(1.0, 0.0, 0.0)) < 1.0); + + const Foam::scalar k = 2.0; + const Foam::scalar a = Foam::pow(2.0, 1.0 / k); + const Foam::vector s = Foam::vector(2.0, 2.0, 4.0) / a; + const Foam::scalar expectedV0 = + (2.0 / 3.0) * s.x() * s.y() * s.z() + * Foam::constant::mathematical::pi * Foam::tgamma(1.0 + 3.0 / k); + + CHECK(scalarClose(model.V0().value(), expectedV0)); +} + +TEST_CASE("modifiedSuperGaussian truncates beyond the beam depth and remains symmetric in-plane") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); + Foam::IOdictionary heatSourceDict(makeHeatSourceDict(*runTime)); + + Foam::heatSourceModels::modifiedSuperGaussian model + ( + additiveFoamTest::suppressStdout + ( + [&]() + { + return Foam::heatSourceModels::modifiedSuperGaussian("modifiedBeam", heatSourceDict, mesh); + } + ) + ); + + CHECK(scalarClose(model.weight(Foam::vector::zero), 1.0)); + CHECK(scalarClose(model.weight(Foam::vector(0.5, 0.0, 1.0)), model.weight(Foam::vector(-0.5, 0.0, 1.0)))); + CHECK(scalarClose(model.weight(Foam::vector(0.0, 0.0, 4.0)), 0.0)); + CHECK(model.V0().value() > 0.0); +} + +TEST_CASE("projectedGaussian clamps the derived exponent and decays away from the center") +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); + Foam::IOdictionary heatSourceDict(makeHeatSourceDict(*runTime)); + + Foam::heatSourceModels::projectedGaussian model + ( + additiveFoamTest::suppressStdout + ( + [&]() + { + return Foam::heatSourceModels::projectedGaussian("projectedBeam", heatSourceDict, mesh); + } + ) + ); + + CHECK(scalarClose(model.weight(Foam::vector::zero), 1.0)); + CHECK(model.weight(Foam::vector(0.25, 0.0, 0.0)) < 1.0); + CHECK(model.weight(Foam::vector(0.0, 0.0, 16.0)) < model.weight(Foam::vector(0.0, 0.0, 1.0))); + + const Foam::scalar aspectRatio = 16.0 / Foam::min(1.0, 2.0); + const Foam::scalar k = projectedK(aspectRatio, 3.0, 0.0); + const Foam::scalar expectedV0 = + 0.5 * Foam::constant::mathematical::pi * 1.0 * 2.0 * 16.0 + * Foam::tgamma(1.0 / k) / (k * std::pow(3.0, 1.0 / k)); + + CHECK(scalarClose(k, 512.0)); + CHECK(scalarClose(model.V0().value(), expectedV0)); +} diff --git a/tests/run b/tests/run index 3054a23..4eacca6 100755 --- a/tests/run +++ b/tests/run @@ -10,6 +10,9 @@ fi test_binaries=" $FOAM_USER_APPBIN/additiveFoamSegmentTests +$FOAM_USER_APPBIN/additiveFoamMovingBeamTests +$FOAM_USER_APPBIN/additiveFoamMovingHeatSourceModelTests +$FOAM_USER_APPBIN/additiveFoamUtilityTests " for test_binary in $test_binaries diff --git a/tests/shared/movingHeatSourceTestFixture.H b/tests/shared/movingHeatSourceTestFixture.H new file mode 100644 index 0000000..90c38d6 --- /dev/null +++ b/tests/shared/movingHeatSourceTestFixture.H @@ -0,0 +1,111 @@ +#ifndef movingHeatSourceTestFixture_H +#define movingHeatSourceTestFixture_H + +#include +#include +#include +#include +#include + +#include "Time.H" +#include "IOdictionary.H" +#include "zeroDimensionalFvMesh.H" + +namespace additiveFoamTest +{ + +static const Foam::fileName fixtureRootPath("."); +static const Foam::fileName fixtureCaseName("fixtures/movingHeatSourceCase"); + +class ScopedStdoutSilencer +{ + int savedFd_; + int nullFd_; + +public: + ScopedStdoutSilencer() + : + savedFd_(-1), + nullFd_(-1) + { + std::fflush(stdout); + + savedFd_ = dup(STDOUT_FILENO); + nullFd_ = open("/dev/null", O_WRONLY); + + if (savedFd_ >= 0 && nullFd_ >= 0) + { + dup2(nullFd_, STDOUT_FILENO); + } + } + + ~ScopedStdoutSilencer() + { + std::fflush(stdout); + + if (savedFd_ >= 0) + { + dup2(savedFd_, STDOUT_FILENO); + close(savedFd_); + } + + if (nullFd_ >= 0) + { + close(nullFd_); + } + } + + ScopedStdoutSilencer(const ScopedStdoutSilencer&) = delete; + ScopedStdoutSilencer& operator=(const ScopedStdoutSilencer&) = delete; +}; + +template +inline auto suppressStdout(Fn&& fn) -> decltype(fn()) +{ + ScopedStdoutSilencer silencer; + return fn(); +} + +inline std::unique_ptr makeTime() +{ + return suppressStdout + ( + []() + { + return std::unique_ptr + ( + new Foam::Time + ( + Foam::Time::controlDictName, + fixtureRootPath, + fixtureCaseName, + false + ) + ); + } + ); +} + +inline Foam::IOdictionary makeHeatSourceDict(Foam::Time& runTime) +{ + return Foam::IOdictionary + ( + Foam::IOobject + ( + "heatSourceDict", + runTime.constant(), + runTime, + Foam::IOobject::MUST_READ, + Foam::IOobject::NO_WRITE + ) + ); +} + +inline Foam::fvMesh makeFixtureMesh(Foam::Time& runTime) +{ + return Foam::zeroDimensionalFvMesh(runTime); +} + +} // namespace additiveFoamTest + +#endif diff --git a/tests/utilities/Make/files b/tests/utilities/Make/files new file mode 100644 index 0000000..761da71 --- /dev/null +++ b/tests/utilities/Make/files @@ -0,0 +1,4 @@ +../shared/testMain.C +utilityTests.C + +EXE = $(FOAM_USER_APPBIN)/additiveFoamUtilityTests diff --git a/tests/utilities/Make/options b/tests/utilities/Make/options new file mode 100644 index 0000000..cf4cb9e --- /dev/null +++ b/tests/utilities/Make/options @@ -0,0 +1,13 @@ +EXE_INC = \ + -I../vendor/doctest \ + -I../../applications/solvers/additiveFoam/utilities/interpolateXY \ + -I../../applications/solvers/additiveFoam/utilities/graph \ + -I$(LIB_SRC)/OpenFOAM/lnInclude \ + -I$(LIB_SRC)/finiteVolume/lnInclude + +EXE_LIBS = \ + -L$(FOAM_USER_LIBBIN) \ + -ladditiveFoamUtilities \ + -lOpenFOAM \ + -lfiniteVolume \ + -lmeshTools diff --git a/tests/utilities/utilityTests.C b/tests/utilities/utilityTests.C new file mode 100644 index 0000000..0a11f14 --- /dev/null +++ b/tests/utilities/utilityTests.C @@ -0,0 +1,82 @@ +#include "doctest.h" + +#include "OStringStream.H" +#include "graph.H" +#include "interpolateXY.H" + +namespace +{ + +bool scalarClose +( + const Foam::scalar lhs, + const Foam::scalar rhs, + const Foam::scalar tol = 1e-9 +) +{ + return Foam::mag(lhs - rhs) <= tol; +} + +} // namespace + +TEST_CASE("interpolateXY handles exact hits, interpolation, clamping, and unsorted x data") +{ + Foam::scalarField xOld(3); + xOld[0] = 3.0; + xOld[1] = 1.0; + xOld[2] = 2.0; + + Foam::scalarField yOld(3); + yOld[0] = 30.0; + yOld[1] = 10.0; + yOld[2] = 20.0; + + CHECK(scalarClose(Foam::interpolateXY(2.0, xOld, yOld), 20.0)); + CHECK(scalarClose(Foam::interpolateXY(1.5, xOld, yOld), 15.0)); + CHECK(scalarClose(Foam::interpolateXY(0.0, xOld, yOld), 10.0)); + CHECK(scalarClose(Foam::interpolateXY(4.0, xOld, yOld), 30.0)); + + const Foam::labelPair labels = Foam::interpolateXYLabels(1.5, xOld, yOld); + CHECK_EQ(labels.first(), 1); + CHECK_EQ(labels.second(), 2); +} + +TEST_CASE("graph wordify normalizes labels and y() returns the only curve") +{ + Foam::scalarField x(2); + x[0] = 0.0; + x[1] = 1.0; + + Foam::scalarField y(2); + y[0] = 2.0; + y[1] = 3.0; + + CHECK_EQ(Foam::graph::wordify("Melt Pool (mm)"), Foam::word("Melt_Pool__mm")); + + Foam::graph g("title", "x", "Melt Pool (mm)", x, y); + + CHECK_EQ(g.y()[0], Foam::scalar(2.0)); + CHECK_EQ(g.y()[1], Foam::scalar(3.0)); +} + +TEST_CASE("graph writeTable emits the stored xy pairs") +{ + Foam::scalarField x(2); + x[0] = 0.0; + x[1] = 1.0; + + Foam::scalarField y(2); + y[0] = 2.0; + y[1] = 3.0; + + Foam::graph g("title", "x", "y", x, y); + Foam::OStringStream os; + g.writeTable(os); + + const std::string output = os.str(); + + CHECK(output.find("0") != std::string::npos); + CHECK(output.find("1") != std::string::npos); + CHECK(output.find("2") != std::string::npos); + CHECK(output.find("3") != std::string::npos); +} From 56f1c7bc0260fdc086eb272fc5bd9394644323db Mon Sep 17 00:00:00 2001 From: Gerry Knapp Date: Mon, 18 May 2026 14:17:54 -0400 Subject: [PATCH 10/10] convert testing to googletest --- .github/workflows/CI.yml | 13 ++ README.md | 2 +- TESTING.md | 32 +++- tests/Allwmake | 53 ++++++ tests/movingBeam/Make/options | 5 +- tests/movingBeam/movingBeamTests.C | 60 +++--- tests/movingHeatSourceModels/Make/options | 5 +- .../movingHeatSourceModelTests.C | 60 +++--- tests/segment/Make/options | 5 +- tests/segment/segmentTests.C | 32 ++-- tests/shared/testMain.C | 7 +- tests/utilities/Make/options | 5 +- tests/utilities/utilityTests.C | 44 ++--- tests/vendor/doctest/LICENSE.txt | 21 --- tests/vendor/doctest/doctest.h | 174 ------------------ 15 files changed, 199 insertions(+), 319 deletions(-) delete mode 100644 tests/vendor/doctest/LICENSE.txt delete mode 100644 tests/vendor/doctest/doctest.h diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8938d4d..0883ad2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -32,13 +32,26 @@ jobs: . /opt/openfoam13/etc/bashrc || true test -n "$WM_PROJECT_DIR" ./Allwmake + - name: Install GoogleTest + run: | + apt-get update + apt-get install -y --no-install-recommends cmake libgtest-dev + if ! find /usr/lib /usr/local/lib -name 'libgtest.a' -o -name 'libgtest.so' | grep -q .; then + cmake -S /usr/src/googletest -B /tmp/googletest-build + cmake --build /tmp/googletest-build -j2 + cp /tmp/googletest-build/lib/libgtest*.a /usr/local/lib/ + fi + test -f /usr/include/gtest/gtest.h + find /usr/lib /usr/local/lib -name 'libgtest.a' -o -name 'libgtest.so' | grep -q . - name: Build native tests run: | . /opt/openfoam13/etc/bashrc || true + test -n "$WM_PROJECT_DIR" ./tests/Allwmake - name: Run native tests run: | . /opt/openfoam13/etc/bashrc || true + test -n "$WM_PROJECT_DIR" ./tests/run - name: Test AdditiveFOAM run: | diff --git a/README.md b/README.md index 39434a1..f49554f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The documentation for `AdditiveFOAM` is hosted on [GitHub Pages](https://ornl.github.io/AdditiveFOAM/). -For local test commands and guidance on adding native C++ tests, see [TESTING.md](TESTING.md). +For local test commands, GoogleTest prerequisites, and guidance on adding native C++ tests, see [TESTING.md](TESTING.md). ### Repository Features | Link | Description | diff --git a/TESTING.md b/TESTING.md index 4165fb6..fdc5870 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2,13 +2,32 @@ AdditiveFOAM has two test layers: -- Native C++ unit-style tests under [`tests/`](tests), built with `wmake` and linked against the existing AdditiveFOAM/OpenFOAM libraries. +- Native C++ unit-style tests under [`tests/`](tests), built with `wmake`, linked against the existing AdditiveFOAM/OpenFOAM libraries, and driven by GoogleTest. - The tutorial smoke run in GitHub Actions, which checks end-to-end integration. ## Prerequisites - An OpenFOAM-13 environment must be sourced before building or running tests. - AdditiveFOAM must be built first so the native tests can link against `libmovingBeamModels`. +- GoogleTest must be installed outside the repository. `./tests/Allwmake` auto-detects standard install locations and also honors `GTEST_ROOT`, `GTEST_INCLUDE_DIR`, and `GTEST_LIB_DIR`. + +## Install GoogleTest + +On Debian/Ubuntu systems: + +```bash +sudo apt-get update +sudo apt-get install -y --no-install-recommends cmake libgtest-dev + +if [ ! -f /usr/lib/x86_64-linux-gnu/libgtest.a ] && [ ! -f /usr/local/lib/libgtest.a ]; then + cmake -S /usr/src/googletest -B /tmp/googletest-build + cmake --build /tmp/googletest-build -j2 + sudo cp /tmp/googletest-build/lib/libgtest*.a /usr/local/lib/ +fi +``` + +If GoogleTest is installed somewhere else, set `GTEST_ROOT` or both +`GTEST_INCLUDE_DIR` and `GTEST_LIB_DIR` before running `./tests/Allwmake`. ## Build And Run @@ -21,6 +40,15 @@ From the repository root: ./tests/run ``` +If GoogleTest is installed in a non-standard location, export one of these before `./tests/Allwmake`: + +```bash +export GTEST_ROOT=/path/to/gtest +# or +export GTEST_INCLUDE_DIR=/path/to/include +export GTEST_LIB_DIR=/path/to/lib +``` + `./tests/Allwmake` builds the native test executables without changing the default production build path. `./tests/run` executes the complete native suite. ## Current Coverage @@ -41,5 +69,3 @@ The `movingBeam` and heat-source-model tests use a small file-backed fixture cas 3. Add a `Make/options` file with the required include paths and linked AdditiveFOAM/OpenFOAM libraries. 4. Add the new directory to [`tests/Allwmake`](tests/Allwmake). 5. Add the produced executable to [`tests/run`](tests/run). - -The vendored header at [`tests/vendor/doctest/doctest.h`](tests/vendor/doctest/doctest.h) keeps the harness self-contained and avoids extra package dependencies. diff --git a/tests/Allwmake b/tests/Allwmake index f0f33c6..c83af86 100755 --- a/tests/Allwmake +++ b/tests/Allwmake @@ -6,6 +6,59 @@ if [ -z "${WM_PROJECT_DIR:-}" ]; then exit 1 fi +find_gtest_include_dir() +{ + for candidate in \ + "${GTEST_INCLUDE_DIR:-}" \ + "${GTEST_ROOT:-}/include" \ + /usr/include \ + /usr/local/include + do + if [ -n "$candidate" ] && [ -f "$candidate/gtest/gtest.h" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +find_gtest_lib_dir() +{ + for candidate in \ + "${GTEST_LIB_DIR:-}" \ + "${GTEST_ROOT:-}/lib" \ + "${GTEST_ROOT:-}/lib64" \ + /usr/lib/x86_64-linux-gnu \ + /usr/lib64 \ + /usr/lib \ + /usr/local/lib64 \ + /usr/local/lib + do + if [ -n "$candidate" ] && \ + { [ -f "$candidate/libgtest.a" ] || [ -f "$candidate/libgtest.so" ]; } + then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +GTEST_INCLUDE_DIR=$(find_gtest_include_dir) || { + echo "Unable to find GoogleTest headers. Install gtest or set GTEST_ROOT/GTEST_INCLUDE_DIR." >&2 + exit 1 +} + +GTEST_LIB_DIR=$(find_gtest_lib_dir) || { + echo "Unable to find the GoogleTest library. Install gtest or set GTEST_ROOT/GTEST_LIB_DIR." >&2 + exit 1 +} + +export GTEST_INCLUDE_DIR +export GTEST_LIB_DIR + . "$WM_PROJECT_DIR/wmake/scripts/AllwmakeParseArguments" wmake $targetType segment diff --git a/tests/movingBeam/Make/options b/tests/movingBeam/Make/options index ae40133..294531b 100644 --- a/tests/movingBeam/Make/options +++ b/tests/movingBeam/Make/options @@ -1,5 +1,5 @@ EXE_INC = \ - -I../vendor/doctest \ + -I$(GTEST_INCLUDE_DIR) \ -I../shared \ -I../../applications/solvers/additiveFoam/movingHeatSource/absorptionModels/absorptionModel \ -I../../applications/solvers/additiveFoam/movingHeatSource/movingBeam \ @@ -10,6 +10,9 @@ EXE_INC = \ -I$(LIB_SRC)/finiteVolume/lnInclude EXE_LIBS = \ + -L$(GTEST_LIB_DIR) \ + -lgtest \ + -lpthread \ -L$(FOAM_USER_LIBBIN) \ -lmovingBeamModels \ -lOpenFOAM \ diff --git a/tests/movingBeam/movingBeamTests.C b/tests/movingBeam/movingBeamTests.C index f22cd0c..9b27b12 100644 --- a/tests/movingBeam/movingBeamTests.C +++ b/tests/movingBeam/movingBeamTests.C @@ -1,4 +1,4 @@ -#include "doctest.h" +#include #include "movingBeam.H" #include "movingHeatSourceTestFixture.H" @@ -6,16 +6,6 @@ namespace { -bool scalarClose -( - const Foam::scalar lhs, - const Foam::scalar rhs, - const Foam::scalar tol = 1e-9 -) -{ - return Foam::mag(lhs - rhs) <= tol; -} - Foam::movingBeam makeBeam(Foam::Time& runTime, const char* sourceName) { Foam::IOdictionary heatSourceDict(additiveFoamTest::makeHeatSourceDict(runTime)); @@ -30,54 +20,54 @@ Foam::movingBeam makeBeam(Foam::Time& runTime, const char* sourceName) } // namespace -TEST_CASE("Foam::movingBeam computes path times and activity from the fixture scan path") +TEST(movingBeamTests, computesPathTimesAndActivityFromTheFixtureScanPath) { auto runTime = additiveFoamTest::makeTime(); Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); - CHECK_EQ(beam.findIndex(0.0), 1); - CHECK_EQ(beam.findIndex(1.5), 1); - CHECK_EQ(beam.findIndex(1.500001), 2); - CHECK_EQ(beam.findIndex(3.000001), 3); - CHECK(beam.activePath()); + EXPECT_EQ(beam.findIndex(0.0), 1); + EXPECT_EQ(beam.findIndex(1.5), 1); + EXPECT_EQ(beam.findIndex(1.500001), 2); + EXPECT_EQ(beam.findIndex(3.000001), 3); + EXPECT_TRUE(beam.activePath()); runTime->setTime(3.6, 0); - CHECK(!beam.activePath()); + EXPECT_FALSE(beam.activePath()); } -TEST_CASE("Foam::movingBeam skips zero-duration point sources when locating the active segment") +TEST(movingBeamTests, skipsZeroDurationPointSourcesWhenLocatingTheActiveSegment) { auto runTime = additiveFoamTest::makeTime(); Foam::movingBeam beam(makeBeam(*runTime, "skipBeam")); - CHECK_EQ(beam.findIndex(0.0), 2); - CHECK_EQ(beam.findIndex(0.5), 2); + EXPECT_EQ(beam.findIndex(0.0), 2); + EXPECT_EQ(beam.findIndex(0.5), 2); } -TEST_CASE("Foam::movingBeam move interpolates travel segments and switches power on boundaries") +TEST(movingBeamTests, moveInterpolatesTravelSegmentsAndSwitchesPowerOnBoundaries) { auto runTime = additiveFoamTest::makeTime(); Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); beam.move(0.0); - CHECK_EQ(beam.position().x(), Foam::scalar(1.0)); - CHECK_EQ(beam.power(), Foam::scalar(0.0)); + EXPECT_DOUBLE_EQ(beam.position().x(), Foam::scalar(1.0)); + EXPECT_DOUBLE_EQ(beam.power(), Foam::scalar(0.0)); beam.move(2.25); - CHECK(scalarClose(beam.position().x(), 2.5)); - CHECK(scalarClose(beam.position().y(), 0.0)); - CHECK(scalarClose(beam.power(), 200.0)); + EXPECT_NEAR(beam.position().x(), 2.5, 1e-9); + EXPECT_NEAR(beam.position().y(), 0.0, 1e-9); + EXPECT_NEAR(beam.power(), 200.0, 1e-9); beam.move(3.0); - CHECK(scalarClose(beam.position().x(), 4.0)); - CHECK(scalarClose(beam.power(), 200.0)); + EXPECT_NEAR(beam.position().x(), 4.0, 1e-9); + EXPECT_NEAR(beam.power(), 200.0, 1e-9); beam.move(3.000001); - CHECK(scalarClose(beam.position().x(), 4.0)); - CHECK(scalarClose(beam.power(), 50.0)); + EXPECT_NEAR(beam.position().x(), 4.0, 1e-9); + EXPECT_NEAR(beam.power(), 50.0, 1e-9); } -TEST_CASE("Foam::movingBeam adjustDeltaT lands on the next path interval when enabled") +TEST(movingBeamTests, adjustDeltaTLandsOnTheNextPathIntervalWhenEnabled) { auto runTime = additiveFoamTest::makeTime(); Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); @@ -85,10 +75,10 @@ TEST_CASE("Foam::movingBeam adjustDeltaT lands on the next path interval when en Foam::scalar dt = 1.0; beam.adjustDeltaT(dt); - CHECK(scalarClose(dt, 0.75)); + EXPECT_NEAR(dt, 0.75, 1e-9); } -TEST_CASE("Foam::movingBeam adjustDeltaT leaves the timestep unchanged when path hits are disabled") +TEST(movingBeamTests, adjustDeltaTLeavesTheTimestepUnchangedWhenPathHitsAreDisabled) { auto runTime = additiveFoamTest::makeTime(); Foam::movingBeam beam(makeBeam(*runTime, "noHitBeam")); @@ -96,5 +86,5 @@ TEST_CASE("Foam::movingBeam adjustDeltaT leaves the timestep unchanged when path Foam::scalar dt = 1.0; beam.adjustDeltaT(dt); - CHECK(scalarClose(dt, 1.0)); + EXPECT_NEAR(dt, 1.0, 1e-9); } diff --git a/tests/movingHeatSourceModels/Make/options b/tests/movingHeatSourceModels/Make/options index 4ac60b9..b4a4380 100644 --- a/tests/movingHeatSourceModels/Make/options +++ b/tests/movingHeatSourceModels/Make/options @@ -1,5 +1,5 @@ EXE_INC = \ - -I../vendor/doctest \ + -I$(GTEST_INCLUDE_DIR) \ -I../shared \ -I../../applications/solvers/additiveFoam/movingHeatSource/absorptionModels/absorptionModel \ -I../../applications/solvers/additiveFoam/movingHeatSource/absorptionModels/constant \ @@ -16,6 +16,9 @@ EXE_INC = \ -I$(LIB_SRC)/finiteVolume/lnInclude EXE_LIBS = \ + -L$(GTEST_LIB_DIR) \ + -lgtest \ + -lpthread \ -L$(FOAM_USER_LIBBIN) \ -lmovingBeamModels \ -lOpenFOAM \ diff --git a/tests/movingHeatSourceModels/movingHeatSourceModelTests.C b/tests/movingHeatSourceModels/movingHeatSourceModelTests.C index d8f2a6d..4fdfa79 100644 --- a/tests/movingHeatSourceModels/movingHeatSourceModelTests.C +++ b/tests/movingHeatSourceModels/movingHeatSourceModelTests.C @@ -1,6 +1,6 @@ #include -#include "doctest.h" +#include #include "KellyAbsorption.H" #include "constantAbsorption.H" @@ -12,16 +12,6 @@ namespace { -bool scalarClose -( - const Foam::scalar lhs, - const Foam::scalar rhs, - const Foam::scalar tol = 1e-9 -) -{ - return Foam::mag(lhs - rhs) <= tol; -} - Foam::IOdictionary makeHeatSourceDict(Foam::Time& runTime) { return additiveFoamTest::makeHeatSourceDict(runTime); @@ -73,7 +63,7 @@ Foam::scalar projectedK } // namespace -TEST_CASE("constant absorption returns the configured eta for any aspect ratio") +TEST(movingHeatSourceModelTests, constantAbsorptionReturnsTheConfiguredEtaForAnyAspectRatio) { auto runTime = additiveFoamTest::makeTime(); Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); @@ -81,11 +71,11 @@ TEST_CASE("constant absorption returns the configured eta for any aspect ratio") Foam::absorptionModels::constant model("testBeam", heatSourceDict, mesh); - CHECK(scalarClose(model.eta(0.5), 0.35)); - CHECK(scalarClose(model.eta(7.5), 0.35)); + EXPECT_NEAR(model.eta(0.5), 0.35, 1e-9); + EXPECT_NEAR(model.eta(7.5), 0.35, 1e-9); } -TEST_CASE("Kelly absorption matches the cone and cylinder formulas and respects etaMin") +TEST(movingHeatSourceModelTests, KellyAbsorptionMatchesTheConeAndCylinderFormulasAndRespectsEtaMin) { auto runTime = additiveFoamTest::makeTime(); Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); @@ -96,13 +86,13 @@ TEST_CASE("Kelly absorption matches the cone and cylinder formulas and respects const Foam::scalar aspectRatio = 2.0; - CHECK(scalarClose(cone.eta(aspectRatio), expectedKellyEta("cone", aspectRatio, 0.45, 0.15))); - CHECK(scalarClose(cylinder.eta(aspectRatio), expectedKellyEta("cylinder", aspectRatio, 0.45, 0.15))); - CHECK(scalarClose(cone.eta(1.0), 0.15)); - CHECK(scalarClose(cylinder.eta(0.75), 0.15)); + EXPECT_NEAR(cone.eta(aspectRatio), expectedKellyEta("cone", aspectRatio, 0.45, 0.15), 1e-9); + EXPECT_NEAR(cylinder.eta(aspectRatio), expectedKellyEta("cylinder", aspectRatio, 0.45, 0.15), 1e-9); + EXPECT_NEAR(cone.eta(1.0), 0.15, 1e-9); + EXPECT_NEAR(cylinder.eta(0.75), 0.15, 1e-9); } -TEST_CASE("superGaussian weight is centered and symmetric and V0 matches the normalization formula") +TEST(movingHeatSourceModelTests, superGaussianWeightIsCenteredAndSymmetricAndV0MatchesTheNormalizationFormula) { auto runTime = additiveFoamTest::makeTime(); Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); @@ -119,9 +109,9 @@ TEST_CASE("superGaussian weight is centered and symmetric and V0 matches the nor ) ); - CHECK(scalarClose(model.weight(Foam::vector::zero), 1.0)); - CHECK(scalarClose(model.weight(Foam::vector(1.0, 0.0, 0.0)), model.weight(Foam::vector(-1.0, 0.0, 0.0)))); - CHECK(model.weight(Foam::vector(1.0, 0.0, 0.0)) < 1.0); + EXPECT_NEAR(model.weight(Foam::vector::zero), 1.0, 1e-9); + EXPECT_NEAR(model.weight(Foam::vector(1.0, 0.0, 0.0)), model.weight(Foam::vector(-1.0, 0.0, 0.0)), 1e-9); + EXPECT_LT(model.weight(Foam::vector(1.0, 0.0, 0.0)), 1.0); const Foam::scalar k = 2.0; const Foam::scalar a = Foam::pow(2.0, 1.0 / k); @@ -130,10 +120,10 @@ TEST_CASE("superGaussian weight is centered and symmetric and V0 matches the nor (2.0 / 3.0) * s.x() * s.y() * s.z() * Foam::constant::mathematical::pi * Foam::tgamma(1.0 + 3.0 / k); - CHECK(scalarClose(model.V0().value(), expectedV0)); + EXPECT_NEAR(model.V0().value(), expectedV0, 1e-9); } -TEST_CASE("modifiedSuperGaussian truncates beyond the beam depth and remains symmetric in-plane") +TEST(movingHeatSourceModelTests, modifiedSuperGaussianTruncatesBeyondTheBeamDepthAndRemainsSymmetricInPlane) { auto runTime = additiveFoamTest::makeTime(); Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); @@ -150,13 +140,13 @@ TEST_CASE("modifiedSuperGaussian truncates beyond the beam depth and remains sym ) ); - CHECK(scalarClose(model.weight(Foam::vector::zero), 1.0)); - CHECK(scalarClose(model.weight(Foam::vector(0.5, 0.0, 1.0)), model.weight(Foam::vector(-0.5, 0.0, 1.0)))); - CHECK(scalarClose(model.weight(Foam::vector(0.0, 0.0, 4.0)), 0.0)); - CHECK(model.V0().value() > 0.0); + EXPECT_NEAR(model.weight(Foam::vector::zero), 1.0, 1e-9); + EXPECT_NEAR(model.weight(Foam::vector(0.5, 0.0, 1.0)), model.weight(Foam::vector(-0.5, 0.0, 1.0)), 1e-9); + EXPECT_NEAR(model.weight(Foam::vector(0.0, 0.0, 4.0)), 0.0, 1e-9); + EXPECT_GT(model.V0().value(), 0.0); } -TEST_CASE("projectedGaussian clamps the derived exponent and decays away from the center") +TEST(movingHeatSourceModelTests, projectedGaussianClampsTheDerivedExponentAndDecaysAwayFromTheCenter) { auto runTime = additiveFoamTest::makeTime(); Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); @@ -173,9 +163,9 @@ TEST_CASE("projectedGaussian clamps the derived exponent and decays away from th ) ); - CHECK(scalarClose(model.weight(Foam::vector::zero), 1.0)); - CHECK(model.weight(Foam::vector(0.25, 0.0, 0.0)) < 1.0); - CHECK(model.weight(Foam::vector(0.0, 0.0, 16.0)) < model.weight(Foam::vector(0.0, 0.0, 1.0))); + EXPECT_NEAR(model.weight(Foam::vector::zero), 1.0, 1e-9); + EXPECT_LT(model.weight(Foam::vector(0.25, 0.0, 0.0)), 1.0); + EXPECT_LT(model.weight(Foam::vector(0.0, 0.0, 16.0)), model.weight(Foam::vector(0.0, 0.0, 1.0))); const Foam::scalar aspectRatio = 16.0 / Foam::min(1.0, 2.0); const Foam::scalar k = projectedK(aspectRatio, 3.0, 0.0); @@ -183,6 +173,6 @@ TEST_CASE("projectedGaussian clamps the derived exponent and decays away from th 0.5 * Foam::constant::mathematical::pi * 1.0 * 2.0 * 16.0 * Foam::tgamma(1.0 / k) / (k * std::pow(3.0, 1.0 / k)); - CHECK(scalarClose(k, 512.0)); - CHECK(scalarClose(model.V0().value(), expectedV0)); + EXPECT_NEAR(k, 512.0, 1e-9); + EXPECT_NEAR(model.V0().value(), expectedV0, 1e-9); } diff --git a/tests/segment/Make/options b/tests/segment/Make/options index 911e830..0f00a72 100644 --- a/tests/segment/Make/options +++ b/tests/segment/Make/options @@ -1,9 +1,12 @@ EXE_INC = \ - -I../vendor/doctest \ + -I$(GTEST_INCLUDE_DIR) \ -I../../applications/solvers/additiveFoam/movingHeatSource/segment \ -I$(LIB_SRC)/OpenFOAM/lnInclude EXE_LIBS = \ + -L$(GTEST_LIB_DIR) \ + -lgtest \ + -lpthread \ -L$(FOAM_USER_LIBBIN) \ -lmovingBeamModels \ -lOpenFOAM \ diff --git a/tests/segment/segmentTests.C b/tests/segment/segmentTests.C index 8f3c0ed..b6af373 100644 --- a/tests/segment/segmentTests.C +++ b/tests/segment/segmentTests.C @@ -1,29 +1,29 @@ #include -#include "doctest.h" +#include #include "segment.H" -TEST_CASE("Foam::segment default construction yields a zeroed point source") +TEST(segmentTests, defaultConstructionYieldsAZeroedPointSource) { Foam::segment seg; - CHECK_EQ(seg.mode(), Foam::scalar(1)); - CHECK_EQ(seg.position().x(), Foam::scalar(0)); - CHECK_EQ(seg.position().y(), Foam::scalar(0)); - CHECK_EQ(seg.position().z(), Foam::scalar(0)); - CHECK_EQ(seg.power(), Foam::scalar(0)); - CHECK_EQ(seg.parameter(), Foam::scalar(0)); - CHECK_EQ(seg.time(), Foam::scalar(0)); + EXPECT_DOUBLE_EQ(seg.mode(), Foam::scalar(1)); + EXPECT_DOUBLE_EQ(seg.position().x(), Foam::scalar(0)); + EXPECT_DOUBLE_EQ(seg.position().y(), Foam::scalar(0)); + EXPECT_DOUBLE_EQ(seg.position().z(), Foam::scalar(0)); + EXPECT_DOUBLE_EQ(seg.power(), Foam::scalar(0)); + EXPECT_DOUBLE_EQ(seg.parameter(), Foam::scalar(0)); + EXPECT_DOUBLE_EQ(seg.time(), Foam::scalar(0)); } -TEST_CASE("Foam::segment parses a space-delimited segment definition") +TEST(segmentTests, parsesASpaceDelimitedSegmentDefinition) { Foam::segment seg(std::string("0 1 2 3 400 5")); - CHECK_EQ(seg.mode(), Foam::scalar(0)); - CHECK_EQ(seg.position().x(), Foam::scalar(1)); - CHECK_EQ(seg.position().y(), Foam::scalar(2)); - CHECK_EQ(seg.position().z(), Foam::scalar(3)); - CHECK_EQ(seg.power(), Foam::scalar(400)); - CHECK_EQ(seg.parameter(), Foam::scalar(5)); + EXPECT_DOUBLE_EQ(seg.mode(), Foam::scalar(0)); + EXPECT_DOUBLE_EQ(seg.position().x(), Foam::scalar(1)); + EXPECT_DOUBLE_EQ(seg.position().y(), Foam::scalar(2)); + EXPECT_DOUBLE_EQ(seg.position().z(), Foam::scalar(3)); + EXPECT_DOUBLE_EQ(seg.power(), Foam::scalar(400)); + EXPECT_DOUBLE_EQ(seg.parameter(), Foam::scalar(5)); } diff --git a/tests/shared/testMain.C b/tests/shared/testMain.C index 9b5d602..9bb465e 100644 --- a/tests/shared/testMain.C +++ b/tests/shared/testMain.C @@ -1,6 +1,7 @@ -#include "doctest.h" +#include -int main() +int main(int argc, char** argv) { - return doctest::runTests(); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/tests/utilities/Make/options b/tests/utilities/Make/options index cf4cb9e..7253dd6 100644 --- a/tests/utilities/Make/options +++ b/tests/utilities/Make/options @@ -1,11 +1,14 @@ EXE_INC = \ - -I../vendor/doctest \ + -I$(GTEST_INCLUDE_DIR) \ -I../../applications/solvers/additiveFoam/utilities/interpolateXY \ -I../../applications/solvers/additiveFoam/utilities/graph \ -I$(LIB_SRC)/OpenFOAM/lnInclude \ -I$(LIB_SRC)/finiteVolume/lnInclude EXE_LIBS = \ + -L$(GTEST_LIB_DIR) \ + -lgtest \ + -lpthread \ -L$(FOAM_USER_LIBBIN) \ -ladditiveFoamUtilities \ -lOpenFOAM \ diff --git a/tests/utilities/utilityTests.C b/tests/utilities/utilityTests.C index 0a11f14..cdb4737 100644 --- a/tests/utilities/utilityTests.C +++ b/tests/utilities/utilityTests.C @@ -1,4 +1,4 @@ -#include "doctest.h" +#include #include "OStringStream.H" #include "graph.H" @@ -7,19 +7,9 @@ namespace { -bool scalarClose -( - const Foam::scalar lhs, - const Foam::scalar rhs, - const Foam::scalar tol = 1e-9 -) -{ - return Foam::mag(lhs - rhs) <= tol; -} - } // namespace -TEST_CASE("interpolateXY handles exact hits, interpolation, clamping, and unsorted x data") +TEST(utilityTests, interpolateXYHandlesExactHitsInterpolationClampingAndUnsortedXData) { Foam::scalarField xOld(3); xOld[0] = 3.0; @@ -31,17 +21,17 @@ TEST_CASE("interpolateXY handles exact hits, interpolation, clamping, and unsort yOld[1] = 10.0; yOld[2] = 20.0; - CHECK(scalarClose(Foam::interpolateXY(2.0, xOld, yOld), 20.0)); - CHECK(scalarClose(Foam::interpolateXY(1.5, xOld, yOld), 15.0)); - CHECK(scalarClose(Foam::interpolateXY(0.0, xOld, yOld), 10.0)); - CHECK(scalarClose(Foam::interpolateXY(4.0, xOld, yOld), 30.0)); + EXPECT_NEAR(Foam::interpolateXY(2.0, xOld, yOld), 20.0, 1e-9); + EXPECT_NEAR(Foam::interpolateXY(1.5, xOld, yOld), 15.0, 1e-9); + EXPECT_NEAR(Foam::interpolateXY(0.0, xOld, yOld), 10.0, 1e-9); + EXPECT_NEAR(Foam::interpolateXY(4.0, xOld, yOld), 30.0, 1e-9); const Foam::labelPair labels = Foam::interpolateXYLabels(1.5, xOld, yOld); - CHECK_EQ(labels.first(), 1); - CHECK_EQ(labels.second(), 2); + EXPECT_EQ(labels.first(), 1); + EXPECT_EQ(labels.second(), 2); } -TEST_CASE("graph wordify normalizes labels and y() returns the only curve") +TEST(utilityTests, graphWordifyNormalizesLabelsAndYReturnsTheOnlyCurve) { Foam::scalarField x(2); x[0] = 0.0; @@ -51,15 +41,15 @@ TEST_CASE("graph wordify normalizes labels and y() returns the only curve") y[0] = 2.0; y[1] = 3.0; - CHECK_EQ(Foam::graph::wordify("Melt Pool (mm)"), Foam::word("Melt_Pool__mm")); + EXPECT_EQ(Foam::graph::wordify("Melt Pool (mm)"), Foam::word("Melt_Pool__mm")); Foam::graph g("title", "x", "Melt Pool (mm)", x, y); - CHECK_EQ(g.y()[0], Foam::scalar(2.0)); - CHECK_EQ(g.y()[1], Foam::scalar(3.0)); + EXPECT_DOUBLE_EQ(g.y()[0], Foam::scalar(2.0)); + EXPECT_DOUBLE_EQ(g.y()[1], Foam::scalar(3.0)); } -TEST_CASE("graph writeTable emits the stored xy pairs") +TEST(utilityTests, graphWriteTableEmitsTheStoredXYPairs) { Foam::scalarField x(2); x[0] = 0.0; @@ -75,8 +65,8 @@ TEST_CASE("graph writeTable emits the stored xy pairs") const std::string output = os.str(); - CHECK(output.find("0") != std::string::npos); - CHECK(output.find("1") != std::string::npos); - CHECK(output.find("2") != std::string::npos); - CHECK(output.find("3") != std::string::npos); + EXPECT_NE(output.find("0"), std::string::npos); + EXPECT_NE(output.find("1"), std::string::npos); + EXPECT_NE(output.find("2"), std::string::npos); + EXPECT_NE(output.find("3"), std::string::npos); } diff --git a/tests/vendor/doctest/LICENSE.txt b/tests/vendor/doctest/LICENSE.txt deleted file mode 100644 index 5ae0eb1..0000000 --- a/tests/vendor/doctest/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016-2023 Viktor Kirilov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/tests/vendor/doctest/doctest.h b/tests/vendor/doctest/doctest.h deleted file mode 100644 index e4bc3b5..0000000 --- a/tests/vendor/doctest/doctest.h +++ /dev/null @@ -1,174 +0,0 @@ - -// ================================================================================================= -// -// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD -// -// Copyright (c) 2016-2017 Viktor Kirilov -// -// Distributed under the MIT Software License -// See accompanying LICENSE.txt file or copy at -// https://opensource.org/licenses/MIT -// -// The documentation can be found at the library's page: -// https://github.com/doctest/doctest -// -// ================================================================================================= - -#ifndef TESTS_VENDOR_DOCTEST_DOCTEST_H -#define TESTS_VENDOR_DOCTEST_DOCTEST_H - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace doctest -{ - -struct TestCase -{ - const char* name; - void (*func)(); -}; - -inline std::vector& registry() -{ - static std::vector tests; - return tests; -} - -struct Registrar -{ - Registrar(const char* name, void (*func)()) - { - registry().push_back({name, func}); - } -}; - -class AssertionFailure : public std::runtime_error -{ -public: - explicit AssertionFailure(const std::string& message) - : - std::runtime_error(message) - {} -}; - -template -[[noreturn]] inline void failEquality -( - const char* file, - int line, - const char* lhsExpr, - const char* rhsExpr, - const Left& lhs, - const Right& rhs -) -{ - std::ostringstream os; - os << file << ":" << line << ": CHECK_EQ(" << lhsExpr << ", " << rhsExpr - << ") failed with lhs=" << lhs << " rhs=" << rhs; - throw AssertionFailure(os.str()); -} - -[[noreturn]] inline void failCheck -( - const char* file, - int line, - const char* expression -) -{ - std::ostringstream os; - os << file << ":" << line << ": CHECK(" << expression << ") failed"; - throw AssertionFailure(os.str()); -} - -template -inline void checkEqual -( - const Left& lhs, - const Right& rhs, - const char* lhsExpr, - const char* rhsExpr, - const char* file, - int line -) -{ - if (!(lhs == rhs)) - { - failEquality(file, line, lhsExpr, rhsExpr, lhs, rhs); - } -} - -inline void check -( - bool condition, - const char* expression, - const char* file, - int line -) -{ - if (!condition) - { - failCheck(file, line, expression); - } -} - -inline int runTests() -{ - int failed = 0; - int passed = 0; - - for (const auto& test : registry()) - { - try - { - test.func(); - ++passed; - std::cout << "[pass] " << test.name << '\n'; - } - catch (const std::exception& err) - { - ++failed; - std::cerr << "[fail] " << test.name << '\n' - << err.what() << '\n'; - } - catch (...) - { - ++failed; - std::cerr << "[fail] " << test.name << '\n' - << "Unknown exception\n"; - } - } - - std::cout << "Executed " << (passed + failed) << " test case(s): " - << passed << " passed, " << failed << " failed\n"; - - return failed == 0 ? 0 : 1; -} - -} // namespace doctest - -#define DOCTEST_DETAIL_CONCAT_IMPL(lhs, rhs) lhs##rhs -#define DOCTEST_DETAIL_CONCAT(lhs, rhs) DOCTEST_DETAIL_CONCAT_IMPL(lhs, rhs) - -#define TEST_CASE(name) \ - static void DOCTEST_DETAIL_CONCAT(doctest_case_, __LINE__)(); \ - static doctest::Registrar DOCTEST_DETAIL_CONCAT \ - ( \ - doctest_registrar_, \ - __LINE__ \ - )(name, &DOCTEST_DETAIL_CONCAT(doctest_case_, __LINE__)); \ - static void DOCTEST_DETAIL_CONCAT(doctest_case_, __LINE__)() - -#define CHECK(expression) \ - doctest::check(static_cast(expression), #expression, __FILE__, __LINE__) - -#define CHECK_EQ(lhs, rhs) \ - doctest::checkEqual((lhs), (rhs), #lhs, #rhs, __FILE__, __LINE__) - -#endif