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