blob: 65805f70272b3f5e266dda16ea50c6f45b16f9b6 [file] [log] [blame]
// Copyright 2024 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 <cassert>
#include <filesystem>
#include <unordered_map>
#include "pw_assert/check.h"
#include "pw_json/builder.h"
#include "pw_unit_test/event_handler.h"
namespace pw::unit_test::json_impl {
// Version of the JSON Test Result Format. Format can be found at
// https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/testing/json_test_results_format.md
inline constexpr int kJsonTestResultsFormatVersion = 3;
/// A class that records test results as a trie, or prefix tree, and is capable
/// of outputting the trie as a json string. The trie is structured as a
/// hierarchical format to reduce duplication of test suite names.
class TestRecordTrie {
public:
/// Constructor that initializes the root test record trie node.
TestRecordTrie() {
root_ = new TestRecordTrieNode();
root_->prefix = "test_results";
failing_results_root_ = new TestRecordTrieNode();
failing_results_root_->prefix = "test_results";
}
/// Destructor that deletes all the allocated memory for the test record trie.
~TestRecordTrie() {
DeleteTestRecordTrie(root_);
DeleteTestRecordTrie(failing_results_root_);
}
/// Adds a test result into the trie, creating new trie nodes if needed.
/// If the test case's result is a failure, record it in the failing-results
/// trie as well.
///
/// @param[in] test_case The test case we want to add.
///
/// @param[in] result The result of the test case.
void AddTestResult(const TestCase& test_case, TestResult result) {
AddTestResultHelper(root_, test_case, result);
if (result == TestResult::kFailure) {
AddTestResultHelper(failing_results_root_, test_case, result);
}
}
/// Adds the test result expectation for a particular test case. Usually, we
/// expect all test results to be PASS. However, unique cases like a test case
/// using the GTEST_SKIP macro will result in the expected result being a SKIP
/// instead of a PASS.
///
/// @param[in] test_case The test case we want to add the expected result for.
///
/// @param[in] result The expected result we want to add for the test case.
void AddTestResultExpectation(const TestCase& test_case,
TestResult expected_result) {
TestRecordTrieNode* curr_node = root_;
// Calculate path to the test, including directories, test file, test suite,
// and test name
std::filesystem::path path_to_test =
std::filesystem::path(test_case.file_name) / test_case.suite_name /
test_case.test_name;
// Walk curr_node through the Trie to the test, creating new
// TestRecordTrieNodes along the way if needed
for (auto dir_entry : path_to_test) {
if (auto search = curr_node->children.find(dir_entry.string());
search != curr_node->children.end()) {
curr_node = search->second;
} else {
TestRecordTrieNode* child_node = new TestRecordTrieNode();
child_node->prefix = dir_entry.string();
curr_node->children[dir_entry.string()] = child_node;
curr_node = child_node;
}
}
// Add the test case's expected result
curr_node->expected_test_result = expected_result;
}
/// Outputs the test record trie as a json string.
///
/// @param[in] summary Test summary that includes counts for each test result
/// type.
///
/// @param[in] seconds_since_epoch Seconds since epoch for when the test run
/// started.
///
/// @param[in] max_json_buffer_size The max size (in bytes) of the buffer to
/// allocate for the json string.
///
/// @param[in] failing_results_only If true, the test record will only contain
/// the failing tests.
///
/// @param[in] interrupted Whether this test run was interrupted or not.
///
/// @param[in] version Version of the test result JSON format.
///
/// @returns The test record json as a string.
std::string GetTestRecordJsonString(
const RunTestsSummary& summary,
int64_t seconds_since_epoch,
size_t max_json_buffer_size,
bool failing_results_only = false,
bool interrupted = false,
int version = kJsonTestResultsFormatVersion) {
// Dynamically allocate a string to serve as the json buffer.
std::string buffer(max_json_buffer_size, '\0');
JsonBuilder builder(buffer.data(), max_json_buffer_size);
JsonObject& object = builder.StartObject();
NestedJsonObject tests_json_object = object.AddNestedObject("tests");
TestRecordTrieNode* starting_trie_node =
failing_results_only ? failing_results_root_ : root_;
GetTestRecordJsonHelper(starting_trie_node, tests_json_object);
// Add test record metadata
object.Add("version", version);
object.Add("interrupted", interrupted);
object.Add("seconds_since_epoch", seconds_since_epoch);
NestedJsonObject num_failures_json =
object.AddNestedObject("num_failures_by_type");
num_failures_json.Add("PASS", summary.passed_tests);
num_failures_json.Add("FAIL", summary.failed_tests);
num_failures_json.Add("SKIP", summary.skipped_tests);
// If the json buffer size was not big enough, then throw an error
PW_CHECK(
object.ok(),
"Test record json buffer is not big enough, please increase size.");
return object.data();
}
private:
/// Used to represent a singular node of the trie.
struct TestRecordTrieNode {
/// Either the name of a directory, file, test suite, or test case.
std::string prefix = "";
/// Whether this node is a leaf in the trie. Leaf nodes represent the
/// results of a singular test case and contains both the expected and
/// actual test result of that test case.
bool is_leaf = false;
/// The expected test result for this node. Success is expected by default.
TestResult expected_test_result = TestResult::kSuccess;
/// The actual test result for this node. Empty if this is not a leaf node.
TestResult actual_test_result{};
/// Children of the current trie node, keyed by the child's prefix.
std::unordered_map<std::string, TestRecordTrieNode*> children{};
};
/// Helper for adding a test result into the specified Trie, creating new trie
/// nodes if needed.
///
/// @param[in] root The root of the Trie we want to add the test result to.
///
/// @param[in] test_case The test case we want to add.
///
/// @param[in] result The result of the test case.
void AddTestResultHelper(TestRecordTrieNode* root,
const TestCase& test_case,
TestResult result) {
TestRecordTrieNode* curr_node = root;
// Calculate path to the test, including directories, test file, test suite,
// and test name
std::filesystem::path path_to_test =
std::filesystem::path(test_case.file_name) / test_case.suite_name /
test_case.test_name;
// Walk curr_node through the Trie to the test, creating new
// TestRecordTrieNodes along the way if needed
for (auto dir_entry : path_to_test) {
if (auto search = curr_node->children.find(dir_entry.string());
search != curr_node->children.end()) {
curr_node = search->second;
} else {
TestRecordTrieNode* child_node = new TestRecordTrieNode();
child_node->prefix = dir_entry.string();
curr_node->children[dir_entry.string()] = child_node;
curr_node = child_node;
}
}
// Add the test case's result
curr_node->is_leaf = true;
curr_node->actual_test_result = result;
}
/// Recursively convert the test record trie into a json object.
///
/// @param[in] curr_node The current node we want to turn into the json
/// object.
///
/// @param[in] curr_json The json object to add new child json objects to.
void GetTestRecordJsonHelper(TestRecordTrieNode* curr_node,
NestedJsonObject& curr_json) {
if (curr_node->is_leaf) {
NestedJsonObject child_json =
curr_json.AddNestedObject(curr_node->prefix);
child_json.Add("expected",
GetTestResultString(curr_node->expected_test_result));
child_json.Add("actual",
GetTestResultString(curr_node->actual_test_result));
} else {
// Don't create a json object for the root TrieNode
if (curr_node->prefix == "test_results") {
for (const auto& child_entry : curr_node->children) {
GetTestRecordJsonHelper(child_entry.second, curr_json);
}
} else {
NestedJsonObject child_json =
curr_json.AddNestedObject(curr_node->prefix);
for (const auto& child_entry : curr_node->children) {
GetTestRecordJsonHelper(child_entry.second, child_json);
}
}
}
}
/// Helper to output a string representation of a `TestResult` object
///
/// @param[in] test_result Test result to output as a string
///
/// @returns A string representation of the passed in `TestResult` object
std::string GetTestResultString(TestResult test_result) {
switch (test_result) {
case TestResult::kFailure:
return "FAIL";
case TestResult::kSuccess:
return "PASS";
case TestResult::kSkipped:
return "SKIP";
}
return "UNKNOWN";
}
/// Helper to recursively delete a test record trie node and all its children
///
/// @param[in] curr_node Node to delete, along with its children.
void DeleteTestRecordTrie(TestRecordTrieNode* curr_node) {
for (const auto& child : curr_node->children) {
DeleteTestRecordTrie(child.second);
}
delete curr_node;
}
// The root node of the test record trie
TestRecordTrieNode* root_;
// The root node of the failing-results test record trie, which is a subset of
// the test record trie that only contains failing tests.
TestRecordTrieNode* failing_results_root_;
};
} // namespace pw::unit_test::json_impl