pw_unit_test: Add RPC service
This implements an RPC service that streams pw_unit_test events, as well
as a unit test main that initializes a server with the test service
registered and an HDLC serial channel.
Change-Id: I6c66f2216c54d82aa70c351d7d3d99d9a5c173de
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/23901
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_unit_test/BUILD b/pw_unit_test/BUILD
index ccc8e49..8ffaf3e 100644
--- a/pw_unit_test/BUILD
+++ b/pw_unit_test/BUILD
@@ -86,6 +86,36 @@
],
)
+pw_cc_library(
+ name = "rpc_service",
+ hdrs = [
+ "public/pw_unit_test/internal/rpc_event_handler.h",
+ "public/pw_unit_test/unit_test_service.h",
+ ],
+ srcs = [
+ "rpc_event_handler.cc",
+ "unit_test_service.cc",
+ ],
+ deps = [
+ ":pw_unit_test",
+ "//pw_log",
+ ],
+)
+
+pw_cc_library(
+ name = "rpc_main",
+ srcs = [
+ "rpc_main.cc",
+ ],
+ deps = [
+ ":pw_unit_test",
+ ":rpc_service",
+ "//pw_hdlc_lite:pw_rpc",
+ "//pw_log",
+ "//pw_rpc:server",
+ ],
+)
+
pw_cc_test(
name = "framework_test",
srcs = ["framework_test.cc"],
diff --git a/pw_unit_test/BUILD.gn b/pw_unit_test/BUILD.gn
index c4494ae..33695f8 100644
--- a/pw_unit_test/BUILD.gn
+++ b/pw_unit_test/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2019 The Pigweed Authors
+# Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -16,6 +16,7 @@
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_unit_test/test.gni")
config("default_config") {
@@ -63,10 +64,6 @@
sources = [ "simple_printing_main.cc" ]
}
-pw_doc_group("docs") {
- sources = [ "docs.rst" ]
-}
-
# Library providing an event handler which logs using pw_log.
pw_source_set("logging_event_handler") {
public_deps = [
@@ -87,6 +84,43 @@
sources = [ "logging_main.cc" ]
}
+pw_source_set("rpc_service") {
+ public_configs = [ ":default_config" ]
+ public_deps = [
+ ":pw_unit_test",
+ ":unit_test_proto.pwpb",
+ ":unit_test_proto.raw_rpc",
+ ]
+ deps = [ dir_pw_log ]
+ public = [
+ "public/pw_unit_test/internal/rpc_event_handler.h",
+ "public/pw_unit_test/unit_test_service.h",
+ ]
+ sources = [
+ "rpc_event_handler.cc",
+ "unit_test_service.cc",
+ ]
+}
+
+pw_source_set("rpc_main") {
+ public_deps = [ ":pw_unit_test" ]
+ deps = [
+ ":rpc_service",
+ "$dir_pw_hdlc_lite:pw_rpc",
+ "$dir_pw_rpc:server",
+ dir_pw_log,
+ ]
+ sources = [ "rpc_main.cc" ]
+}
+
+pw_proto_library("unit_test_proto") {
+ sources = [ "pw_unit_test_proto/unit_test.proto" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
pw_test("framework_test") {
sources = [ "framework_test.cc" ]
}
diff --git a/pw_unit_test/docs.rst b/pw_unit_test/docs.rst
index 0f9c56f..6fb2fb4 100644
--- a/pw_unit_test/docs.rst
+++ b/pw_unit_test/docs.rst
@@ -6,6 +6,8 @@
``pw_unit_test`` unit testing library with a `Google Test`_-compatible API,
built on top of embedded-friendly primitives.
+.. _Google Test: https://github.com/google/googletest/blob/master/googletest/docs/primer.md
+
``pw_unit_test`` is a portable library which can run on almost any system from
from bare metal to a full-fledged desktop OS. It does this by offloading the
responsibility of test reporting and output to the underlying system,
@@ -210,5 +212,29 @@
# ...
}
+RPC service
+===========
+``pw_unit_test`` provides an RPC service which runs unit tests on demand and
+streams the results back to the client. The service is defined in
+``pw_unit_test_proto/unit_test.proto``, and implemented by the GN target
+``$dir_pw_unit_test:rpc_service``.
-.. _Google Test: https://github.com/google/googletest/blob/master/googletest/docs/primer.md
+To set up RPC-based unit tests in your application, instantiate a
+``pw::unit_test::UnitTestService`` and register it with your RPC server.
+
+.. code:: c++
+
+ #include "pw_rpc/server.h"
+ #include "pw_unit_test/unit_test_service.h"
+
+ // Server setup; refer to pw_rpc docs for more information.
+ pw::rpc::Channel channels[] = {
+ pw::rpc::Channel::Create<1>(&my_output),
+ };
+ pw::rpc::Server server(channels);
+
+ pw::unit_test::UnitTestService unit_test_service;
+
+ void RegisterServices() {
+ server.RegisterService(unit_test_services);
+ }
diff --git a/pw_unit_test/public/pw_unit_test/internal/rpc_event_handler.h b/pw_unit_test/public/pw_unit_test/internal/rpc_event_handler.h
new file mode 100644
index 0000000..ad27fc0
--- /dev/null
+++ b/pw_unit_test/public/pw_unit_test/internal/rpc_event_handler.h
@@ -0,0 +1,42 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_unit_test/event_handler.h"
+
+namespace pw::unit_test {
+
+class UnitTestService;
+
+namespace internal {
+
+// Unit test event handler that streams test events through an RPC service.
+class RpcEventHandler : public EventHandler {
+ public:
+ RpcEventHandler(UnitTestService& service) : service_(service) {}
+
+ void RunAllTestsStart() override;
+ void RunAllTestsEnd(const RunTestsSummary& run_tests_summary) override;
+ void TestCaseStart(const TestCase& test_case) override;
+ void TestCaseEnd(const TestCase& test_case, TestResult result) override;
+ void TestCaseExpect(const TestCase& test_case,
+ const TestExpectation& expectation) override;
+ void TestCaseDisabled(const TestCase& test_case) override;
+
+ private:
+ UnitTestService& service_;
+};
+
+} // namespace internal
+} // namespace pw::unit_test
diff --git a/pw_unit_test/public/pw_unit_test/unit_test_service.h b/pw_unit_test/public/pw_unit_test/unit_test_service.h
new file mode 100644
index 0000000..f5c7a79
--- /dev/null
+++ b/pw_unit_test/public/pw_unit_test/unit_test_service.h
@@ -0,0 +1,57 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_log/log.h"
+#include "pw_unit_test/internal/rpc_event_handler.h"
+#include "pw_unit_test_proto/unit_test.pwpb.h"
+#include "pw_unit_test_proto/unit_test.raw_rpc.pb.h"
+
+namespace pw::unit_test {
+
+class UnitTestService final : public generated::UnitTest<UnitTestService> {
+ public:
+ UnitTestService() : handler_(*this), verbose_(false) {}
+
+ void Run(ServerContext& ctx, ConstByteSpan request, RawServerWriter& writer);
+
+ private:
+ friend class internal::RpcEventHandler;
+
+ // TODO(frolv): This function essentially performs what a pw_protobuf RPC
+ // method would do. Once that API is implemented, this service should be
+ // migrated to it.
+ template <typename WriteFunction>
+ void WriteEvent(WriteFunction event_writer) {
+ protobuf::NestedEncoder<2, 3> encoder(writer_.PayloadBuffer());
+ Event::Encoder event(&encoder);
+ event_writer(event);
+ if (Result<ConstByteSpan> result = encoder.Encode(); result.ok()) {
+ writer_.Write(result.value());
+ }
+ }
+
+ void WriteTestRunStart();
+ void WriteTestRunEnd(const RunTestsSummary& summary);
+ void WriteTestCaseStart(const TestCase& test_case);
+ void WriteTestCaseEnd(TestResult result);
+ void WriteTestCaseDisabled(const TestCase& test_case);
+ void WriteTestCaseExpectation(const TestExpectation& expectation);
+
+ internal::RpcEventHandler handler_;
+ RawServerWriter writer_;
+ bool verbose_;
+};
+
+} // namespace pw::unit_test
diff --git a/pw_unit_test/pw_unit_test_proto/unit_test.proto b/pw_unit_test/pw_unit_test_proto/unit_test.proto
new file mode 100644
index 0000000..ace948e
--- /dev/null
+++ b/pw_unit_test/pw_unit_test_proto/unit_test.proto
@@ -0,0 +1,89 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+syntax = "proto3";
+
+package pw.unit_test;
+
+message TestCaseDescriptor {
+ // Name of the test suite to which this test case belongs.
+ string suite_name = 1;
+
+ // Name of the test case.
+ string test_name = 2;
+
+ // Path to the file in which the test case is defined.
+ string file_name = 3;
+}
+
+message TestCaseExpectation {
+ // The source code for the expression which was run.
+ string expression = 1;
+
+ // The expression with arguments evaluated.
+ string evaluated_expression = 2;
+
+ // Line number at which the expectation is located.
+ uint32 line_number = 3;
+
+ // Whether the expectation succeeded.
+ bool success = 4;
+}
+
+enum TestCaseResult {
+ SUCCESS = 0;
+ FAILURE = 1;
+ SKIPPED = 2;
+}
+
+message TestRunStart {}
+
+message TestRunEnd {
+ uint32 passed = 1;
+ uint32 failed = 2;
+ uint32 skipped = 3;
+ uint32 disabled = 4;
+}
+
+message Event {
+ oneof type {
+ // Unit test run has started.
+ TestRunStart test_run_start = 1;
+
+ // Unit test run has ended.
+ TestRunEnd test_run_end = 2;
+
+ // Start of a test case.
+ TestCaseDescriptor test_case_start = 3;
+
+ // End of a test case.
+ TestCaseResult test_case_end = 4;
+
+ // Encountered a disabled test case.
+ TestCaseDescriptor test_case_disabled = 5;
+
+ // Expectation statement within a test case.
+ TestCaseExpectation test_case_expectation = 6;
+ }
+};
+
+message TestRunRequest {
+ // Whether to send expectation events for successful checks.
+ bool report_passed_expectations = 1;
+}
+
+service UnitTest {
+ // Runs registered unit tests, streaming back test events as they occur.
+ rpc Run(TestRunRequest) returns (stream Event) {}
+}
diff --git a/pw_unit_test/rpc_event_handler.cc b/pw_unit_test/rpc_event_handler.cc
new file mode 100644
index 0000000..291dec3
--- /dev/null
+++ b/pw_unit_test/rpc_event_handler.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_unit_test/internal/rpc_event_handler.h"
+
+#include "pw_unit_test/unit_test_service.h"
+
+namespace pw::unit_test::internal {
+
+void RpcEventHandler::RunAllTestsStart() { service_.WriteTestRunStart(); }
+
+void RpcEventHandler::RunAllTestsEnd(const RunTestsSummary& run_tests_summary) {
+ service_.WriteTestRunEnd(run_tests_summary);
+}
+
+void RpcEventHandler::TestCaseStart(const TestCase& test_case) {
+ service_.WriteTestCaseStart(test_case);
+}
+
+void RpcEventHandler::TestCaseEnd(const TestCase&, TestResult result) {
+ service_.WriteTestCaseEnd(result);
+}
+
+void RpcEventHandler::TestCaseExpect(const TestCase&,
+ const TestExpectation& expectation) {
+ service_.WriteTestCaseExpectation(expectation);
+}
+
+void RpcEventHandler::TestCaseDisabled(const TestCase& test_case) {
+ service_.WriteTestCaseDisabled(test_case);
+}
+
+} // namespace pw::unit_test::internal
diff --git a/pw_unit_test/rpc_main.cc b/pw_unit_test/rpc_main.cc
new file mode 100644
index 0000000..04a70ad
--- /dev/null
+++ b/pw_unit_test/rpc_main.cc
@@ -0,0 +1,67 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_hdlc_lite/encoder.h"
+#include "pw_hdlc_lite/rpc_channel.h"
+#include "pw_hdlc_lite/rpc_packets.h"
+#include "pw_hdlc_lite/sys_io_stream.h"
+#include "pw_log/log.h"
+#include "pw_unit_test/framework.h"
+#include "pw_unit_test/unit_test_service.h"
+
+// TODO(frolv): This file is largely copied from the HDLC RPC example. It should
+// be updated to use the system RPC server facade when that is ready.
+
+namespace pw {
+namespace {
+
+constexpr size_t kMaxTransmissionUnit = 256;
+
+// Used to write HDLC data to pw::sys_io.
+stream::SysIoWriter writer;
+
+// Set up the output channel for the pw_rpc server to use to use.
+hdlc_lite::RpcChannelOutputBuffer<kMaxTransmissionUnit> hdlc_channel_output(
+ writer, hdlc_lite::kDefaultRpcAddress, "HDLC channel");
+
+rpc::Channel channels[] = {rpc::Channel::Create<1>(&hdlc_channel_output)};
+
+// Declare the pw_rpc server with the HDLC channel.
+rpc::Server server(channels);
+
+unit_test::UnitTestService unit_test_service;
+
+void RegisterServices() { server.RegisterService(unit_test_service); }
+
+} // namespace
+
+extern "C" int main() {
+ // Send log messages to HDLC address 1. This prevents logs from interfering
+ // with pw_rpc communications.
+ log_basic::SetOutput([](std::string_view log) {
+ hdlc_lite::WriteInformationFrame(1, std::as_bytes(std::span(log)), writer);
+ });
+
+ RegisterServices();
+
+ // Declare a buffer for decoding incoming HDLC frames.
+ std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+
+ PW_LOG_INFO("Starting pw_rpc server");
+ hdlc_lite::ReadAndProcessPackets(server, hdlc_channel_output, input_buffer);
+
+ return 0;
+}
+
+} // namespace pw
diff --git a/pw_unit_test/unit_test_service.cc b/pw_unit_test/unit_test_service.cc
new file mode 100644
index 0000000..86fb37c
--- /dev/null
+++ b/pw_unit_test/unit_test_service.cc
@@ -0,0 +1,109 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_unit_test/unit_test_service.h"
+
+#include "pw_log/log.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_unit_test/framework.h"
+
+namespace pw::unit_test {
+
+void UnitTestService::Run(ServerContext&,
+ ConstByteSpan request,
+ RawServerWriter& writer) {
+ writer_ = std::move(writer);
+ verbose_ = false;
+
+ protobuf::Decoder decoder(request);
+
+ Status status;
+ while ((status = decoder.Next()).ok()) {
+ switch (static_cast<TestRunRequest::Fields>(decoder.FieldNumber())) {
+ case TestRunRequest::Fields::REPORT_PASSED_EXPECTATIONS:
+ decoder.ReadBool(&verbose_);
+ break;
+ }
+ }
+
+ if (status != Status::OutOfRange()) {
+ writer_.Finish(status);
+ return;
+ }
+
+ PW_LOG_DEBUG("Starting unit test run");
+
+ pw::unit_test::RegisterEventHandler(&handler_);
+ RUN_ALL_TESTS();
+ pw::unit_test::RegisterEventHandler(nullptr);
+ PW_LOG_DEBUG("Unit test run complete");
+
+ writer_.Finish();
+}
+
+void UnitTestService::WriteTestRunStart() {
+ // Write out the key for the start field (even though the message is empty).
+ WriteEvent([&](Event::Encoder& event) { event.GetTestRunStartEncoder(); });
+}
+
+void UnitTestService::WriteTestRunEnd(const RunTestsSummary& summary) {
+ WriteEvent([&](Event::Encoder& event) {
+ TestRunEnd::Encoder test_run_end = event.GetTestRunEndEncoder();
+ test_run_end.WritePassed(summary.passed_tests);
+ test_run_end.WriteFailed(summary.failed_tests);
+ });
+}
+
+void UnitTestService::WriteTestCaseStart(const TestCase& test_case) {
+ WriteEvent([&](Event::Encoder& event) {
+ TestCaseDescriptor::Encoder descriptor = event.GetTestCaseStartEncoder();
+ descriptor.WriteSuiteName(test_case.suite_name);
+ descriptor.WriteTestName(test_case.test_name);
+ descriptor.WriteFileName(test_case.file_name);
+ });
+}
+
+void UnitTestService::WriteTestCaseEnd(TestResult result) {
+ WriteEvent([&](Event::Encoder& event) {
+ event.WriteTestCaseEnd(static_cast<TestCaseResult>(result));
+ });
+}
+
+void UnitTestService::WriteTestCaseDisabled(const TestCase& test_case) {
+ WriteEvent([&](Event::Encoder& event) {
+ TestCaseDescriptor::Encoder descriptor = event.GetTestCaseDisabledEncoder();
+ descriptor.WriteSuiteName(test_case.suite_name);
+ descriptor.WriteTestName(test_case.test_name);
+ descriptor.WriteFileName(test_case.file_name);
+ });
+}
+
+void UnitTestService::WriteTestCaseExpectation(
+ const TestExpectation& expectation) {
+ if (!verbose_ && expectation.success) {
+ return;
+ }
+
+ WriteEvent([&](Event::Encoder& event) {
+ TestCaseExpectation::Encoder test_case_expectation =
+ event.GetTestCaseExpectationEncoder();
+ test_case_expectation.WriteExpression(expectation.expression);
+ test_case_expectation.WriteEvaluatedExpression(
+ expectation.evaluated_expression);
+ test_case_expectation.WriteLineNumber(expectation.line_number);
+ test_case_expectation.WriteSuccess(expectation.success);
+ });
+}
+
+} // namespace pw::unit_test