-
Notifications
You must be signed in to change notification settings - Fork 4
Cmake tutorial exercise
module load cmake
module load git
First, update your repo using git fetch
to get the new branches we've prepared, then checkout the CMake version of the code, you can check the diff between the master and cmake_master to see that only a few changes exist (if you are curious)
cd /path/to/SummerSchool2013/SWE
git fetch origin
git co –t origin/master_cmake
git submodule update
we need to update submodules after changing branches, because the swe_solvers subdirectory also has its own cmake branch which we want to use.
Unfortunately, the default cray wrappers for the gnu modules don't work well with CMake, so we will skip the compiler wrappers and tell CMake that we want to use the native gcc toolchain. (Remember that the cray has compute nodes and login nodes, generally you want to compile on the login nodes, but run the code on the compute nodes, they might be differnt, so cross-compilation is setup by the wrappers).
export CXX=g++
export CC=gcc
export FC=gfortran
CMake will pick up these environment variables and use them to set the default compiler. If you use CMake again later in the course, you might want to remember these commands.
If you receive a message like
CMake Error at /project/csvis/biddisco/src/cmake/Modules/CMakeTestCCompiler.cmake:61 (message):
The C compiler "/opt/cray/xt-asyncpe/5.13.01/bin/cc" is not able to compile
a simple test program.
then the problem is that the compiler is confused by the cray wrappers and it's best to skip them.
Create a build directory. Do not build directly inside the source directory, in general putting build files inside of a tree which is under source control leads to pain and misery in the long term.
cd /path/to/SummerSchool2013
mkdir build
cd build
Now we have a build directory outside of the source tree and we're ready to run cmake.
ccmake /path/to/SummerSchool2013/SWE
You should see a simple text based GUI where you can make edits. Initially, you only need to configure (once or more) until there are no unset variables, then generate. Press 't' to see advanced, hidden options.
Run make and hopefully you'll have a complied binary, try it out using
bin/swe_simple -x 64 -y 64 -o ./test
Hopefully, you have a working executable. It will generate output in the current directory with filename prefix test_
. Play with params and see what happens. Don't make X,Y too large as it will take a long time to run and possibly consume too much memory (remember that all students are using the same node).
Edit your CMakeLists.txt
with options to enable testing and adding the test subdirectory:
include(CTest)
if (BUILD_TESTING)
enable_testing()
add_subdirectory(test)
endif(BUILD_TESTING)
and in the test directory create a simple CMakeLists.txt file with
add_executable(test_simple test.c)
add_test(
NAME SimpleTest
COMMAND $<TARGET_FILE:test_simple>
)
Create a simple test.c
file in a test subdirectory with only an int main that returns 0 or 1 (randomly if you like). Your test can be as simple as this, because we're only testing our ability to write a test, not actually test something!
int main()
{
return 0;
}
Make sure you are in the build directory and try your test by using ctest
(remember 1 is fail, 0 is pass)
In general, users of your code will use
make test
If you want to see what is happening, try enabling verbose output
ctest -V
The swe_solvers already supports netCDF output, but to enable it, we need to do several things
- Enable the netCDF output using a compile definition
- Add the include path to our compile flags
- link the netCDF library to our target
If we are using the Cray wrappers (we're not), then finding packages is easy, you just do nothing, the wrappers add the include path and link flags to your make file automatically. To work in a more general environment, one needs to use a find_package(...)
construct.
Consider the following CMake snippet, which you must add to your CMakeLists.txt, before the section where SWE_SRCS is defined (and before the add_subdirectory(test)
section as we'll need a netCDF test later).
#--------------------------------------------------------------------------
# Select the output writer
#--------------------------------------------------------------------------
OPTION(ENABLE_NETCDF "Enable NetCDF results writer" OFF)
if(ENABLE_NETCDF)
LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
FIND_PACKAGE(NetCDF REQUIRED)
add_definitions(-D"WRITENETCDF")
include_directories(${NETCDF_INCLUDES})
set(SWE_NETCDF_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/src/writer/NetCdfWriter.cpp)
endif(ENABLE_NETCDF)
In order to find_package(netcdf) we must load a special module to locate the netcdf package, unfortunately one is not provided by default in this version of cmake, so we set a module path to tell cmake to find one and add the find module into our source repository in a cmake directory (we've added this FindNetCDF.cmake
file for you already, but feel free to look at it to see what it does).
Once you have enabled NetCDF by setting ENABLE_NETCDF ON
in ccmake, the find module does not know where to look at first, so set the following variables to these values (they may not be visible initially, so you must press 't' to see advanced options).
NETCDF_INCLUDES /opt/cray/netcdf/4.3.0/GNU/47/include
NETCDF_LIBRARIES /opt/cray/netcdf/4.3.0/GNU/47/lib/libnetcdf.so
Top Tip : To find the correct include paths and libraries, try
module show netcdf
to list out what the module is adding to the paths for the compiler wrappers.
We also need to add the netCDF writer to the build, so change the source files section to add the ${SWE_NETCDF_SRCS}
set(SWE_SRCS
src/blocks/SWE_Block.cpp
src/blocks/SWE_WavePropagationBlock.cpp
src/writer/VtkWriter.cpp
src/tools/Logger.cpp
src/examples/swe_simple.cpp
${WIN32_SRCS}
${SWE_NETCDF_SRCS}
)
Now configure (press 'c') and generate (press 'g') with netCDF enabled. If you try to build the project now, it should compile ok, but fail to link, try it ...
make
TO make it link, we need to add the netCDF libarary to the link like this
#--------------------------------------------------------------------------
# simple non mpi example
#--------------------------------------------------------------------------
add_executable(swe_simple
${SWE_SRCS}
)
target_link_libraries(swe_simple ${NETCDF_LIBRARIES})
make
Now it should compile and link and we will get a working binary
bin/swe_simple -x 64 -y 64 -o ./test
This time, the output will be a netCDF file with the name test_00.nc
The following excercise is quite complex, we wish to run the solver, generate an output file using netCDF, and then read the netCDF file back in and check that it is what we expect. We won't test the physics of the solver, just a simple check to see if the contents of the netCDF file are the right size, so we can say this is an IO test for the netCDF generation.
Ctest won’t let us run two programs in a single test (i.e. a chained series of commands, such as run the solver, then run a test), but it will allow us to run a script - and we can put multiple commands into the script. To solve cross platform compatibility we don't want to use a (linux), bash or (windows) bat, or other script. We will use a CMake script instead and make that the test.
You'll need to create a file called netcdf_test.cpp
in the test directory, put the following code into it.
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <netcdf.h>
/* Handle errors by printing an error message and exiting with a
* non-zero status. */
#define ERRCODE 2
#define ERR(e) {printf("Error: %s\n", nc_strerror(e)); exit(ERRCODE);}
int
main(int argc, char *argv[])
{
using namespace std;
/* This will be the netCDF ID for the file and data variable. */
int ncid, varid, dimid;
int ndims_in, nvars_in, ngatts_in, unlimdimid_in;
/* Loop indexes, and error handling. */
int x, y, retval ;
size_t x_val, y_val ;
if (argc <= 3)
{
std::cout << "Usage: " << argv[0] << " <Filename> <x_dim> <y_dim>" << std::endl;
exit(1);
}
char *pFilename = argv[1];
int x_true = atoi(argv[2]);
int y_true = atoi(argv[3]);
cout << "Processing: " << pFilename << " x " << x_true << " y " << y_true << endl;
/* Open the file. NC_NOWRITE tells netCDF we want read-only access
* to the file.*/
if ((retval = nc_open(pFilename, NC_NOWRITE, &ncid)))
ERR(retval);
/* There are a number of inquiry functions in netCDF which can be
used to learn about an unknown netCDF file. NC_INQ tells how
many netCDF variables, dimensions, and global attributes are in
the file; also the dimension id of the unlimited dimension, if
there is one. */
if ((retval = nc_inq(ncid, &ndims_in, &nvars_in, &ngatts_in, &unlimdimid_in)))
ERR(retval);
// cout << "ndims " << ndims_in << "nvars " << nvars_in << endl;
/* Get the varid of the data variable, based on its name. */
if ((retval = nc_inq_dimid(ncid, "x", &dimid)))
ERR(retval);
if ((retval = nc_inq_dimlen(ncid, dimid, &x_val )))
ERR(retval);
if ((retval = nc_inq_dimid(ncid, "y", &dimid)))
ERR(retval);
if ((retval = nc_inq_dimlen(ncid, dimid, &y_val )))
ERR(retval);
// cout << "x " << x_val << "y " << y_val << endl;
/* Close the file, freeing all resources. */
if ((retval = nc_close(ncid)))
ERR(retval);
retval = !((x_true == x_val)&&(y_true==y_val));
cout << "retval " << retval << endl;
return retval;
}
This is the code which opens the file, reads a variable/dataset and find the size and compares it to the expected size (which depends on the grid sizes we pass in as params).
Add the executable generation to the test/CMakeLists.txt
file
#--------------------------------------------------------------------------
# complex test to try netCDF check
#--------------------------------------------------------------------------
add_executable(test_netcdf netcdf_test.cpp)
target_link_libraries(test_netcdf ${NETCDF_LIBRARIES})
We need to drive our test using a script, be careful that you get file names consistent when ytou create these files.
create a file called run_test.cmake
in your test subdirectory with these contents
macro(EXEC_CHECK CMD A1 A2 A3)
execute_process(COMMAND ${CMD} ${A1} ${A2} ${A3} RESULT_VARIABLE CMD_RESULT)
if(CMD_RESULT)
message("Result was ${CMD_RESULT}")
message(FATAL_ERROR "Error running ${CMD}")
endif()
endmacro()
EXEC_CHECK(${CMD1} ${ARG1} ${ARG2} ${ARG3})
EXEC_CHECK(${CMD2} ${ARG4} ${ARG5} ${ARG6})
What does this script do? It creates macro, which executes a process (CMD) with 3 args and then checks the return value of the process and throws an error if it is 1.
We simply call this macro twice, once with the solver, once with the test program which checks the output.
Add the following code to your test/CMakeLists.txt
just after the declaration of the add_executable
containing the netcdf_test
set(X_SIZE 64)
set(Y_SIZE 64)
add_test(NAME NetCDF_IO_TEST
COMMAND ${CMAKE_COMMAND}
-DCMD1=$<TARGET_FILE:swe_simple>
-DARG1=--grid-size-x=${X_SIZE}
-DARG2=--grid-size-y=${Y_SIZE}
-DARG3=--output-basepath=${CMAKE_CURRENT_BINARY_DIR}/temp
-DCMD2=$<TARGET_FILE:test_netcdf>
-DARG4=${CMAKE_CURRENT_BINARY_DIR}/temp_00.nc
-DARG5=${X_SIZE}
-DARG6=${Y_SIZE}
-P ${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake
)
What is happening here? WE declare that our test size will be 64x64, and we add a test where the command to run is essentially setting up this
cmake "solver 64 64 out test out 64 64" -P run_test.cmake
because we can't pass arguments to the run_test.cmake
script itself, we instead create CMake variables which are passed in instead.
Once you've compiled everything and are happy that it works, try
make test
hopefully your tests pass.
try
ctest -V
You can make your test fail, by changing the values of these params passed to the test part
-DARG5=${X_SIZE}
-DARG6=${Y_SIZE}
to (for example)
-DARG5=${X_SIZE}
-DARG6=65
now we are generating data with a grid of 64x64, but testing against a grid of 64x65. We expect or test to fail. Try it!
For added bonus points, change the
set(X_SIZE 64)
set(Y_SIZE 64)
to be user configured options using
set(X_SIZE 64 CACHE STRING "User X grid size")
and now you can manually change the grid size and rerun the tests using different sizes (remember to configure, generate, then make when you change params).