Friday, October 29, 2021

[SOLVED] How to compile a subset of all targets in a CMake-generated Makefile that start with a given prefix in a bash for loop

Issue

I want to write a shell script that will only compile and execute the unit test binaries in my project. All targets in the Makefile (generated by CMake) associated with unit tests begin with UnitTest, e.g. UnitTestArray. How can I write a for loop in the shell script to individually compile all unit test binaries? I know how to sequentially run the pre-compiled binaries in a loop. It's just the compilation part that I am uncertain about. See my shell script below.

WORKDIR=$PWD
BUILD=~/Dropbox/Projects/Apeiron/build
BIN=$BUILD/bin

# Compile test binaries
cd $BUILD
for target in [UnitTest* in Makefile] # I'm not sure how to write this for loop.
do
  echo ""
  echo "Compiling $target" 
  make -j 8 $target 
done

# Run tests
cd $BIN
for test in UnitTest*
do
  echo ""
  echo "Running $test" 
  ./$test 
done

cd $WORKDIR

The following are some example target rules in the generated Makefile:

#=============================================================================
# Target rules for targets named UnitTestApeiron

# Build rule for target.
UnitTestApeiron: cmake_check_build_system
    $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 UnitTestApeiron
.PHONY : UnitTestApeiron

# fast build rule for target.
UnitTestApeiron/fast:
    $(MAKE) $(MAKESILENT) -f CMakeFiles/UnitTestApeiron.dir/build.make CMakeFiles/UnitTestApeiron.dir/build
.PHONY : UnitTestApeiron/fast

#=============================================================================
# Target rules for targets named UnitTestArray

# Build rule for target.
UnitTestArray: cmake_check_build_system
    $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 UnitTestArray
.PHONY : UnitTestArray

# fast build rule for target.
UnitTestArray/fast:
    $(MAKE) $(MAKESILENT) -f CMakeFiles/UnitTestArray.dir/build.make CMakeFiles/UnitTestArray.dir/build
.PHONY : UnitTestArray/fast

Solution

I resorted to the solutions suggested by users @MadScientist and @Saboteur in the above comments. The following is the modification of my shell script using @Saboteur's solution:

WORKDIR=$PWD
BUILD=~/Dropbox/Projects/Apeiron/build
BIN=$BUILD/bin

# Compile test binaries
cd $BUILD
declare -a TargetList=$(grep "cmake_check_build_system" $BUILD/Makefile|cut -d":" -f1)
for target in ${TargetList[@]}; do
  # Check if target keyword contains the substring "UnitTest"
  if grep -q "UnitTest" <<< "$target"; then
     printf "\nCompiling $target\n" 
     make -j 16 $target
  fi
done

# Run tests
cd $BIN
for test in UnitTest*
do
  printf "\nRunning $test\n" 
  ./$test 
done

cd $WORKDIR

This yields the following output:

$ ./unit_test.sh 
Compiling UnitTestArray
[ 16%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
[ 33%] Linking CXX static library ../../../lib/libgtest.a
[ 33%] Built target gtest
[ 50%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
[ 66%] Linking CXX static library ../../../lib/libgtest_main.a
[ 66%] Built target gtest_main
[ 83%] Building CXX object CMakeFiles/UnitTestArray.dir/libs/DataContainers/test/UnitTestArray.cpp.o
[100%] Linking CXX executable bin/UnitTestArray
[100%] Built target UnitTestArray

Compiling UnitTestApeiron
Consolidate compiler generated dependencies of target gtest
[ 33%] Built target gtest
Consolidate compiler generated dependencies of target gtest_main
[ 66%] Built target gtest_main
[ 83%] Building CXX object CMakeFiles/UnitTestApeiron.dir/test/UnitTestApeiron.cpp.o
[100%] Linking CXX executable bin/UnitTestApeiron
[100%] Built target UnitTestApeiron

Running UnitTestApeiron
Running main() from /home/niran90/Dropbox/Projects/Apeiron/build/_deps/googletest-src/googletest/src/gtest_main.cc
[==========] Running 15 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 15 tests from ApeironTest
[ RUN      ] ApeironTest.Min
[       OK ] ApeironTest.Min (0 ms)
[ RUN      ] ApeironTest.Max
[       OK ] ApeironTest.Max (0 ms)
[ RUN      ] ApeironTest.MinMax
[       OK ] ApeironTest.MinMax (0 ms)
[ RUN      ] ApeironTest.Bound
[       OK ] ApeironTest.Bound (0 ms)
[ RUN      ] ApeironTest.Sgn
[       OK ] ApeironTest.Sgn (0 ms)
[ RUN      ] ApeironTest.Abs
[       OK ] ApeironTest.Abs (0 ms)
[ RUN      ] ApeironTest.Floor
[       OK ] ApeironTest.Floor (0 ms)
[ RUN      ] ApeironTest.Ceil
[       OK ] ApeironTest.Ceil (0 ms)
[ RUN      ] ApeironTest.Round
[       OK ] ApeironTest.Round (0 ms)
[ RUN      ] ApeironTest.isEqual
[       OK ] ApeironTest.isEqual (0 ms)
[ RUN      ] ApeironTest.isLess
[       OK ] ApeironTest.isLess (0 ms)
[ RUN      ] ApeironTest.isLessEqual
[       OK ] ApeironTest.isLessEqual (0 ms)
[ RUN      ] ApeironTest.isLarger
[       OK ] ApeironTest.isLarger (0 ms)
[ RUN      ] ApeironTest.isLargerEqual
[       OK ] ApeironTest.isLargerEqual (1 ms)
[ RUN      ] ApeironTest.isBounded
[       OK ] ApeironTest.isBounded (0 ms)
[----------] 15 tests from ApeironTest (1 ms total)

[----------] Global test environment tear-down
[==========] 15 tests from 1 test suite ran. (1 ms total)
[  PASSED  ] 15 tests.

Running UnitTestArray
Running main() from /home/niran90/Dropbox/Projects/Apeiron/build/_deps/googletest-src/googletest/src/gtest_main.cc
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from ArrayTest
[ RUN      ] ArrayTest.Initialisation
[       OK ] ArrayTest.Initialisation (0 ms)
[ RUN      ] ArrayTest.AssignmentOperator
[       OK ] ArrayTest.AssignmentOperator (0 ms)
[----------] 2 tests from ArrayTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 2 tests.

I can see @MadScientist's suggestion to use CTest being very useful when the number of gtest unit tests is quite large. This is because CTest provides a concise summary of the passed/failed tests. I had to configure my project CMakeLists.txt as follows:

enable_testing()

# Fetch GoogleTest directories
include(FetchContent)
FetchContent_Declare(GoogleTest URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip)
FetchContent_MakeAvailable(GoogleTest)

# Add unit test executables
add_executable(UnitTestApeiron ${PROJECT_SOURCE_DIR}/test/UnitTestApeiron.cpp)
add_executable(UnitTestArray ${PROJECT_SOURCE_DIR}/libs/DataContainers/test/UnitTestArray.cpp)

# Link with gtest, gtest_main, and associated libraries.
target_link_libraries(UnitTestApeiron gtest gtest_main)
target_link_libraries(UnitTestArray gtest gtest_main)

add_test(UnitTestApeiron ${BIN_DIRECTORY}/UnitTestApeiron)
add_test(UnitTestArray ${BIN_DIRECTORY}/UnitTestArray)

Then, simply running ctest in the project build directory gives:

$ ctest 
Test project /home/niran90/Dropbox/Projects/Apeiron/build
    Start 1: UnitTestApeiron
1/2 Test #1: UnitTestApeiron ..................   Passed    0.01 sec
    Start 2: UnitTestArray
2/2 Test #2: UnitTestArray ....................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 2

Total Test time (real) =   0.01 sec


Answered By - niran90