C++でRcppを使ったユニットテストを書く

RからC++を使うためにRcppがありますが、ユニットテストを書くためにC++のコードからRcppを使う必要があったので、できるようにしました。

追記: Rcppを用いたC++コードをテストするには、 [testthat::use_catch()](https://testthat.r-lib.org/reference/use_catch.html) が最善の方法だと思います。それとは別に、下記のようなこともできるという紹介です。

## 動機

Rcppを使ってC++のコードを書くときは、Rのデータ構造をC++のデータ構造として扱います。例えばRのコードから数値のベクトルを、 `Rcpp::NumericVector` としてC++のコードに渡します。

Rcppを用いて書いたC++のコードをRパッケージにするとき、C++のコードをC++のユニットテストフレームワーク(例えばGoogle Test)でテストしたいです。ですが単に `#include <Rcpp.h>` してコンパイル、リンクして実行ファイルを作っても動きません。他にも必要なことがあるので調べました。

本文中で、テスト対象のパッケージ名を `anRcppSample` とします。全コードは [こちらのレポジトリ](https://github.com/zettsu-t/rcppSample) にあります。

## 概要

C++のコードをコンパイル、リンクしてできる実行ファイルに `libR.so` をリンクして、Rを起動すればよいです。起動するとRのプロンプトが出るので、`library(anRcppSample)` と改行を入力して、テスト対象のパッケージ `anRcppSample` をロードします。R の REPL から抜けてC++コードに制御が戻るので、C++のユニットテストができます。ですがプロンプトに手作業で入力するとテストを自動化できないので、R処理系に自動的にコードを流し込むようにします。

## 作成するパッケージ

### ファイル構成

sum関数相当の処理をC++で行うRパッケージを作ります。エラー処理などは本記事の目的には必要ないので省略しています。

|ディレクトリ|ファイル名|内容|
|:----------------------|:----------------------|:----------------------------------------------------------------|
|R/|sample.R|パッケージの実装(R)|
|src/|sample.h, sample.cpp|パッケージの実装(C++)|
|tests/testthat/|test-sample.R|パッケージのテスト(R)|
|tests/|test_sample.cpp|パッケージのテスト(C++)|
|tests/|CMakeLists.txt, CMakeLists.txt.in|C++のユニットテストをビルドする設定|

### パッケージの実装

Rのsum関数に相当する処理をC++で実装して、Rパッケージとして使えるようにします。

```r:sample.R
#' Sum in C++
#'
#' @param xs A numeric vector to sum
#' @return The sum of xs
#'
#' @export
#' @useDynLib anRcppSample, .registration=TRUE
#' @importFrom Rcpp sourceCpp
sample_sum_r <- function(xs) {
  sample_sum_cpp(xs)
}
```

```cpp:sample.h
#ifndef SRC_SAMPLE_H
#define SRC_SAMPLE_H

#include <Rcpp.h>

//' Sum in C++
//'
//' @param xs A numeric vector to sum
//' @return The sum of xs
// [[Rcpp::export]]
extern double sample_sum_cpp(const Rcpp::NumericVector& xs);

#endif // SRC_SAMPLE_H
```

```cpp:sample.cpp
#include "sample.h"
#include <numeric>

double sample_sum_cpp(const Rcpp::NumericVector& xs) {
    constexpr double init = 0.0;
    return std::accumulate(xs.begin(), xs.end(), init);
}
```

Rのコードは、このようにテストを書きます。

```r:test-sample.R
test_that("A small test", {
  expect_equal(sample_sum_r(c(1.0, 0.5, 0.25)), 1.75)
})
```

## C++コードのユニットテスト

### テストを記述する

インクルード文とmain文をこのように書きます。

```cpp:test_sample.cpp
#include "sample.h"
#include <cstring>
#include <gtest/gtest.h>
#define R_INTERFACE_PTRS
#include <Rembedded.h>
#include <Rinterface.h>

int main(int argc, char *argv[]) {
    char name[] = "test_sample";
    char arg1[] = "--no-save";
    char *args[]{name, arg1, nullptr};
    Rf_initEmbeddedR((sizeof(args) / sizeof(args[0])) - 1, args);
    ptr_R_ReadConsole = custom_r_readconsole;
    R_ReplDLLinit();
    R_ReplDLLdo1();

    ::testing::InitGoogleTest(&argc, argv);
    auto result = RUN_ALL_TESTS();
    Rf_endEmbeddedR(0);
    return result;
}
```

1. `Rf_initEmbeddedR` でRを起動します。このとき引数 `argc`, `argv` を渡します。最初の要素はおそらくプログラム名なので何でもよいでしょう、二番目以降の要素はRに渡す引数です。引数として `--no-save` もしくはそれに代わるオプションが必須です。
1. `ptr_R_ReadConsole` はグローバル変数で、Rのプロンプトに入力する文字列を、R処理系が必要とするときに呼び出すコールバック関数へのポインタです。後で説明しますが、Rのコードを流し込むコードを返す関数 `custom_r_readconsole` を登録します。
1. `R_ReplDLLinit` でRのREPLを初期化し、`R_ReplDLLdo1` でREPLを起動します。`ptr_R_ReadConsole` に何も登録しなければRのREPLが起動してプロンプトが出ますので、試すとよいでしょう。
1. Rのプロンプトに入力した処理が終了すると続きを実行します。ここではGoogle Testでユニットテストを実行します。

インクルードファイルは、`Rembedded.h` と `Rinterface.h` が必要です。インクルードする前に `#define R_INTERFACE_PTRS` が必要です。このマクロがないと、コールバック関数を設定できません。

次に、Rのコードを流し込むコールバック関数 `custom_r_readconsole` を定義します。

```cpp
namespace {
int custom_r_readconsole(const char *prompt, unsigned char *buf, int buflen, int hist) {
    constexpr int return_code = 1;
    const std::string r_code{"library(anRcppSample)\n"};
    const auto r_code_buf_size = r_code.size() + 1;

    if (!buf || !buflen) {
        return return_code;
    }
    *buf = '\0';

    if (static_cast<decltype(r_code_buf_size)>(buflen) < r_code_buf_size) {
        return return_code;
    }

    std::strncpy(reinterpret_cast<char*>(buf), r_code.c_str(), r_code_buf_size);
    return return_code;
}
} // namespace
```

[Writing R Extensions](https://cran.r-project.org/doc/manuals/r-release/R-exts.html#Setting-R-callbacks) に従って、 `R_ReadConsole` の代わりになる関数を実装します。
+ `buf` が指す領域に、最大 buflen 文字 (bytes)のRコードを設定します。設定するコードは終端を `"\n\0"` にします。`buf` にコピーするのはC文字列なので、`std::string` の文字数 + 終端1文字をコピーします。
+ prompt と hist は、今回無視します
+ 返り値は入力が得られないときは0、得られるときは正の整数にします。ここでは1にします。

コードが `buflen` 以内に収まれば一まとまりのコードを1回で渡し、収まらなければ改行で区切って渡します。 `buflen` の大きさは実行時に分かりますが、手元で試したところ4096でした。今回はテスト対象のパッケージをロードするコード `library(anRcppSample)` だけ実行すればよいので1回で返せると想定します。

Gootle Testを使って、いつも通りユニットテストを書きます。

```cpp
class TestAll : public ::testing::Test {};
TEST_F(TestAll, Small) {
    const Rcpp::NumericVector arg {1.0, 0.5, 0.25};
    const auto actual = sample_sum_cpp(arg);
    EXPECT_EQ(1.75, actual);
}
```

### CMakeを使う

`libR.so` をリンクすることと、パスを設定する、という二点に注意します。それ以外は、Google TestをCMakeから使う方法について、他の記事をご参照ください。

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
configure_file(CMakeLists.txt.in googletest-download/CMakeLists.txt)

project(rCppSample CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")

# R_HOME should be set or the R runtime fails
set(R_ROOT_DIR "$ENV{R_HOME}")
set(RLIB "R")
find_path(R_INCLUDE_DIR R.h HINTS "${R_ROOT_DIR}" /usr/share /opt PATH_SUFFIXES include R/include)
find_library(R_LIBRARY NAMES "${RLIB}" HINTS "${R_ROOT_DIR}" PATH_SUFFIXES lib R/lib)

file(GLOB_RECURSE RCPP_INCLUDE_DIR "${R_ROOT_DIR}/*/Rcpp.h")
if(NOT RCPP_INCLUDE_DIR)
file(GLOB_RECURSE RCPP_INCLUDE_DIR "/home/*/Rcpp.h")
endif()

string(REGEX REPLACE "/Rcpp.h$" "" RCPP_INCLUDE_DIR "${RCPP_INCLUDE_DIR}")
find_path(RCPP_INCLUDE_DIR "Rcpp.h" HINTS "${RCPP_INCLUDE_DIR}")
include_directories("${R_INCLUDE_DIR}" "${RCPP_INCLUDE_DIR}")

execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . RESULT_VARIABLE result WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download)

if(result)
  message(FATAL_ERROR "CMake step for googletest failed: ${result}")
endif()

execute_process(COMMAND ${CMAKE_COMMAND} --build . RESULT_VARIABLE result WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download)

if(result)
  message(FATAL_ERROR "Build step for googletest failed: ${result}")
endif()

set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_CURRENT_BINARY_DIR}/googletest-src ${CMAKE_CURRENT_BINARY_DIR}/googletest-build EXCLUDE_FROM_ALL)

enable_testing()
include(GoogleTest)

# Include header files
set(BASEPATH "${CMAKE_SOURCE_DIR}")
include_directories("${BASEPATH}" "${BASEPATH}/../src")

# Executable unit tests
add_compile_options(-DSTRICT_R_HEADERS)
add_executable(test_sample ../src/sample.cpp test_sample.cpp)
target_link_libraries(test_sample "${R_LIBRARY}" gtest_main)
gtest_add_tests(TARGET test_sample)
```

```cmake:CMakeLists.txt.in
cmake_minimum_required(VERSION 3.10)
project(googletest-download NONE)

include(ExternalProject)
ExternalProject_Add(googletest
  GIT_REPOSITORY    https://github.com/google/googletest.git
  GIT_TAG           main
  GIT_SHALLOW       TRUE
  SOURCE_DIR        "${CMAKE_CURRENT_BINARY_DIR}/googletest-src"
  BINARY_DIR        "${CMAKE_CURRENT_BINARY_DIR}/googletest-build"
  CONFIGURE_COMMAND ""
  BUILD_COMMAND     ""
  INSTALL_COMMAND   ""
  TEST_COMMAND      ""
)
```

### テストを実行する

Rパッケージのトップディレクトリから、以下を実行します。環境変数 `R_HOME` を設定しないと、テストの実行に失敗します。RStudio などから実行する場合は `R_HOME` は設定済だと思いますが、Dockerfile や GitHub Actions では `R_HOME` を明示的に設定する必要があるでしょう。

```bash
mkdir -p tests/build
cd tests/build
cmake ..
make
make test
cd CMakeFiles/test_sample.dir
lcov -d . -c -o coverage.info
lcov -r coverage.info "/usr/*" "*/googletest/*" "/opt/boost*" -o coverageFiltered.info
genhtml -o lcovHtml --num-spaces 4 -s --legend coverageFiltered.info
cd ../../../..
```

make test が失敗するかいつまで経っても終了しない場合は、テストプログラム `test_sample` を直接起動します。Rパッケージのロードに失敗しているかもしれません。

```bash
./test_sample --output-on-failure
```