Skip to content

Commit 5b75d67

Browse files
committed
twister: Add support for Cpputest
Similar to gTest, CppuTest is a CPP framework for unit tests. This commit adds support based on the console output of a cpputest test suite application. Signed-off-by: Victor Chavez <[email protected]>
1 parent ea29907 commit 5b75d67

File tree

9 files changed

+299
-3
lines changed

9 files changed

+299
-3
lines changed

doc/develop/test/twister.rst

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ harness: <string>
560560
- gtest
561561
- robot
562562
- ctest
563+
- cpputest
563564
- shell
564565
- power
565566

@@ -773,8 +774,8 @@ Most everyday users will run with no arguments.
773774
Harnesses
774775
*********
775776

776-
Harnesses ``ztest``, ``gtest`` and ``console`` are based on parsing of the
777-
output and matching certain phrases. ``ztest`` and ``gtest`` harnesses look
777+
Harnesses ``ztest``, ``gtest``, ``console`` and ``cpputest`` are based on parsing of the
778+
output and matching certain phrases. ``ztest``, ``gtest`` and ``cpputest`` harnesses look
778779
for pass/fail/etc. frames defined in those frameworks.
779780

780781
Some widely used harnesses that are not supported yet:
@@ -820,6 +821,25 @@ Gtest
820821
Use ``gtest`` harness if you've already got tests written in the gTest
821822
framework and do not wish to update them to zTest.
822823

824+
Cpputest
825+
========
826+
827+
``cpputest`` does not output information for each test case result by default. As this
828+
harness is based on parsing the output it is necessry to enable verbose within ``cpputest``:
829+
830+
.. code-block:: cpp
831+
832+
#include <CppUTest/CommandLineTestRunner.h>
833+
#include <posix_board_if.h>
834+
int main(void)
835+
{
836+
const char *cppu_test_args[] = {__FILE__, "-v"};
837+
int num_args = std::size(cppu_test_args);
838+
int test_Res = CommandLineTestRunner::RunAllTests(num_args, cppu_test_args);
839+
posix_exit(test_Res);
840+
return 0;
841+
}
842+
823843
Pytest
824844
======
825845

scripts/pylib/twister/twisterlib/harness.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,70 @@ def _parse_report_file(self, report):
11521152
else:
11531153
tc.status = TwisterStatus.PASS
11541154

1155+
class Cpputest(Harness):
1156+
TEST_START_PATTERN = r".*(?<!Failure in )TEST\((?P<suite_name>[^,]+), (?P<test_name>[^\)]+)\)"
1157+
TEST_FAIL_PATTERN = r".*Failure in TEST\((?P<suite_name>[^,]+), (?P<test_name>[^\)]+)\).*"
1158+
FINISHED_PATTERN = (
1159+
r".*(OK|Errors) \(\d+ tests, \d+ ran, \d+ checks, \d+ ignored,"
1160+
+ r" \d+ filtered out, \d+ ms\)"
1161+
)
1162+
def __init__(self):
1163+
super().__init__()
1164+
self.tc = None
1165+
self.has_failures = False
1166+
self.started = False
1167+
1168+
def handle(self, line):
1169+
if not self.started:
1170+
self.instance.testcases = []
1171+
self.started = True
1172+
if self.status != TwisterStatus.NONE:
1173+
return
1174+
1175+
if test_start_match := re.search(self.TEST_START_PATTERN, line):
1176+
# If a new test starts and there is an unfinished test, mark it as passed
1177+
if self.tc is not None:
1178+
self.tc.status = TwisterStatus.PASS
1179+
self.tc.output = self.testcase_output
1180+
self.testcase_output = ""
1181+
self.tc = None
1182+
suite_name = test_start_match.group("suite_name")
1183+
test_name = test_start_match.group("test_name")
1184+
if suite_name not in self.detected_suite_names:
1185+
self.detected_suite_names.append(suite_name)
1186+
name = f"{self.id}.{suite_name}.{test_name}"
1187+
tc = self.instance.get_case_by_name(name)
1188+
assert tc is None, f"CppUTest error, {name} running twice"
1189+
tc = self.instance.get_case_or_create(name)
1190+
self.tc = tc
1191+
self.tc.status = TwisterStatus.STARTED
1192+
self.testcase_output += line + "\n"
1193+
self._match = True
1194+
elif test_fail_match := re.search(self.TEST_FAIL_PATTERN, line):
1195+
suite_name = test_fail_match.group("suite_name")
1196+
test_name = test_fail_match.group("test_name")
1197+
name = f"{self.id}.{suite_name}.{test_name}"
1198+
tc = self.instance.get_case_by_name(name)
1199+
if tc is not None:
1200+
tc.status = TwisterStatus.FAIL
1201+
self.has_failures = True
1202+
tc.output = self.testcase_output
1203+
self.testcase_output = ""
1204+
self.tc = None
1205+
elif finished_match := re.search(self.FINISHED_PATTERN, line):
1206+
# No need to check result if previously there was a failure
1207+
# or no tests were run
1208+
if self.has_failures or self.tc is None:
1209+
return
1210+
tc = self.instance.get_case_or_create(self.tc.name)
1211+
finish_result = finished_match.group(1)
1212+
if finish_result == "OK":
1213+
self.status = TwisterStatus.PASS
1214+
tc.status = TwisterStatus.PASS
1215+
else:
1216+
self.status = TwisterStatus.FAIL
1217+
tc.status = TwisterStatus.FAIL
1218+
11551219
class HarnessImporter:
11561220

11571221
@staticmethod

scripts/pylib/twister/twisterlib/testinstance.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,10 @@ def testsuite_runnable(testsuite, fixtures):
227227
'power',
228228
'test',
229229
'gtest',
230+
'cpputest',
230231
'robot',
231232
'ctest',
232-
'shell'
233+
'shell',
233234
]:
234235
can_run = True
235236
# if we have a fixture that is also being supplied on the

scripts/tests/twister/test_harness.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
Bsim,
2323
Console,
2424
Gtest,
25+
Cpputest,
2526
Harness,
2627
HarnessImporter,
2728
Pytest,
@@ -54,6 +55,17 @@
5455
"[00:00:00.000,000] <inf> label: [----------] Global test environment tear-down"
5556
)
5657

58+
SAMPLE_CPPUTEST_NO_TESTS = (
59+
"Errors (ran nothing, 0 tests, 0 ran, 0 checks, 0 ignored, 0 filtered out, 0 ms)")
60+
SAMPLE_CPPUTEST_START_FMT = "[00:00:00.000,000] <inf> label: TEST({suite}, {test})"
61+
SAMPLE_CPPUTEST_END_PASS_FMT = "[00:00:00.000,000] <inf> label: OK ({tests} tests" \
62+
", {ran} ran, {checks} checks, {ignored} ignored," \
63+
" {filtered} filtered out, {time} ms)"
64+
SAMPLE_CPPUTEST_FAIL_FMT = "[00:00:00.000,000] <inf> label: Failure in TEST({suite}, {test})"
65+
SAMPLE_CPPUTEST_END_FAIL_FMT = "[00:00:00.000,000] <inf> label: Errors({failures} failures" \
66+
", {tests} tests, {ran} ran, {checks} checks, {ignored} ignored," \
67+
" {filtered} filtered out, {time} ms)"
68+
5769

5870
def process_logs(harness, logs):
5971
for line in logs:
@@ -1235,3 +1247,122 @@ def test_bsim_build(monkeypatch, tmp_path):
12351247
with open(new_exe_path, "r") as file:
12361248
exe_content = file.read()
12371249
assert "TEST_EXE" in exe_content
1250+
1251+
@pytest.fixture
1252+
def cpputest(tmp_path):
1253+
mock_platform = mock.Mock()
1254+
mock_platform.name = "mock_platform"
1255+
mock_platform.normalized_name = "mock_platform"
1256+
mock_testsuite = mock.Mock()
1257+
mock_testsuite.name = "mock_testsuite"
1258+
mock_testsuite.detailed_test_id = True
1259+
mock_testsuite.id = "id"
1260+
mock_testsuite.testcases = []
1261+
mock_testsuite.harness_config = {}
1262+
outdir = tmp_path / 'cpputest_out'
1263+
outdir.mkdir()
1264+
1265+
instance = TestInstance(
1266+
testsuite=mock_testsuite, platform=mock_platform, toolchain='zephyr', outdir=outdir
1267+
)
1268+
1269+
harness = Cpputest()
1270+
harness.configure(instance)
1271+
return harness
1272+
1273+
1274+
def test_cpputest_start_test_no_suites_detected(cpputest):
1275+
process_logs(cpputest, [SAMPLE_CPPUTEST_NO_TESTS])
1276+
assert len(cpputest.detected_suite_names) == 0
1277+
assert cpputest.status == TwisterStatus.NONE
1278+
1279+
1280+
def test_cpputest_start_test(cpputest):
1281+
process_logs(
1282+
cpputest,
1283+
[
1284+
SAMPLE_CPPUTEST_START_FMT.format(
1285+
suite="suite_name", test="test_name"
1286+
),
1287+
],
1288+
)
1289+
assert cpputest.status == TwisterStatus.NONE
1290+
assert len(cpputest.detected_suite_names) == 1
1291+
assert cpputest.detected_suite_names[0] == "suite_name"
1292+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") is not None
1293+
assert (
1294+
cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.STARTED
1295+
)
1296+
1297+
1298+
def test_cpputest_one_test_passed(cpputest):
1299+
process_logs(
1300+
cpputest,
1301+
[
1302+
SAMPLE_CPPUTEST_START_FMT.format(
1303+
suite="suite_name", test="test_name"
1304+
),
1305+
SAMPLE_CPPUTEST_END_PASS_FMT.format(
1306+
tests=1, ran=1, checks=5, ignored=0, filtered=0, time=10
1307+
)
1308+
],
1309+
)
1310+
assert len(cpputest.detected_suite_names) == 1
1311+
assert cpputest.detected_suite_names[0] == "suite_name"
1312+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") != TwisterStatus.NONE
1313+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.PASS
1314+
1315+
1316+
def test_cpputest_multiple_test_passed(cpputest):
1317+
logs = []
1318+
total_passed_tests = 5
1319+
for i in range(0, total_passed_tests):
1320+
logs.append(SAMPLE_CPPUTEST_START_FMT.format(suite="suite_name",
1321+
test=f"test_name_{i}"))
1322+
logs.append(SAMPLE_CPPUTEST_END_PASS_FMT.format(
1323+
tests=total_passed_tests, ran=total_passed_tests, checks=5, ignored=0, filtered=0, time=10
1324+
))
1325+
process_logs(cpputest, logs)
1326+
assert len(cpputest.detected_suite_names) == 1
1327+
assert cpputest.detected_suite_names[0] == "suite_name"
1328+
for i in range(0, total_passed_tests):
1329+
test_name = f"id.suite_name.test_name_{i}"
1330+
assert cpputest.instance.get_case_by_name(test_name) != TwisterStatus.NONE
1331+
assert cpputest.instance.get_case_by_name(test_name).status == TwisterStatus.PASS
1332+
1333+
1334+
def test_cpputest_test_failed(cpputest):
1335+
process_logs(
1336+
cpputest,
1337+
[
1338+
SAMPLE_CPPUTEST_START_FMT.format(
1339+
suite="suite_name", test="test_name"
1340+
),
1341+
SAMPLE_CPPUTEST_FAIL_FMT.format(
1342+
suite="suite_name", test="test_name"
1343+
)
1344+
],
1345+
)
1346+
assert cpputest.status == TwisterStatus.NONE
1347+
assert len(cpputest.detected_suite_names) == 1
1348+
assert cpputest.detected_suite_names[0] == "suite_name"
1349+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") != TwisterStatus.NONE
1350+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.FAIL
1351+
1352+
1353+
def test_cpputest_test_repeated(cpputest):
1354+
with pytest.raises(
1355+
AssertionError,
1356+
match=r"CppUTest error, id.suite_name.test_name running twice",
1357+
):
1358+
process_logs(
1359+
cpputest,
1360+
[
1361+
SAMPLE_CPPUTEST_START_FMT.format(
1362+
suite="suite_name", test="test_name"
1363+
),
1364+
SAMPLE_CPPUTEST_START_FMT.format(
1365+
suite="suite_name", test="test_name"
1366+
),
1367+
],
1368+
)

tests/cpputest/base/CMakeLists.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright (c) 2025, Victor Chavez ([email protected])
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
cmake_minimum_required(VERSION 3.20.0)
5+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
6+
project(cpputest_sample)
7+
8+
include(FetchContent)
9+
FetchContent_Declare(
10+
CppUTest
11+
GIT_REPOSITORY https://github.com/cpputest/cpputest.git
12+
GIT_TAG v4.0
13+
)
14+
set(TESTS OFF CACHE BOOL "Switch off CppUTest Test build")
15+
16+
FetchContent_MakeAvailable(CppUTest)
17+
18+
19+
target_sources(app PRIVATE src/main.cpp
20+
src/test_suite.cpp
21+
)
22+
23+
target_link_libraries(app PRIVATE
24+
CppUTest
25+
CppUTestExt)

tests/cpputest/base/prj.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CONFIG_CPP=y
2+
CONFIG_LOG=y
3+
CONFIG_STD_CPP17=y
4+
CONFIG_REQUIRES_FULL_LIBCPP=y

tests/cpputest/base/src/main.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright (c) 2025, Victor Chavez ([email protected])
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
#include <CppUTest/CommandLineTestRunner.h>
6+
#include <posix_board_if.h> // posix_exit
7+
8+
int main(void)
9+
{
10+
/* Enable cpputest verbose mode (-v) to get the name
11+
* of tests that have passed. Otherwise only a dot
12+
* is printed per test.
13+
*/
14+
const char *cppu_test_args[] = {__FILE__, "-v", "-c"};
15+
int num_args = std::size(cppu_test_args);
16+
int test_Res = CommandLineTestRunner::RunAllTests(num_args, cppu_test_args);
17+
/* Exit before main ends as zephyr idle thread runs in the background */
18+
posix_exit(test_Res);
19+
return 0;
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2025, Victor Chavez ([email protected])
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
#include <zephyr/logging/log.h>
6+
#include <CppUTest/CommandLineTestRunner.h>
7+
#include <CppUTestExt/MockSupport.h>
8+
9+
LOG_MODULE_REGISTER(test_suite, CONFIG_LOG_DEFAULT_LEVEL);
10+
11+
TEST_GROUP(my_test_group) {
12+
void setup() final
13+
{
14+
}
15+
void teardown() final
16+
{
17+
}
18+
};
19+
20+
TEST(my_test_group, test_1) {
21+
22+
}

tests/cpputest/base/testcase.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
common:
2+
tags:
3+
- test_framework
4+
5+
tests:
6+
base.my_test_group:
7+
platform_allow:
8+
- native_sim/native/64
9+
harness: "cpputest"

0 commit comments

Comments
 (0)