是否可以使用 Catch2 来测试 MPI 代码?

Is it possible to use Catch2 for testing an MPI code?

我正在处理相当大的 MPI 代码。我开始将单元测试包含到现有代码库中。但是,一旦被测单元使用 MPI 例程,测试可执行文件就会崩溃并显示错误消息 "calling an MPI routine before MPI_Init"

是的,有可能。

https://github.com/catchorg/Catch2/issues/566 中所述,您必须提供自定义主函数。

#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
#include <mpi.h>

int main( int argc, char* argv[] ) {
    MPI_Init(&argc, &argv);
    int result = Catch::Session().run( argc, argv );
    MPI_Finalize();
    return result;
}

要结合使用 Catch2 和 MPI 来增强您的体验,您可能希望避免冗余的控制台输出。这需要将一些代码注入 ConsoleReporter::testRunEnded of catch.hpp.

#include <mpi.h>
void ConsoleReporter::testRunEnded(TestRunStats const& _testRunStats) {
    int rank id = -1;
    MPI Comm rank(MPI COMM WORLD,&rank id);
    if(rank id != 0 && testRunStats.totals.testCases.allPassed())
        return;
    printTotalsDivider(_testRunStats.totals);
    printTotals(_testRunStats.totals);
    stream << std::endl;
    StreamingReporterBase::testRunEnded(_testRunStats);
}

最后,您可能还想使用不同数量的 MPI 等级来执行测试用例。我发现以下是一个简单且有效的解决方案:

SCENARIO("Sequential Testing", "[1rank]") {
    // Perform sequential tests here
}
SCENARIO("Parallel Testing", "[2ranks]") {
    // Perform parallel tests here
}

然后您可以使用

单独调用标签场景
mpiexec -1 ./application [1rank]
mpiexec -2 ./application [2rank]

对于希望在 运行 Catch2 分发时删除所有重复的控制台输出的任何人,这里有一个解决方案。

catch.hpp中找到ConsoleReporter的定义(在v2.10.0中,在第15896行)。它看起来像:

ConsoleReporter::ConsoleReporter(ReporterConfig const& config)
    : StreamingReporterBase(config),
    m_tablePrinter(new TablePrinter(config.stream(),
        [&config]() -> std::vector<ColumnInfo> {
        if (config.fullConfig()->benchmarkNoAnalysis())
        {
            return{
                { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left },
                { "     samples", 14, ColumnInfo::Right },
                { "  iterations", 14, ColumnInfo::Right },
                { "        mean", 14, ColumnInfo::Right }
            };
        }
        else
        {
            return{
                { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 32, ColumnInfo::Left },
                { "samples      mean       std dev", 14, ColumnInfo::Right },
                { "iterations   low mean   low std dev", 14, ColumnInfo::Right },
                { "estimated    high mean  high std dev", 14, ColumnInfo::Right }
            };
        }
    }())) {}
ConsoleReporter::~ConsoleReporter() = default;

虽然此处未显示,但 base-class StreamingReporterBase 提供了一个 stream 属性,我们将通过显示的 failbit 技巧禁用该属性 .

在上面的最终 {} 中(一个空的构造函数定义),插入:

// I solemnly swear that I am up to no good
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);

// silence non-root nodes
if (rank != 0)
    stream.setstate(std::ios_base::failbit);  

您可以在 this repo 上查看示例。

除了提供自定义主函数外,还可以通过强制每个 MPI 进程将其测试报告转储到单独的文件中来规避经常遇到的多个 MPI 进程的重复输出问题,例如进程 p 将其测试报告转储到 report_p.xml

一种快速而肮脏的方法是扩展命令行参数向量 argv,为进程相关的文件名添加额外条目,并与 --out.

配对

来源:

// mytests.cpp
#define CATCH_CONFIG_RUNNER

#include "catch.hpp"
#include <mpi.h>
#include <string>
#include <vector>

int 
main(int argc, char* argv[]) {
  MPI_Init(&argc, &argv);

  int mpi_rank;
  MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);

  // make space for two extra arguments
  std::vector<const char*> new_argv(argc + 2);
  for (int i = 0; i < argc; i++) {
    new_argv[i] = argv[i];
  }

  // set "--out report_p.xml" as last two arguments
  auto filename = "report_" + std::to_string(mpi_rank) + ".xml";
  new_argv[argc] = "--out";
  new_argv[argc+1] = filename.data();

  int result = Catch::Session().run(new_argv.size(), new_argv.data());
  MPI_Finalize();
  return result;
}

编译并运行:

mpic++ mytests.cpp -o mytests
mpirun -np 4 ./mytests --reporter junit

预期输出文件

report_0.xml
report_1.xml
report_2.xml
report_3.xml

MPI 使用 std::ostream 运行 处理写入同一文件的风险,即破坏 XML 格式、崩溃 JUnit XML 解析器和失败 CI 管道即使所有的测试都通过了。将每个进程的测试报告转储到它们自己的单独文件中可以避免此问题。如果需要,以后可以连接单独的文件。

即使上例中的额外参数已放在参数列表的末尾,它们也不会覆盖命令行中先前的任何 --out 键值。相反,内置的 CLI 解析器将其视为参数关键字的重复并抛出错误。因此,上述方法的更具包容性的实现不会扩展参数列表,而是将对应于 --out 值的 argv 指针替换为附加的文件名。

PS: and 一定认真研究了 Catch 源代码以找出智能代码注入,向他们致敬!

当所有测试都通过后,我只需要听一次(例如通过大师级别)。但我发现,如果测试失败,了解它在哪个级别失败以及如何失败仍然是有益的。我的解决方案不需要操作 catch.hpp 文件,而只需要自定义 main 看起来像这样:

#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
#include "mpiHelpers.hpp"
#include <sstream>

int main( int argc, char* argv[] ) {
    MPI_Init_H autoInit { argc, argv }; // calls MPI_Finalize() on destruction

    std::stringstream ss;

    /* save old buffer and redirect output to string stream */
    auto cout_buf = std::cout.rdbuf( ss.rdbuf() ); 

    int result = Catch::Session().run( argc, argv );

    /* reset buffer */
    std::cout.rdbuf( cout_buf );

    MPI_Comm_H world { MPI_COMM_WORLD };

    std::stringstream printRank;
    printRank << "Rank ";
    printRank.width(2);
    printRank << std::right << world.rank() << ":\n";

    for ( int i{1}; i<world.size(); ++i ){
        MPI_Barrier(world);
        if ( i == world.rank() ){
            /* if all tests are passed, it's enough if we hear that from 
             * the master. Otherwise, print results */
            if ( ss.str().rfind("All tests passed") == std::string::npos )
                std::cout << printRank.str() + ss.str();
        }
    }
    /* have master print last, because it's the one with the most assertions */
    MPI_Barrier(world);
    if ( world.isMaster() )
        std::cout << printRank.str() + ss.str();

    return result;
}

所以我只是将输出重定向到字符串流缓冲区。然后我可以稍后再决定是否打印它们。

MPI_Init_HMPI_Comm_H这两个辅助函数并不是必须的,你可以用标准的MPI_InitMPI_Comm来完成,但是对于完整性,它们在这里:

#ifndef MPI_HELPERS
#define MPI_HELPERS

#include <iostream>
#include <mpi.h>

class MPI_Init_H {
    public:
    /* constructor initialises MPI */
    MPI_Init_H( int argc, char* argv[] ){
        MPI_Init(&argc, &argv);
    }
    /* destructor finalises MPI */
    ~MPI_Init_H(){ MPI_Finalize(); }
};

/* content of mpiH_Comm.hpp */
class MPI_Comm_H
{
private:
    MPI_Comm m_comm;
    int m_size;
    int m_rank;

public:
    MPI_Comm_H( MPI_Comm comm = MPI_COMM_NULL ) :
        m_comm {comm}
    {
        if ( m_comm != MPI_COMM_NULL ){
            MPI_Comm_size(m_comm, &m_size);
            MPI_Comm_rank(m_comm, &m_rank);
        }
        else {
            m_size = 0;
            m_rank = -1;
        }
    }

    /* contextual conversion to bool, which returns true if m_comm is a valid
     * communicator and false if it is MPI_COMM_NULL */
    operator bool() const { return m_comm != MPI_COMM_NULL; }
    
    const MPI_Comm& comm() const {
        #ifndef NDEBUG
        if ( !(*this) )
            std::cerr
                << "WARNING: You called comm() on a null communicator!\n";
        #endif
        return m_comm;
    }

    int rank() const {
        return m_rank;
    }

    int size() const {
        assert( *this && "You called size() on a null communicator!");
        return m_size;
    }

    int master() const {
        assert( *this && "You called master() on a null communicator!");
        return 0;
    }

    bool isMaster() const { return rank() == 0; }

    /* allow conversion to MPI_Comm */
    operator const MPI_Comm&() const { return m_comm; }
};

#endif