Problem Generator and Unit Tests

CUP:

4

Author:

Cheng Li

Type:

Feature

Created:

01-07-2017

Builds-on:

CUP2

Overview

In CUP2, we discussed that the design principle of Canoe is to allow dissembling a big program into small pieces, and then assemble them together in any arbitrary way to tackle another problem. In this CUP, we discuss the concepts of unit test and problem generator. The former permits test small pieces of code, and the latter permits generating the initial condition for a new problem.

Unit Test

A unit test encompasses a small piece of code that tests a single function or a certain behavior of a module. The unit test is the starting point of assembling a bigger program. A unit test is a crucial step to ensure that the code is behaving as expected.

Canoe uses gtest (google test) as the unit test framework. All test subroutines are placed under tests/ folder. According to the gtest convention, the name of the test subroutine should start with TEST_F for testing member functions of a class or TEST for testing standalone functions. For example, the following code snippet defines a test subroutine for the interpolation function interp_weno5:

TEST(interp_weno5, test_case1) {
  double phim2 = 1.0;
  double phim1 = 2.0;
  double phi = 3.0;
  double phip1 = 4.0;
  double phip2 = 5.0;
  double result = interp_weno5(phim2, phim1, phi, phip1, phip2);
  double expected_result = 2.5000000000000004;
  EXPECT_NEAR(result, expected_result, 1.E-10);
}

in which, the interp_weno5 function is a function that yields the interpolated value at the cell interface using the WENO5 scheme, based on the cell averaged quantities, schematically denoted as:

| x_{-2} | x_{-1} | x_0 | x_1 | x_2 |
                   ^
                   |
                   return value

The function EXPECT_NEAR is a gtest macro that checks whether the result is close to the expected_result within a tolerance of 1.E-10.

You can define as many test subroutines as you want, all starting with the macro definition TEST. The first argument of the TEST macro is the name of the test subroutine, and the second argument is the name of the test case.

The main function of the unit test reads as follows:

int main(int argc, char **argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

As the name suggests, the InitGoogleTest function initializes the gtest framework, and the RUN_ALL_TESTS function runs all the test subroutines.

After compiling the code, all unit tests will be built into executable files under the build/tests/ folder. To run the unit tests, simply type ctest in the build/tests folder. The output should look like this:

Test project /Users/chengcli/Development/canoe/build/tests
    Start 1: test_weno.release
1/5 Test #1: test_weno.release ................   Passed    0.02 sec
    Start 2: test_glog.release
2/5 Test #2: test_glog.release ................   Passed    0.02 sec
    Start 3: test_absorber.release
3/5 Test #3: test_absorber.release ............   Passed    0.07 sec
    Start 4: test_yaml_read.release
4/5 Test #4: test_yaml_read.release ...........   Passed    0.02 sec
    Start 5: test_mesh.release
5/5 Test #5: test_mesh.release ................***Failed    0.07 sec

80% tests passed, 1 tests failed out of 5

Total Test time (real) =   0.19 sec

The following tests FAILED:
    5 - test_mesh.release (Failed)
Errors while running CTest

The output shows that 4 out of 5 tests passed, and the test test_mesh.release failed. The ctest command also generates a log file Testing/Temporary/LastTest.log that stores the detailed information of the test.

For a more sophisticated example of unit test, take a look at the tests/test_impl.cpp file. This file contains a test class that tests the implementation of the MeshBlock::Impl class, which is an extension module of the MeshBlock class. The structure of the unit test class is as follows:

class TestImpl: public ::testing::Test {
protected:
  virtual void SetUp() {
    // set up the test fixture
  }

  virtual void TearDown() {
    // tear down the test fixture
  }

  // declare member variables
};

TEST_F(TestImpl, TestName) {
  // test subroutine
}

The SetUp and TearDown functions are called before and after each test subroutine, respectively. The TEST_F macro is used to test member functions of a class. The first argument of the TEST_F macro is the name of the test class, and the second argument is the name of the test subroutine. Here is a snippet of the implementation of the TestImpl class:

class TestImpl: public ::testing::Test {
 protected:
  ParameterInput *pinput;
  Mesh *pmesh;

  virtual void SetUp() {
    IOWrapper infile;
    infile.Open("test_thermodynamics.inp", IOWrapper::FileMode::read);

    pinput = new ParameterInput;
    pinput->LoadFromFile(infile);

    ...

    IndexMap::InitFromAthenaInput(pinput);
    Thermodynamics::InitFromAthenaInput(pinput);

    int restart = false;
    int mesh_only = false;
    pmesh = new Mesh(pinput, mesh_only);
    ...
    pmesh->Initialize(restart, pinput);
  }

  virtual void TearDown() {
    delete pinput;
    delete pmesh;
    Thermodynamics::Destroy();
    IndexMap::Destroy();
  }
}

TEST_F(TestImpl, GatherPrimitive) {
  // test subroutine
  ...
}

TEST_F(TestImpl, GatherConserved) {
  // test subroutine
  ...
}

In the above code snippet, the SetUp function reads the input file test_thermodynamics.inp and initializes the pinput and pmesh variables. The TearDown function deletes the pinput and pmesh variables and destroys the Thermodynamics and IndexMap classes.

The TEST_F macro defines two test subroutines GatherPrimitive and GatherConserved, which test the procedures of gathering the primitive and conserved variables, respectively. A bigger program can thus be assembled by combining incrementally more test subroutines.

Finally, the instructions for compiling the unit tests are specified in the tests/CMakeLists.txt file:

setup_test(test_weno)
...
if (${NVAPOR} EQUAL 2)
  if (${NCLOUD} EQUAL 4)
    setup_test(test_impl)
    ...
  endif()
endif()

The setup_test is a predefined macro that sets up the compilation of the unit test. The argument of the setup_test macro should be the name of the cpp file that contains the test subroutines. Be aware that certain unit tests may require specific compilation flags. For example, the test_impl unit test requires the NVAPOR variable to be 2 and the NCLOUD variable to be 4. The setup_test macro checks whether the NVAPOR and NCLOUD variables satisfy the requirements. If not, the unit test will not be compiled.

Problem Generator

Once smaller functions are tested, the next step is to assemble them into the solver that Canoe provides. The problem generator is a file that generates the initial condition for a specific problem. Examples of problem generators can be found under the examples/ folder. Each example problem is a folder that may contain the following files:

  • <problem>.cpp: the problem generator

  • <problem>.inp: the input file for the problem

  • <problem>.yaml: the configuration file for the problem

We will leave the discussion of the input file and the configuration file to other CUPs. Here we focus on the problem generator. An example snippet of the problem generator is shown below:

void MeshBlock::ProblemGenerator(ParameterInput *pin) {
  ...
  // construct atmosphere from bottom up
  for (int k = ks; k <= ke; ++k)
    for (int j = js; j <= je; ++j) {
      air.SetZero();
      air.w[iH2O] = xH2O;
      air.w[IPR] = Ps;
      air.w[IDN] = Ts;
      ...
      for (int i = is; i <= ie; ++i) {
        air.w[IVX] = 1. * sin(2. * M_PI * rand() / RAND_MAX);
        AirParcelHelper::distribute_to_conserved(this, k, j, i, air);
        pthermo->Extrapolate(&air, pcoord->dx1f(i),
                             Thermodynamics::Method::PseudoAdiabat, grav,
                             1.e-5);
      }
      ...
      pimpl->prad->CalFlux(this, k, j, is, ie + 1);
    }
}

The ProblemGenerator function is a member function of the MeshBlock, which is a high-level manager class that manages the data and the operations on a mesh grid. The ProblemGenerator function is automatically called inside

pmesh->Initialize(restart, pinput);

function of the Mesh class (see the code snippet in the TestImpl class).

Inside the body of the ProblemGenerator function, the initial condition is constructed by setting the conserved variables of each cell. The AirParcelHelper::distribute_to_conserved function converts the primitive variables to the conserved variables. The pthermo->Extrapolate function extrapolates the primitive variables to another level along a pseudo-adiabat. At the end, the pimpl->prad->CalFlux function calculates the radiation fluxes from level is to ie + 1 at the k-th and j-th cell.

Similar to the unit tests, the problem generator is compiled by adding the following line to the CMakeLists.txt file when building the executables of the Canoe:

setup_problem(<problem>)

where <problem> is the name of the problem generator file without the .cpp extension.