Skip to content

Commit d4ecb01

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 5736aed commit d4ecb01

File tree

9 files changed

+317
-3
lines changed

9 files changed

+317
-3
lines changed

doc/develop/test/twister.rst

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ harness: <string>
561561
- robot
562562
- ctest
563563
- shell
564+
- cpputest
564565

565566
See :ref:`twister_harnesses` for more information.
566567

@@ -755,8 +756,8 @@ Most everyday users will run with no arguments.
755756
Harnesses
756757
*********
757758

758-
Harnesses ``ztest``, ``gtest`` and ``console`` are based on parsing of the
759-
output and matching certain phrases. ``ztest`` and ``gtest`` harnesses look
759+
Harnesses ``ztest``, ``gtest``, ``console`` and ``cpputest`` are based on parsing of the
760+
output and matching certain phrases. ``ztest``, ``gtest`` and ``cpputest`` harnesses look
760761
for pass/fail/etc. frames defined in those frameworks.
761762

762763
Some widely used harnesses that are not supported yet:
@@ -802,6 +803,27 @@ Gtest
802803
Use ``gtest`` harness if you've already got tests written in the gTest
803804
framework and do not wish to update them to zTest.
804805

806+
Cpputest
807+
========
808+
809+
``cpputest`` does not output information for each test case result by default. As this
810+
harness is based on parsing the output it is necessry to enable verbose within ``cpputest``:
811+
812+
.. code-block:: cpp
813+
814+
#include <CppUTest/CommandLineTestRunner.h>
815+
#include <posix_board_if.h>
816+
817+
int main(void)
818+
{
819+
const char *cppu_test_args[] = {__FILE__, "-v"};
820+
int num_args = std::size(cppu_test_args);
821+
int test_Res = CommandLineTestRunner::RunAllTests(num_args, cppu_test_args);
822+
posix_exit(test_Res);
823+
return 0;
824+
}
825+
826+
805827
Pytest
806828
======
807829

scripts/pylib/twister/twisterlib/harness.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,87 @@ def _parse_report_file(self, report):
11331133
else:
11341134
tc.status = TwisterStatus.PASS
11351135

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

11381219
@staticmethod

scripts/pylib/twister/twisterlib/testinstance.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ def testsuite_runnable(testsuite, fixtures):
226226
'gtest',
227227
'robot',
228228
'ctest',
229-
'shell'
229+
'shell',
230+
'cpputest'
230231
]:
231232
can_run = True
232233
# if we have a fixture that is also being supplied on the

scripts/tests/twister/test_harness.py

Lines changed: 130 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:
@@ -1209,6 +1221,124 @@ def test_gtest_repeated_run(gtest):
12091221
],
12101222
)
12111223

1224+
@pytest.fixture
1225+
def cpputest(tmp_path):
1226+
mock_platform = mock.Mock()
1227+
mock_platform.name = "mock_platform"
1228+
mock_platform.normalized_name = "mock_platform"
1229+
mock_testsuite = mock.Mock()
1230+
mock_testsuite.name = "mock_testsuite"
1231+
mock_testsuite.detailed_test_id = True
1232+
mock_testsuite.id = "id"
1233+
mock_testsuite.testcases = []
1234+
mock_testsuite.harness_config = {}
1235+
outdir = tmp_path / 'cpputest_out'
1236+
outdir.mkdir()
1237+
1238+
instance = TestInstance(
1239+
testsuite=mock_testsuite, platform=mock_platform, toolchain='zephyr', outdir=outdir
1240+
)
1241+
1242+
harness = Cpputest()
1243+
harness.configure(instance)
1244+
return harness
1245+
1246+
1247+
def test_cpputest_start_test_no_suites_detected(cpputest):
1248+
process_logs(cpputest, [SAMPLE_CPPUTEST_NO_TESTS])
1249+
assert len(cpputest.detected_suite_names) == 0
1250+
assert cpputest.status == TwisterStatus.NONE
1251+
1252+
1253+
def test_cpputest_start_test(cpputest):
1254+
process_logs(
1255+
cpputest,
1256+
[
1257+
SAMPLE_CPPUTEST_START_FMT.format(
1258+
suite="suite_name", test="test_name"
1259+
),
1260+
],
1261+
)
1262+
assert cpputest.status == TwisterStatus.NONE
1263+
assert len(cpputest.detected_suite_names) == 1
1264+
assert cpputest.detected_suite_names[0] == "suite_name"
1265+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") is not None
1266+
assert (
1267+
cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.STARTED
1268+
)
1269+
1270+
1271+
def test_cpputest_one_test_passed(cpputest):
1272+
process_logs(
1273+
cpputest,
1274+
[
1275+
SAMPLE_CPPUTEST_START_FMT.format(
1276+
suite="suite_name", test="test_name"
1277+
),
1278+
SAMPLE_CPPUTEST_END_PASS_FMT.format(
1279+
tests=1, ran=1, checks=5, ignored=0, filtered=0, time=10
1280+
)
1281+
],
1282+
)
1283+
assert len(cpputest.detected_suite_names) == 1
1284+
assert cpputest.detected_suite_names[0] == "suite_name"
1285+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") != TwisterStatus.NONE
1286+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.PASS
1287+
1288+
1289+
def test_cpputest_multiple_test_passed(cpputest):
1290+
logs = []
1291+
total_passed_tests = 5
1292+
for i in range(0, total_passed_tests):
1293+
logs.append(SAMPLE_CPPUTEST_START_FMT.format(suite="suite_name",
1294+
test="test_name_%d" % i))
1295+
logs.append(SAMPLE_CPPUTEST_END_PASS_FMT.format(
1296+
tests=total_passed_tests, ran=total_passed_tests, checks=5, ignored=0, filtered=0, time=10
1297+
))
1298+
process_logs(cpputest, logs)
1299+
assert len(cpputest.detected_suite_names) == 1
1300+
assert cpputest.detected_suite_names[0] == "suite_name"
1301+
for i in range(0, total_passed_tests):
1302+
test_name = "id.suite_name.test_name_%d" % i
1303+
assert cpputest.instance.get_case_by_name(test_name) != TwisterStatus.NONE
1304+
assert cpputest.instance.get_case_by_name(test_name).status == TwisterStatus.PASS
1305+
1306+
1307+
def test_cpputest_test_failed(cpputest):
1308+
process_logs(
1309+
cpputest,
1310+
[
1311+
SAMPLE_CPPUTEST_START_FMT.format(
1312+
suite="suite_name", test="test_name"
1313+
),
1314+
SAMPLE_CPPUTEST_FAIL_FMT.format(
1315+
suite="suite_name", test="test_name"
1316+
)
1317+
],
1318+
)
1319+
assert cpputest.status == TwisterStatus.NONE
1320+
assert len(cpputest.detected_suite_names) == 1
1321+
assert cpputest.detected_suite_names[0] == "suite_name"
1322+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") != TwisterStatus.NONE
1323+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.FAIL
1324+
1325+
1326+
def test_cpputest_test_repeated(cpputest):
1327+
with pytest.raises(
1328+
AssertionError,
1329+
match=r"CppUTest error, id.suite_name.test_name running twice",
1330+
):
1331+
process_logs(
1332+
cpputest,
1333+
[
1334+
SAMPLE_CPPUTEST_START_FMT.format(
1335+
suite="suite_name", test="test_name"
1336+
),
1337+
SAMPLE_CPPUTEST_START_FMT.format(
1338+
suite="suite_name", test="test_name"
1339+
),
1340+
],
1341+
)
12121342

12131343
def test_bsim_build(monkeypatch, tmp_path):
12141344
mocked_instance = mock.Mock()

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.yml

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)