pw_unit_test: Add test suite filter
This updates the unit test framework to accept an optional list of test
suites to run. If set, RUN_ALL_TESTS will only run test cases which
match the provided suites. Additionally, tracking of skipped and
disabled tests in a test run is added.
The unit test RPC service is also updated to accept a list of suites in
a test run request.
Change-Id: Ia1aefdcf2314c24431bff75378c30fcdfdd24c27
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/40061
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Reviewed-by: Joe Ethier <jethier@google.com>
diff --git a/pw_unit_test/BUILD.gn b/pw_unit_test/BUILD.gn
index a693e09..aa80fb9 100644
--- a/pw_unit_test/BUILD.gn
+++ b/pw_unit_test/BUILD.gn
@@ -30,9 +30,9 @@
pw_source_set("pw_unit_test") {
public_configs = [ ":default_config" ]
public_deps = [
- "$dir_pw_polyfill",
- "$dir_pw_preprocessor",
- "$dir_pw_string",
+ dir_pw_polyfill,
+ dir_pw_preprocessor,
+ dir_pw_string,
]
public = [
"public/pw_unit_test/event_handler.h",
@@ -90,6 +90,7 @@
":pw_unit_test",
":unit_test_proto.pwpb",
":unit_test_proto.raw_rpc",
+ "$dir_pw_containers:vector",
]
deps = [ dir_pw_log ]
public = [
diff --git a/pw_unit_test/docs.rst b/pw_unit_test/docs.rst
index 27fda58..5459484 100644
--- a/pw_unit_test/docs.rst
+++ b/pw_unit_test/docs.rst
@@ -109,6 +109,18 @@
return RUN_ALL_TESTS();
}
+Test filtering
+^^^^^^^^^^^^^^
+If using C++17, filters can be set on the test framework to run only a subset of
+the registered unit tests. This is useful when many tests are bundled into a
+single application image.
+
+Currently, only a test suite filter is supported. This is set by calling
+``pw::unit_test::SetTestSuitesToRun`` with a list of suite names.
+
+.. note::
+ Test filtering is only supported in C++17.
+
Build system integration
^^^^^^^^^^^^^^^^^^^^^^^^
``pw_unit_test`` integrates directly into Pigweed's GN build system. To define
@@ -271,3 +283,8 @@
client = HdlcRpcClient(serial.Serial(device, baud), PROTO)
run_tests(client.rpcs())
+
+pw_unit_test.rpc
+^^^^^^^^^^^^^^^^
+.. automodule:: pw_unit_test.rpc
+ :members: EventHandler, run_tests
diff --git a/pw_unit_test/framework.cc b/pw_unit_test/framework.cc
index 69529bc..70b36d3 100644
--- a/pw_unit_test/framework.cc
+++ b/pw_unit_test/framework.cc
@@ -14,6 +14,7 @@
#include "pw_unit_test/framework.h"
+#include <algorithm>
#include <cstring>
namespace pw {
@@ -49,15 +50,23 @@
int Framework::RunAllTests() {
run_tests_summary_.passed_tests = 0;
run_tests_summary_.failed_tests = 0;
+ run_tests_summary_.skipped_tests = 0;
+ run_tests_summary_.disabled_tests = 0;
if (event_handler_ != nullptr) {
event_handler_->RunAllTestsStart();
}
for (const TestInfo* test = tests_; test != nullptr; test = test->next()) {
- if (test->enabled()) {
+ if (ShouldRunTest(*test)) {
test->run();
- } else if (event_handler_ != nullptr) {
- event_handler_->TestCaseDisabled(test->test_case());
+ } else if (!test->enabled()) {
+ run_tests_summary_.disabled_tests++;
+
+ if (event_handler_ != nullptr) {
+ event_handler_->TestCaseDisabled(test->test_case());
+ }
+ } else {
+ run_tests_summary_.skipped_tests++;
}
}
if (event_handler_ != nullptr) {
@@ -115,6 +124,26 @@
event_handler_->TestCaseExpect(current_test_->test_case(), expectation);
}
+bool Framework::ShouldRunTest(const TestInfo& test_info) {
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ // Test suite filtering is only supported if using C++17.
+ if (!test_suites_to_run_.empty()) {
+ std::string_view test_suite(test_info.test_case().suite_name);
+
+ bool suite_matches =
+ std::any_of(test_suites_to_run_.begin(),
+ test_suites_to_run_.end(),
+ [&](auto& name) { return test_suite == name; });
+
+ if (!suite_matches) {
+ return false;
+ }
+ }
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+ return test_info.enabled();
+}
+
bool TestInfo::enabled() const {
constexpr size_t kStringSize = sizeof("DISABLED_") - 1;
return std::strncmp("DISABLED_", test_case().test_name, kStringSize) != 0 &&
diff --git a/pw_unit_test/public/pw_unit_test/event_handler.h b/pw_unit_test/public/pw_unit_test/event_handler.h
index b7b931d..3a5a710 100644
--- a/pw_unit_test/public/pw_unit_test/event_handler.h
+++ b/pw_unit_test/public/pw_unit_test/event_handler.h
@@ -84,6 +84,12 @@
// The number of passed tests among the run tests.
int failed_tests;
+
+ // The number of tests skipped or filtered out.
+ int skipped_tests;
+
+ // The number of disabled tests encountered.
+ int disabled_tests;
};
// An event handler is responsible for collecting and processing the results of
diff --git a/pw_unit_test/public/pw_unit_test/framework.h b/pw_unit_test/public/pw_unit_test/framework.h
index b9db35b..959cef8 100644
--- a/pw_unit_test/public/pw_unit_test/framework.h
+++ b/pw_unit_test/public/pw_unit_test/framework.h
@@ -29,6 +29,8 @@
#include "pw_unit_test/event_handler.h"
#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+#include <string_view>
+
#include "pw_string/string_builder.h"
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
@@ -155,7 +157,10 @@
constexpr Framework()
: current_test_(nullptr),
current_result_(TestResult::kSuccess),
- run_tests_summary_{.passed_tests = 0, .failed_tests = 0},
+ run_tests_summary_{.passed_tests = 0,
+ .failed_tests = 0,
+ .skipped_tests = 0,
+ .disabled_tests = 0},
exit_status_(0),
event_handler_(nullptr),
memory_pool_() {}
@@ -177,6 +182,17 @@
// are sent to the registered event handler, if any.
int RunAllTests();
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ // Only run test suites whose names are included in the provided list during
+ // the next test run. This is C++17 only; older versions of C++ will run all
+ // non-disabled tests.
+ void SetTestSuitesToRun(std::span<std::string_view> test_suites) {
+ test_suites_to_run_ = test_suites;
+ }
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+ bool ShouldRunTest(const TestInfo& test_info);
+
// Constructs an instance of a unit test class and runs the test.
//
// Tests are constructed within a static memory pool at run time instead of
@@ -280,6 +296,10 @@
// Handler to which to dispatch test events.
EventHandler* event_handler_;
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ std::span<std::string_view> test_suites_to_run_;
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
// Memory region in which to construct test case classes as they are run.
// TODO(frolv): Make the memory pool size configurable.
static constexpr size_t kTestMemoryPoolSizeBytes = 16384;
@@ -376,6 +396,12 @@
virtual void PigweedTestBody() = 0;
};
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+inline void SetTestSuitesToRun(std::span<std::string_view> test_suites) {
+ internal::Framework::Get().SetTestSuitesToRun(test_suites);
+}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
} // namespace unit_test
} // namespace pw
diff --git a/pw_unit_test/pw_unit_test_proto/unit_test.proto b/pw_unit_test/pw_unit_test_proto/unit_test.proto
index ace948e..4a10e8b 100644
--- a/pw_unit_test/pw_unit_test_proto/unit_test.proto
+++ b/pw_unit_test/pw_unit_test_proto/unit_test.proto
@@ -81,6 +81,9 @@
message TestRunRequest {
// Whether to send expectation events for successful checks.
bool report_passed_expectations = 1;
+
+ // Optional list of test suites to run.
+ repeated string test_suite = 2;
}
service UnitTest {
diff --git a/pw_unit_test/py/pw_unit_test/rpc.py b/pw_unit_test/py/pw_unit_test/rpc.py
index d1294d5..9fc8280 100644
--- a/pw_unit_test/py/pw_unit_test/rpc.py
+++ b/pw_unit_test/py/pw_unit_test/rpc.py
@@ -113,6 +113,7 @@
def run_tests(rpcs: pw_rpc.client.Services,
report_passed_expectations: bool = False,
+ test_suites: Iterable[str] = (),
event_handlers: Iterable[EventHandler] = (
LoggingEventHandler(), ),
timeout_s: OptionalTimeout = UseDefault.VALUE) -> bool:
@@ -126,6 +127,7 @@
test_responses = iter(
unit_test_service.Run(
report_passed_expectations=report_passed_expectations,
+ test_suite=test_suites,
pw_rpc_timeout_s=timeout_s))
# Read the first response, which must be a test_run_start message.
diff --git a/pw_unit_test/unit_test_service.cc b/pw_unit_test/unit_test_service.cc
index 86fb37c..b43537f 100644
--- a/pw_unit_test/unit_test_service.cc
+++ b/pw_unit_test/unit_test_service.cc
@@ -14,6 +14,7 @@
#include "pw_unit_test/unit_test_service.h"
+#include "pw_containers/vector.h"
#include "pw_log/log.h"
#include "pw_protobuf/decoder.h"
#include "pw_unit_test/framework.h"
@@ -26,6 +27,11 @@
writer_ = std::move(writer);
verbose_ = false;
+ // List of test suite names to run. The string views in this vector point to
+ // data in the raw protobuf request message, so it is only valid for the
+ // duration of this function.
+ pw::Vector<std::string_view, 16> suites_to_run;
+
protobuf::Decoder decoder(request);
Status status;
@@ -34,6 +40,24 @@
case TestRunRequest::Fields::REPORT_PASSED_EXPECTATIONS:
decoder.ReadBool(&verbose_);
break;
+
+ case TestRunRequest::Fields::TEST_SUITE: {
+ std::string_view suite_name;
+ if (!decoder.ReadString(&suite_name).ok()) {
+ break;
+ }
+
+ if (!suites_to_run.full()) {
+ suites_to_run.push_back(suite_name);
+ } else {
+ PW_LOG_ERROR("Maximum of %d test suite filters supported",
+ suites_to_run.max_size());
+ writer_.Finish(Status::InvalidArgument());
+ return;
+ }
+
+ break;
+ }
}
}
@@ -42,12 +66,19 @@
return;
}
- PW_LOG_DEBUG("Starting unit test run");
+ PW_LOG_INFO("Starting unit test run");
- pw::unit_test::RegisterEventHandler(&handler_);
+ RegisterEventHandler(&handler_);
+ SetTestSuitesToRun(suites_to_run);
+ PW_LOG_DEBUG("%u test suite filters applied",
+ static_cast<unsigned>(suites_to_run.size()));
+
RUN_ALL_TESTS();
- pw::unit_test::RegisterEventHandler(nullptr);
- PW_LOG_DEBUG("Unit test run complete");
+
+ RegisterEventHandler(nullptr);
+ SetTestSuitesToRun({});
+
+ PW_LOG_INFO("Unit test run complete");
writer_.Finish();
}
@@ -62,6 +93,8 @@
TestRunEnd::Encoder test_run_end = event.GetTestRunEndEncoder();
test_run_end.WritePassed(summary.passed_tests);
test_run_end.WriteFailed(summary.failed_tests);
+ test_run_end.WriteSkipped(summary.skipped_tests);
+ test_run_end.WriteDisabled(summary.disabled_tests);
});
}