Skip to content

Commit f399e87

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 patterns for test when CppUTest verbose mode is enabled. Signed-off-by: Victor Chavez <[email protected]>
1 parent c5fa9af commit f399e87

File tree

9 files changed

+304
-10
lines changed

9 files changed

+304
-10
lines changed

doc/develop/test/twister.rst

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -482,15 +482,19 @@ harness: <string>
482482
- pytest
483483
- gtest
484484
- robot
485-
486-
Harnesses ``ztest``, ``gtest`` and ``console`` are based on parsing of the
487-
output and matching certain phrases. ``ztest`` and ``gtest`` harnesses look
488-
for pass/fail/etc. frames defined in those frameworks. Use ``gtest``
489-
harness if you've already got tests written in the gTest framework and do
490-
not wish to update them to zTest. The ``console`` harness tells Twister to
491-
parse a test's text output for a regex defined in the test's YAML file.
492-
The ``robot`` harness is used to execute Robot Framework test suites
493-
in the Renode simulation framework.
485+
- cpputest
486+
487+
Harnesses ``ztest``, ``gtest``, ``cpputest`` and ``console`` are based on parsing of the
488+
output and matching certain phrases. ``ztest``, ``gtest`` and ``cpputest`` harnesses look
489+
for pass/fail/etc. frames defined in those frameworks. Use ``gtest`` or ``cpputest``
490+
harness if you've already got tests written with either of these framework and do
491+
If you are using ``cpputest``, you will need to enable verbose within your ``cpputest``
492+
application when calling ``CommandLineTestRunner::RunAllTests``.
493+
The reason for this is that by default ``cpputest`` does not output any information
494+
about individual test cases.
495+
The ``console`` harness tells Twister to parse a test's text output
496+
for a regex defined in the test's YAML file. The ``robot`` harness is used
497+
to execute Robot Framework test suites in the Renode simulation framework.
494498

495499
Some widely used harnesses that are not supported yet:
496500

scripts/pylib/twister/twisterlib/harness.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,86 @@ def handle(self, line):
782782
tc.reason = "Test failure"
783783

784784

785+
class Cpputest(Harness):
786+
TEST_START_PATTERN = r".*(?<!Failure in )TEST\((?P<suite_name>[^,]+), (?P<test_name>[^\)]+)\)"
787+
TEST_FAIL_PATTERN = r".*Failure in TEST\((?P<suite_name>[^,]+), (?P<test_name>[^\)]+)\).*"
788+
FINISHED_PATTERN = r".*(OK|Errors) \(\d+ tests, \d+ ran, \d+ checks, \d+ ignored, \d+ filtered out, \d+ ms\)"
789+
790+
def __init__(self):
791+
super().__init__()
792+
self.tc = None
793+
self.has_failures = False
794+
self.started = False
795+
796+
def handle(self, line):
797+
if not self.started:
798+
self.instance.testcases = []
799+
self.started = True
800+
if self.status != TwisterStatus.NONE:
801+
return
802+
803+
# Check if a new test starts
804+
test_start_match = re.search(self.TEST_START_PATTERN, line)
805+
if test_start_match:
806+
# If a new test starts and there is an unfinished test, mark it as passed
807+
if self.tc is not None:
808+
self.tc.status = TwisterStatus.PASS
809+
self.tc.output = self.testcase_output
810+
self.testcase_output = ""
811+
self.tc = None
812+
813+
suite_name = test_start_match.group("suite_name")
814+
test_name = test_start_match.group("test_name")
815+
if suite_name not in self.detected_suite_names:
816+
self.detected_suite_names.append(suite_name)
817+
818+
name = "{}.{}.{}".format(self.id, suite_name, test_name)
819+
820+
tc = self.instance.get_case_by_name(name)
821+
assert tc is None, "CppUTest error, {} running twice".format(name)
822+
823+
tc = self.instance.get_case_or_create(name)
824+
self.tc = tc
825+
self.tc.status = TwisterStatus.STARTED
826+
self.testcase_output += line + "\n"
827+
self._match = True
828+
829+
# Check if a test failure occurred
830+
test_fail_match = re.search(self.TEST_FAIL_PATTERN, line)
831+
if test_fail_match:
832+
suite_name = test_fail_match.group("suite_name")
833+
test_name = test_fail_match.group("test_name")
834+
name = "{}.{}.{}".format(self.id, suite_name, test_name)
835+
836+
tc = self.instance.get_case_by_name(name)
837+
if tc is not None:
838+
tc.status = TwisterStatus.FAIL
839+
self.has_failures = True
840+
tc.output = self.testcase_output
841+
self.testcase_output = ""
842+
self.tc = None
843+
return
844+
845+
# Check if the test run finished
846+
finished_match = re.search(self.FINISHED_PATTERN, line)
847+
if finished_match:
848+
# No need to check result if previously there was a failure
849+
# or no tests were run
850+
if self.has_failures or self.tc is None:
851+
return
852+
853+
tc = self.instance.get_case_or_create(self.tc.name)
854+
855+
finish_result = finished_match.group(1)
856+
if finish_result == "OK":
857+
self.status = TwisterStatus.PASS
858+
tc.status = TwisterStatus.PASS
859+
else:
860+
self.status = TwisterStatus.FAIL
861+
tc.status = TwisterStatus.FAIL
862+
return
863+
864+
785865
class Ztest(Test):
786866
pass
787867

scripts/pylib/twister/twisterlib/testinstance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def get_case_or_create(self, name):
194194
def testsuite_runnable(testsuite, fixtures):
195195
can_run = False
196196
# console harness allows us to run the test and capture data.
197-
if testsuite.harness in [ 'console', 'ztest', 'pytest', 'test', 'gtest', 'robot']:
197+
if testsuite.harness in [ 'console', 'ztest', 'pytest', 'test', 'gtest', 'robot', 'cpputest']:
198198
can_run = True
199199
# if we have a fixture that is also being supplied on the
200200
# command-line, then we need to run the test, not just build it.

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,
@@ -53,6 +54,17 @@
5354
"[00:00:00.000,000] <inf> label: [----------] Global test environment tear-down"
5455
)
5556

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

5769
def process_logs(harness, logs):
5870
for line in logs:
@@ -1077,6 +1089,124 @@ def test_gtest_repeated_run(gtest):
10771089
)
10781090

10791091

1092+
@pytest.fixture
1093+
def cpputest(tmp_path):
1094+
mock_platform = mock.Mock()
1095+
mock_platform.name = "mock_platform"
1096+
mock_platform.normalized_name = "mock_platform"
1097+
mock_testsuite = mock.Mock()
1098+
mock_testsuite.name = "mock_testsuite"
1099+
mock_testsuite.detailed_test_id = True
1100+
mock_testsuite.id = "id"
1101+
mock_testsuite.testcases = []
1102+
mock_testsuite.harness_config = {}
1103+
outdir = tmp_path / 'cpputest_out'
1104+
outdir.mkdir()
1105+
1106+
instance = TestInstance(testsuite=mock_testsuite, platform=mock_platform, outdir=outdir)
1107+
1108+
harness = Cpputest()
1109+
harness.configure(instance)
1110+
return harness
1111+
1112+
1113+
def test_cpputest_start_test_no_suites_detected(cpputest):
1114+
process_logs(cpputest, [SAMPLE_CPPUTEST_NO_TESTS])
1115+
assert len(cpputest.detected_suite_names) == 0
1116+
assert cpputest.status == TwisterStatus.NONE
1117+
1118+
1119+
def test_cpputest_start_test(cpputest):
1120+
process_logs(
1121+
cpputest,
1122+
[
1123+
SAMPLE_CPPUTEST_START_FMT.format(
1124+
suite="suite_name", test="test_name"
1125+
),
1126+
],
1127+
)
1128+
assert cpputest.status == TwisterStatus.NONE
1129+
assert len(cpputest.detected_suite_names) == 1
1130+
assert cpputest.detected_suite_names[0] == "suite_name"
1131+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") is not None
1132+
assert (
1133+
cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.STARTED
1134+
)
1135+
1136+
1137+
def test_cpputest_one_test_passed(cpputest):
1138+
process_logs(
1139+
cpputest,
1140+
[
1141+
SAMPLE_CPPUTEST_START_FMT.format(
1142+
suite="suite_name", test="test_name"
1143+
),
1144+
SAMPLE_CPPUTEST_END_PASS_FMT.format(
1145+
tests=1, ran=1, checks=5, ignored=0, filtered=0, time=10
1146+
)
1147+
],
1148+
)
1149+
assert len(cpputest.detected_suite_names) == 1
1150+
assert cpputest.detected_suite_names[0] == "suite_name"
1151+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") != TwisterStatus.NONE
1152+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.PASS
1153+
1154+
1155+
def test_cpputest_multiple_test_passed(cpputest):
1156+
logs = []
1157+
total_passed_tests = 5
1158+
for i in range(0, total_passed_tests):
1159+
logs.append(SAMPLE_CPPUTEST_START_FMT.format(suite="suite_name",
1160+
test="test_name_%d" % i))
1161+
logs.append(SAMPLE_CPPUTEST_END_PASS_FMT.format(
1162+
tests=total_passed_tests, ran=total_passed_tests, checks=5, ignored=0, filtered=0, time=10
1163+
))
1164+
process_logs(cpputest, logs)
1165+
assert len(cpputest.detected_suite_names) == 1
1166+
assert cpputest.detected_suite_names[0] == "suite_name"
1167+
for i in range(0, total_passed_tests):
1168+
test_name = "id.suite_name.test_name_%d" % i
1169+
assert cpputest.instance.get_case_by_name(test_name) != TwisterStatus.NONE
1170+
assert cpputest.instance.get_case_by_name(test_name).status == TwisterStatus.PASS
1171+
1172+
1173+
def test_cpputest_test_failed(cpputest):
1174+
process_logs(
1175+
cpputest,
1176+
[
1177+
SAMPLE_CPPUTEST_START_FMT.format(
1178+
suite="suite_name", test="test_name"
1179+
),
1180+
SAMPLE_CPPUTEST_FAIL_FMT.format(
1181+
suite="suite_name", test="test_name"
1182+
)
1183+
],
1184+
)
1185+
assert cpputest.status == TwisterStatus.NONE
1186+
assert len(cpputest.detected_suite_names) == 1
1187+
assert cpputest.detected_suite_names[0] == "suite_name"
1188+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name") != TwisterStatus.NONE
1189+
assert cpputest.instance.get_case_by_name("id.suite_name.test_name").status == TwisterStatus.FAIL
1190+
1191+
1192+
def test_cpputest_test_repeated(cpputest):
1193+
with pytest.raises(
1194+
AssertionError,
1195+
match=r"CppUTest error, id.suite_name.test_name running twice",
1196+
):
1197+
process_logs(
1198+
cpputest,
1199+
[
1200+
SAMPLE_CPPUTEST_START_FMT.format(
1201+
suite="suite_name", test="test_name"
1202+
),
1203+
SAMPLE_CPPUTEST_START_FMT.format(
1204+
suite="suite_name", test="test_name"
1205+
),
1206+
],
1207+
)
1208+
1209+
10801210
def test_bsim_build(monkeypatch, tmp_path):
10811211
mocked_instance = mock.Mock()
10821212
build_dir = tmp_path / "build_dir"

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) 2024, 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) 2024, 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) 2024, 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)