diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a8b0756..8938d4d 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'}} @@ -13,7 +14,7 @@ jobs: CI: defaults: run: - shell: bash -ileo pipefail {0} + shell: bash {0} strategy: matrix: cxx: ['g++'] @@ -21,18 +22,27 @@ 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 + . /opt/openfoam13/etc/bashrc || true + test -n "$WM_PROJECT_DIR" ./Allwmake + - name: Build native tests + run: | + . /opt/openfoam13/etc/bashrc || true + ./tests/Allwmake + - name: Run native tests + run: | + . /opt/openfoam13/etc/bashrc || true + ./tests/run - name: Test AdditiveFOAM run: | - . /opt/openfoam10/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 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..4165fb6 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,45 @@ +# 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 native suite currently builds four executables: + +- `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 + +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/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 * * * * * * * * * * * * * // 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 new file mode 100755 index 0000000..f0f33c6 --- /dev/null +++ b/tests/Allwmake @@ -0,0 +1,14 @@ +#!/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 +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 new file mode 100755 index 0000000..4eacca6 --- /dev/null +++ b/tests/run @@ -0,0 +1,27 @@ +#!/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 +$FOAM_USER_APPBIN/additiveFoamMovingBeamTests +$FOAM_USER_APPBIN/additiveFoamMovingHeatSourceModelTests +$FOAM_USER_APPBIN/additiveFoamUtilityTests +" + +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/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/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/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); +} 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