CMake - 作为构建过程的一部分运行测试并将stdout输出捕获到文件

时间:2016-08-23 15:28:02

标签: unit-testing cmake

我们有几个单元测试,我们希望将其作为构建过程的一部分运行。

为了实现这一点,我有一个帮助脚本,它创建一个运行测试的自定义命令,如果成功,则创建一个文件"test_name.passed"

然后我添加了一个自定义目标"test_name.run",该目标取决于"test_name.passed"

我们的想法是,如果"test_name.passed"不存在或者早于"test_name",则会运行自定义命令。

构建将继续运行自定义命令,直到测试通过。一旦通过,后续版本就不会调用自定义命令,因此测试不会在不需要的情况下运行。

到目前为止,这一切都与所描述的完全相同

这是脚本:

# create command which runs the test and creates a sentinel file if it passes
add_custom_command(
    OUTPUT  ${TEST_NAME}.passed
    COMMAND $<TARGET_FILE:${TEST_NAME}>
    COMMAND ${CMAKE_COMMAND} -E touch ${TEST_NAME}.passed
    DEPENDS ${TEST_NAME}
    )

# create test.run module which depends on test.passed
add_custom_target(${TEST_NAME}.run
    ALL
    DEPENDS ${TEST_NAME}.passed
    )

问题 - stdout

上的噪音

问题在于我们的测试通常会将大量信息记录到stdout,这会造成非常嘈杂的构建。

我现在尝试将stdout捕获到文件中,并且仅在发生故障时显示测试输出。

我的第一次尝试是尝试使用Bash shell脚本语法 - 将stdout捕获到文件中,当退出状态为错误时,请抓住该文件。

add_custom_command(
    OUTPUT  ${TEST_NAME}.passed
    COMMAND $<TARGET_FILE:${TEST_NAME}> > ${TEST_NAME}.output || cat ${TEST_NAME}.output
    COMMAND ${CMAKE_COMMAND} -E touch ${TEST_NAME}.passed
    DEPENDS ${TEST_NAME}
    )

这不起作用,因为即使测试失败,我也会获得创建的sentinal "test_name.passed"文件,这意味着下次我尝试构建它时认​​为测试通过了。

可能的不合标准修复

通过与ctest集成,我可以通过ctest运行每个测试并使用命令行选项--output-on-failure

add_custom_command(
    OUTPUT  ${TEST_NAME}.passed
    COMMAND ctest --build-config $<CONFIGURATION> --tests-regex ${TEST_NAME} --output-on-failure
    COMMAND ${CMAKE_COMMAND} -E touch ${TEST_NAME}.passed
    DEPENDS ${TEST_NAME}
    )

这方面的问题有两方面。

  1. 大大增加了构建时间。每个测试都必须通过一个单独的ctest进程执行,所有注册的测试名称都要根据正则表达式进行解析等。随着单个测试的数量,我们有额外的时间相加。
  2. ctest默认输出很多噪音。指定--quiet标志会抑制--output-on-failure标志,因此您可以输出嘈杂或无输出 - 无法仅获取故障。
  3. 问题

    有没有办法实现我想要的?

    即:

    • 手动运行测试(即:不通过ctest)
    • 将输出捕获到文件
    • 仅在测试退出状态指示失败时输出该文件。
    • 如果测试退出状态表示成功,请触摸sentinel文件。

    跨平台方法的奖励积分,但如果它必须只是Linux,那就这样吧。

2 个答案:

答案 0 :(得分:1)

问题在于没有标准方法将输出从通过add_custom_command调用的命令重定向到文件。然而,CMake命令execute_process确实具有该功能。

因此,一种可能的解决方案是从配置的CMake脚本运行测试可执行文件,该脚本本身作为CMake自定义命令运行。以下代码概述了必要的步骤:

在添加测试的CMakeLists.txt中,配置CMake脚本模板:

configure_file("test_runner.cmake.in" "test_runner_${TEST_NAME}.cmake" @ONLY)

然后添加一个自定义命令以在构建时调用脚本:

add_custom_command(
    OUTPUT  ${TEST_NAME}.passed
    COMMAND ${CMAKE_COMMAND} -P "test_runner_${TEST_NAME}.cmake" $<TARGET_FILE:${TEST_NAME}>
    DEPENDS ${TEST_NAME}
    WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")

测试可执行文件的实际路径通过生成器表达式作为参数传递给脚本。

模板测试运行器脚本test_runner.cmake.in使用execute_process运行测试可执行文件,并将错误输出重定向到日志文件:

set (_testExecutable "${CMAKE_ARGV3}")
execute_process(COMMAND ${_testExecutable} ERROR_FILE "@TEST_NAME@.output" RESULT_VARIABLE _testResult)
if (_testResult)
    file(REMOVE "@TEST_NAME@.passed")
    file(READ "@TEST_NAME@.output" _contents)
    message (STATUS "${_contents}")
else()
    file(WRITE "@TEST_NAME@.passed" "")
endif()

如果测试失败,脚本将删除sentinel文件并输出错误日志。 如果测试成功,脚本将创建sentinel文件。

答案 1 :(得分:0)

回答我自己的问题,因为我找到了适合我的解决方案。

在下面的摘录中,${ARG_NAME}是我们构建并想要运行的测试二进制文件。

运行测试时创建了2个文件

  • ${ARG_NAME}.output包含测试输出
  • ${ARG_NAME}.passed,如果测试成功,则创建一个标记文件

custom_command由几个命令组成,我们依赖于cmake会在其中一个命令失败后立即停止执行的事实。

我们进行测试:

${ARG_NAME}

所有测试输出都重定向到${ARG_NAME}.output,因此测试通过时我们不会污染标准输出

${ARG_NAME} >> ${OUTPUT_FILE} 2>&1

使用测试的退出状态,如果发生故障,我们会捕获文件:

${ARG_NAME} >> ${OUTPUT_FILE} 2>&1 || cat ${OUTPUT_FILE}

但是,只有在测试通过时我们才会创建sentinel文件,所以我们捕获文件并运行false

${ARG_NAME} >> ${OUTPUT_FILE} 2>&1 || (cat ${OUTPUT_FILE} && false)

如果测试通过(退出状态为0),那么我们将不会运行cat output && false,因此运行下一个cmake命令

${CMAKE_COMMAND} -E touch ${PASSED_FILE}

最后,我们创建一个custom_target,它取决于.passed文件

set(OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}.output)
set(PASSED_FILE ${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}.passed)

# create test.passed command which runs this test and creates a sentinel file
# if it passes
add_custom_command(
    OUTPUT
        ${PASSED_FILE}

    COMMAND
        ${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME} >> ${OUTPUT_FILE} 2>&1 
              || (cat ${OUTPUT_FILE} && false)

    COMMAND
        ${CMAKE_COMMAND} -E touch ${PASSED_FILE}

    COMMENT
        "Running ${ARG_NAME} tests"

    DEPENDS
        ${ARG_NAME}

    USES_TERMINAL
    )

# create test.run target which depends on test.passed
add_custom_target(${ARG_NAME}.run
    ALL
    DEPENDS ${PASSED_FILE}
    )