Unit testing

AMR-Wind uses GoogleTest to provide unit-testing capabilities for the source code. Unit tests are built by default when compiling AMR-Wind using CMake and can be run using the amr_wind_unit_tests executable — see Compiling AMR-Wind for details of building AMR-Wind with CMake. This section documents the process of running the unit tests, the unit test scaffolding facilities available within AMR-Wind unit testing infrastructure, and a brief overview of the process of creating new unit tests.

Warning

The legacy GNUMakefile system does not support building unit tests. User must use the CMake build process to be able to run unit tests.

Running unit tests

To run the unit test suite, simply execute the amr_wind_unit_tests executable at the command prompt. This will execute all the unit tests and print a summary of the success/failure status for each tests. The executable will also print a summary of the total number of passed, failed, and skipped tests at the end of execution. Additional arguments can be used to control the behavior of execution of unit tests, .e.g., to run a single test or a subset of tests.

# Show command line arguments and brief help
./amr_wind_unit_tests -h

# List available tests
./amr_wind_unit_tests --gtest_list_tests

# Run a single test
./amr_wind_unit_tests --gtest_filter="ABLTest.abl_forcing"

# Run all tests belonging to a test suite
./amr_wind_unit_tests --gtest_filter="ABLTest.*"

# Run all tests beginning with keyword
./amr_wind_unit_tests --gtest_filter="*.*field*"

# Run all tests except for those in ABLTest suite
./amr_wind_unit_tests --gtest_filter="-ABLTest.*"

Basic unit test concepts

This section provides a quick overview of basic unit testing concepts for beginners. We recommend reading at least GoogleTest Primer docs to get a better understanding of unit test concepts and how to use GoogleTest to create new unit tests. More advanced use cases can be found in GoogleTest advanced docs. Unit tests small functions that test a specific aspect of the code. These tests are organized into test suites that group different tests by category. For example, all tests related to the definition of fields and field repository is organized in the test suite FieldRepoTest. To run just this subset of tests use:

# Run only tests related to field repository
./amr_wind_unit_tests --gtest_filter="FieldRepoTest.*"

Assertions

Within a test, we check for expected behavior using *assertions* that test for different conditions. For example, if we were writing a function square that took one argument, a real number, and returned the square of the input value, we would write a unit test as shown below:

//! Return the square of the number
double square(double x)
{
    return x * x;
}

TEST(TestSquare, test_square)
{
    EXPECT_EQ(square(4), 16);
    EXPECT_EQ(square(5), 25);

    // Check negative numbers too
    EXPECT_EQ(square(-4), 16);
    EXPECT_EQ(square(-5), 25);

}

In the above example, EXPECT_EQ is an assertion provided by GoogleTest that allows us to check that the two arguments are equal. We use this to test that the output of the function matches expected values. Unit tests can be used to check other expected behaviors of the function rather than just its correctness. For example, consider a function square_root that computes the square root of a given real number, but is supposed to throw a runtime error if it encounters a negative number. Unit tests allow us to test both cases: 1. the function produces the correct results for positive numbers and, 2. it throws an error for negative numbers. The following code shows this example

//! Return the square root of a number
double square_root(double x)
{
    if (x < 0)
        throw std::runtime_error("Square root requires a positive number");

    return std::sqrt(x);
}

TEST(TestSquareRoot, test_sqrt)
{
    EXPECT_NEAR(square_root(4.0), 2.0, 1.0e-12);
    EXPECT_NEAR(square_root(16.0), 4.0, 1.0e-12);

    // Check sqrt(2) to a lower tolerance (1.0e-3)
    EXPECT_NEAR(square_root(2.0), 1.41421356237, 1.0e-3);

    // Check that negative number creates runtime error
    EXPECT_THROW(square_root(-2.0), std::runtime_error);
}

Tests and Test Fixtures

GoogleTest supports two types of tests: simple tests, and tests that require fixtures. Simple tests are tests that test standalone functions that require no additional data structures for its execution – see GoogleTest Simple Test docs. Test fixtures, on the other hand, allow you to group all necessary data in a custom test class that can be reused for multiple tests. Defining a new simple test requires the use of TEST() macro, and defining a new test with a fixture requires the use of TEST_F() macro. In AMR-Wind, we use test fixtures extensively to perform actions like creating a mesh and generating some test fields that will be used to perform the tests.

Unit testing in AMR-Wind

Unit test files are in amr-wind/unit_tests directory. All unit test code is written within the amr_wind_tests namespace. This section describes the scaffolding available to create unit tests and provides a few examples of unit tests to help developers write new ones.

Unit test scaffolding

While unit testing simple functions is straightforward, performing unit tests on CFD applications is more complicated. Most often this will require the creation of a test mesh, generating field data structures that will be used to set up and run the test. This is also complicated by the fact that AMReX creates several global data structures, e.g., Geometry and ParmParse, that must be properly reset between each test to ensure a clean environment for each test. AMR-Wind provides a few classes that provide the necessary scaffolding to quickly setup and run tests.

Within the amr-wind/unit_tests directory, the scaffolding utilities related to testing are in aw_test_utils directory. This section provides a brief overview of the core classes and their purpose.

pp_utils - ParmParse utilities

Classes written in AMR-Wind often require user inputs that are generally read in from input files through amrex::ParmParse (see docs). pp_utils are a set of functions that create skeleton input data that are used by various classes during initialization. For example, it populates the problem domain, mesh sizes, etc. so that amrex::AmrMesh can be initialized properly.

default_mesh_inputs()

Populates ParmParse data structure with all the necessary inputs to create an AMRMesh instance.

default_time_inputs()

Populates ParmParse data structure with necessary inputs for amr_wind::SimTime.

class AmrexTestEnv

AmrexTestEnv is a subclass of ::testing::Environment that provides global setup and teardown actions. This classes is registered with the GoogleTest environment and is responsible for calling amrex::Initialize() and amrex::Finalize() at appropriate times. It also customizes the AMReX setup by changing a few defaults:

  • Disables AMReX’s default signal_handling behavior and restores standard C++ exception handling. This is necessary for EXPECT_THROW type assertions to function properly.

  • Sets the verbosity such that no messages are printed from AMReX library during the execution of unit tests.

  • Calling ParmParse::Finalize() immediately after amrex::Initialize() so that each test can begin with a clean parameter environment.

The last two actions can be overridden by the user for specific invocations of the unit test executable by providing additional command line arguments. For example, to set the verbosity:

./amr_wind_unit_tests amrex.verbose=1

And to keep parameters provided in the command line for use with tests:

./amr_wind_unit_tests utest.keep_parameters=1

In normal development workflow, users will almost never have to interact with AmrexTestEnv directly.

class AmrexTest

AmrexTest is a test fixture is derived from ::testing::Test and is the base class for all the other test fixtures used within AMR-Wind unit testing infrastructure. It provides setup and teardown actions that call ParmParse::Initialize() and ParmParse::Finalize() actions respectively to create a clean inputs table environment for each test. The setup/teardown actions are called before and after a TEST_F() body is executed. This fixture does not create a mesh or related data structures, and can be used as a base fixture for tests that do not require any underlying mesh description.

The following example, taken from one of the unit tests in AMR-Wind, shows usage of this test fixture:

 1// All unit tests are created within the `amr_wind_tests` namespace
 2namespace amr_wind_tests {
 3
 4// Anonymous namespace to declare utility functions used within this file
 5namespace {
 6
 7// Helper function to populate ParmParse with all the necessary inputs used by
 8// SimTime class.
 9void build_simtime_params()
10{
11    amrex::ParmParse pp("time");
12    pp.add("stop_time", 2.0);
13    pp.add("max_step", 10);
14    pp.add("fixed_dt", -0.1);
15    pp.add("init_shrink", 0.1);
16    pp.add("cfl", 0.45);
17    pp.add("verbose", -1);
18    pp.add("regrid_interval", 3);
19    pp.add("plot_interval", 1);
20    pp.add("checkpoint_interval", 2);
21}
22
23} // namespace
24
25// Create unique namespace for this test fixture. This is useful to group tests
26// related to SimTime object for filtering during execution.
27class SimTimeTest : public AmrexTest {};
28
29// This is an example of a unit test that tests SimTime behavior with the
30// AmrexTest test fixture
31TEST_F(SimTimeTest, fixed_dt_loop)
32{
33    // Call helper function to populate the defaults
34    build_simtime_params();
35    {
36        // Override defaults to switch to fixed timestep
37        amrex::ParmParse pp("time");
38        pp.add("fixed_dt", 0.2);
39    }
40
41    // Create the object that is being tested
42    amr_wind::SimTime time;
43    time.parse_parameters();
44
45    // Perform tests
46    int counter = 0;
47    int regrid_counter = 0;
48    int plot_counter = 0;
49    int chkpt_counter = 0;
50    while (time.new_timestep()) {
51        time.set_current_cfl(2.0);
52        ++counter;
53
54        if (time.write_plot_file()) ++plot_counter;
55        if (time.write_checkpoint()) ++chkpt_counter;
56        if (time.do_regrid()) ++regrid_counter;
57    }
58    EXPECT_EQ(counter, 10);
59    EXPECT_EQ(plot_counter, 10);
60    EXPECT_EQ(chkpt_counter, 5);
61    EXPECT_EQ(regrid_counter, 3);
62
63    EXPECT_FALSE(time.write_last_checkpoint());
64    EXPECT_FALSE(time.write_last_plot_file());
65}
66
67} // namespace amr_wind_tests
class AmrTestMesh

AmrTestMesh is a concrete implementation of amrex::AmrCore that creates an AMR mesh that can be used with unit testing. In addition to implementing the basic level data creation methods and refinement routines ErrorEst, it also creates an amr_wind::FieldRepo instance for creating and manipulating fields from within unit tests. AmrTestMesh is never directly created within unit tests, instead it is created on-demand through the test fixture MeshTest described next.

class MeshTest

MeshTest is the base test fixture for any test that requires a mesh and associated field data that will be used by the test. In addition to performing setup/teardown actions described in AmrexTest, it also resets the default amrex::Geometry static data so that different tests can run on different problem domains prescribed by the test fixture.

Almost all unit tests within AMR-Wind use MeshTest as their base test fixture. In order to allow grouping tests in logical test suites. The following example shows the basic usage of this test fixture.

 1/** Example showing the use of MeshTest test fixture in AMR-Wind unit tests
 2 *
 3 */
 4
 5// AMR-Wind unit test namespace
 6namespace amr_wind_tests {
 7
 8// Create a unique name for this test suite (for grouping and filtering)
 9class DemoTest : public MeshTest
10{};
11
12TEST_F(DemoTest, test_demo_meshtest)
13{
14    // Before performing any actions the mesh has to be initialized
15    initialize_mesh();
16
17    // Now all data structures are ready for use by the test
18
19    // Access the amr_wind::CFDSim object
20    auto& sim = sim();
21
22    // Access the simulation time object
23    auto& time = sim.time();
24
25    // Access the field repository object
26    auto& repo = sim.repo();
27
28    // Access the mesh itself
29    auto& mesh = sim.mesh();
30
31    //
32    // Perform tests with data
33    //
34
35    // By default, field repository must be empty
36    EXPECT_EQ(repo.num_fields(), 0);
37}
38
39} // namespace amr_wind_tests

The next example shows a more advanced use case where the user can override defaults before creating the mesh.

 1TEST_F(DemoTest, test_meshtest_advanced)
 2{
 3    // This test shows a more advanced example to create intermediate data
 4    // before generating mesh
 5
 6    // 1. populate the default parameters
 7    populate_parameters();
 8
 9    // 2. Change default mesh resolution
10    amrex::Vector<int> ncell{{16, 16, 32}};
11    pp.addarr("ncell", ncell);
12
13    // 3. Create the mesh instance
14    create_mesh_instance();
15
16    // 4. Declare additional fields
17    auto& repo = sim().repo();
18
19    repo.declare_cc_field("velocity", 3, 1, 2);
20    repo.declare_cc_field("density", 1, 1, 1);
21    repo.declare_nd_field("pressure", 1, 1, 1);
22
23    EXPECT_EQ(repo.num_fields, 3);
24
25    // 5. Create the mesh structure
26    initialize_mesh();
27
28    // Check that there is at least 1 level
29    EXPECT_EQ(repo.num_active_levels(), 1);
30}

Methods defined by MeshTest

initialize_mesh()

A test must call initialize_mesh() before performing any tests that require a mesh or associated fields. Behind the scenes, initialize_mesh() performs several actions. It calls populate_parameters(), creates a mesh instance, creates levels from scratch. After a call to this function, the mesh is ready for use.

populate_parameters()

Populate default parameters necessary for creating an AMRMesh and amr_wind::SimTime objects.

create_mesh_instance()

Create a new AMRMesh instance. This doesn’t create the level data from scratch yet. That is deferred until initialize_mesh() is called.

Example unit tests

Following are a list of unit tests available within AMR-Wind repository that can be used as starting points for users to write new tests:

test_simtime.cpp

Simple unit test example that tests the behavior of amr_wind::SimTime. This test only relies on AmrexTest and does not require a mesh.

test_abl_init.cpp

This is an example that uses MeshTest to generate a test mesh and test the ABL initial conditions generator algorithms.

test_refinement.cpp

This is an advanced example that test the user-defined nested mesh refinement algorithm by creating a test fixture that is capable of adaptive mesh refinement based on the criteria.