diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a8b0756..0883ad2 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,40 @@ 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: 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: | - . /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..f49554f 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, 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 new file mode 100644 index 0000000..fdc5870 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,71 @@ +# Testing + +AdditiveFOAM has two test layers: + +- 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 + +From the repository root: + +```bash +. /path/to/openfoam/etc/bashrc +./Allwmake +./tests/Allwmake +./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 + +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). 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..c83af86 --- /dev/null +++ b/tests/Allwmake @@ -0,0 +1,67 @@ +#!/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 + +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 +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..294531b --- /dev/null +++ b/tests/movingBeam/Make/options @@ -0,0 +1,20 @@ +EXE_INC = \ + -I$(GTEST_INCLUDE_DIR) \ + -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$(GTEST_LIB_DIR) \ + -lgtest \ + -lpthread \ + -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..9b27b12 --- /dev/null +++ b/tests/movingBeam/movingBeamTests.C @@ -0,0 +1,90 @@ +#include + +#include "movingBeam.H" +#include "movingHeatSourceTestFixture.H" + +namespace +{ + +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(movingBeamTests, computesPathTimesAndActivityFromTheFixtureScanPath) +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); + + 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); + EXPECT_FALSE(beam.activePath()); +} + +TEST(movingBeamTests, skipsZeroDurationPointSourcesWhenLocatingTheActiveSegment) +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "skipBeam")); + + EXPECT_EQ(beam.findIndex(0.0), 2); + EXPECT_EQ(beam.findIndex(0.5), 2); +} + +TEST(movingBeamTests, moveInterpolatesTravelSegmentsAndSwitchesPowerOnBoundaries) +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); + + beam.move(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); + 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); + EXPECT_NEAR(beam.position().x(), 4.0, 1e-9); + EXPECT_NEAR(beam.power(), 200.0, 1e-9); + + beam.move(3.000001); + EXPECT_NEAR(beam.position().x(), 4.0, 1e-9); + EXPECT_NEAR(beam.power(), 50.0, 1e-9); +} + +TEST(movingBeamTests, adjustDeltaTLandsOnTheNextPathIntervalWhenEnabled) +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "testBeam")); + + Foam::scalar dt = 1.0; + beam.adjustDeltaT(dt); + + EXPECT_NEAR(dt, 0.75, 1e-9); +} + +TEST(movingBeamTests, adjustDeltaTLeavesTheTimestepUnchangedWhenPathHitsAreDisabled) +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::movingBeam beam(makeBeam(*runTime, "noHitBeam")); + + Foam::scalar dt = 1.0; + beam.adjustDeltaT(dt); + + EXPECT_NEAR(dt, 1.0, 1e-9); +} 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..b4a4380 --- /dev/null +++ b/tests/movingHeatSourceModels/Make/options @@ -0,0 +1,26 @@ +EXE_INC = \ + -I$(GTEST_INCLUDE_DIR) \ + -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$(GTEST_LIB_DIR) \ + -lgtest \ + -lpthread \ + -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..4fdfa79 --- /dev/null +++ b/tests/movingHeatSourceModels/movingHeatSourceModelTests.C @@ -0,0 +1,178 @@ +#include + +#include + +#include "KellyAbsorption.H" +#include "constantAbsorption.H" +#include "modifiedSuperGaussian.H" +#include "movingHeatSourceTestFixture.H" +#include "projectedGaussian.H" +#include "superGaussian.H" + +namespace +{ + +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(movingHeatSourceModelTests, constantAbsorptionReturnsTheConfiguredEtaForAnyAspectRatio) +{ + auto runTime = additiveFoamTest::makeTime(); + Foam::fvMesh mesh(additiveFoamTest::makeFixtureMesh(*runTime)); + Foam::IOdictionary heatSourceDict(makeHeatSourceDict(*runTime)); + + Foam::absorptionModels::constant model("testBeam", heatSourceDict, mesh); + + EXPECT_NEAR(model.eta(0.5), 0.35, 1e-9); + EXPECT_NEAR(model.eta(7.5), 0.35, 1e-9); +} + +TEST(movingHeatSourceModelTests, KellyAbsorptionMatchesTheConeAndCylinderFormulasAndRespectsEtaMin) +{ + 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; + + 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(movingHeatSourceModelTests, superGaussianWeightIsCenteredAndSymmetricAndV0MatchesTheNormalizationFormula) +{ + 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); + } + ) + ); + + 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); + 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); + + EXPECT_NEAR(model.V0().value(), expectedV0, 1e-9); +} + +TEST(movingHeatSourceModelTests, modifiedSuperGaussianTruncatesBeyondTheBeamDepthAndRemainsSymmetricInPlane) +{ + 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); + } + ) + ); + + 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(movingHeatSourceModelTests, projectedGaussianClampsTheDerivedExponentAndDecaysAwayFromTheCenter) +{ + 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); + } + ) + ); + + 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); + 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)); + + EXPECT_NEAR(k, 512.0, 1e-9); + EXPECT_NEAR(model.V0().value(), expectedV0, 1e-9); +} 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..0f00a72 --- /dev/null +++ b/tests/segment/Make/options @@ -0,0 +1,14 @@ +EXE_INC = \ + -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 \ + -lfiniteVolume \ + -lmeshTools diff --git a/tests/segment/segmentTests.C b/tests/segment/segmentTests.C new file mode 100644 index 0000000..b6af373 --- /dev/null +++ b/tests/segment/segmentTests.C @@ -0,0 +1,29 @@ +#include + +#include +#include "segment.H" + +TEST(segmentTests, defaultConstructionYieldsAZeroedPointSource) +{ + Foam::segment seg; + + 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(segmentTests, parsesASpaceDelimitedSegmentDefinition) +{ + Foam::segment seg(std::string("0 1 2 3 400 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/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..9bb465e --- /dev/null +++ b/tests/shared/testMain.C @@ -0,0 +1,7 @@ +#include + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} 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..7253dd6 --- /dev/null +++ b/tests/utilities/Make/options @@ -0,0 +1,16 @@ +EXE_INC = \ + -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 \ + -lfiniteVolume \ + -lmeshTools diff --git a/tests/utilities/utilityTests.C b/tests/utilities/utilityTests.C new file mode 100644 index 0000000..cdb4737 --- /dev/null +++ b/tests/utilities/utilityTests.C @@ -0,0 +1,72 @@ +#include + +#include "OStringStream.H" +#include "graph.H" +#include "interpolateXY.H" + +namespace +{ + +} // namespace + +TEST(utilityTests, interpolateXYHandlesExactHitsInterpolationClampingAndUnsortedXData) +{ + 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; + + 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); + EXPECT_EQ(labels.first(), 1); + EXPECT_EQ(labels.second(), 2); +} + +TEST(utilityTests, graphWordifyNormalizesLabelsAndYReturnsTheOnlyCurve) +{ + Foam::scalarField x(2); + x[0] = 0.0; + x[1] = 1.0; + + Foam::scalarField y(2); + y[0] = 2.0; + y[1] = 3.0; + + EXPECT_EQ(Foam::graph::wordify("Melt Pool (mm)"), Foam::word("Melt_Pool__mm")); + + Foam::graph g("title", "x", "Melt Pool (mm)", x, y); + + EXPECT_DOUBLE_EQ(g.y()[0], Foam::scalar(2.0)); + EXPECT_DOUBLE_EQ(g.y()[1], Foam::scalar(3.0)); +} + +TEST(utilityTests, graphWriteTableEmitsTheStoredXYPairs) +{ + 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(); + + 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); +}