Skip to content

Commit

Permalink
[yaml] Add internal::Node class (RobotLocomotion#15865)
Browse files Browse the repository at this point in the history
This will be be used to avoid yaml-cpp quirks and dependency conflicts
in future commits.
  • Loading branch information
jwnimmer-tri authored Oct 7, 2021
1 parent 8f79cd8 commit 25499ab
Show file tree
Hide file tree
Showing 4 changed files with 723 additions and 0 deletions.
18 changes: 18 additions & 0 deletions common/yaml/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@ drake_cc_package_library(
name = "yaml",
visibility = ["//visibility:public"],
deps = [
":yaml_node",
":yaml_read_archive",
":yaml_write_archive",
],
)

drake_cc_library(
name = "yaml_node",
srcs = ["yaml_node.cc"],
hdrs = ["yaml_node.h"],
deps = [
"//common:essential",
],
)

drake_cc_library(
name = "yaml_read_archive",
srcs = ["yaml_read_archive.cc"],
Expand Down Expand Up @@ -57,6 +67,14 @@ drake_cc_library(
],
)

drake_cc_googletest(
name = "yaml_node_test",
deps = [
":yaml_node",
"//common/test_utilities:expect_throws_message",
],
)

drake_cc_googletest(
name = "yaml_performance_test",
deps = [
Expand Down
262 changes: 262 additions & 0 deletions common/yaml/test/yaml_node_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
#include "drake/common/yaml/yaml_node.h"

#include <fmt/format.h>
#include <gtest/gtest.h>

#include "drake/common/test_utilities/expect_throws_message.h"

namespace drake {
namespace yaml {
namespace internal {

// Make pretty googletest output. (Per googletest, this cannot be defined in
// the anonymous namespace.)
static void PrintTo(const NodeType& node_type, std::ostream* os) {
*os << "NodeType::k" << Node::GetTypeString(node_type);
}

namespace {

// Boilerplate for std::visit.
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

// Check the default constructor.
GTEST_TEST(YamlNodeTest, DefaultConstructor) {
Node dut;
EXPECT_EQ(dut.GetType(), NodeType::kScalar);
EXPECT_TRUE(dut.IsScalar());
EXPECT_TRUE(dut.IsEmptyScalar());
EXPECT_EQ(dut.GetScalar(), "");
}

// Sanity check of defaulted operators. We don't need to test them
// exhaustively, because they are defaulted.
GTEST_TEST(YamlNodeTest, DefaultCopyMove) {
Node dut = Node::MakeScalar("foo");

// Copy constructor.
Node foo(dut);
EXPECT_EQ(dut.GetScalar(), "foo");
EXPECT_EQ(foo.GetScalar(), "foo");

// Move constructor.
Node bar(std::move(dut));
EXPECT_EQ(bar.GetScalar(), "foo");
// It is important for performance that the move constructor actually moves
// the stored data, instead of copying it.
EXPECT_EQ(dut.GetScalar(), "");
}

// Parameterize the remainder of the tests across the three possible types.
using Param = std::tuple<NodeType, std::string_view>;
class YamlNodeParamaterizedTest : public testing::TestWithParam<Param> {
protected:
// Returns the test suite's desired type.
NodeType GetExpectedType() {
return std::get<0>(GetParam());
}

// Returns the test suite's desired type string.
std::string_view GetExpectedTypeString() {
return std::get<1>(GetParam());
}

// Returns a new, empty Node with using the test suite's desired type.
Node MakeEmptyDut() {
switch (GetExpectedType()) {
case NodeType::kScalar: return Node::MakeScalar();
case NodeType::kSequence: return Node::MakeSequence();
case NodeType::kMapping: return Node::MakeMapping();
}
DRAKE_UNREACHABLE();
}

// Return a new, non-empty Node with using the test suite's desired type.
Node MakeNonEmptyDut() {
switch (GetExpectedType()) {
case NodeType::kScalar: {
return Node::MakeScalar("foo");
}
case NodeType::kSequence: {
Node result = Node::MakeSequence();
result.Add(Node::MakeScalar("item"));
return result;
}
case NodeType::kMapping: {
Node result = Node::MakeMapping();
result.Add("key", Node::MakeScalar("value"));
return result;
}
}
DRAKE_UNREACHABLE();
}

// Given a function name, returns the expected exception message in case the
// runtime type of the Node is incorect.
std::string GetExpectedCannot(std::string_view operation) {
return fmt::format(
".*Cannot.*{}.*on a {}.*",
operation, GetExpectedTypeString());
}
};

// Check runtime type interrogation.
TEST_P(YamlNodeParamaterizedTest, GetType) {
Node dut = MakeEmptyDut();
EXPECT_EQ(dut.GetType(), GetExpectedType());
EXPECT_EQ(dut.GetTypeString(), GetExpectedTypeString());
EXPECT_EQ(dut.IsScalar(), GetExpectedType() == NodeType::kScalar);
EXPECT_EQ(dut.IsEmptyScalar(), GetExpectedType() == NodeType::kScalar);
EXPECT_EQ(dut.IsSequence(), GetExpectedType() == NodeType::kSequence);
EXPECT_EQ(dut.IsMapping(), GetExpectedType() == NodeType::kMapping);
}

// Check static type string conversion.
TEST_P(YamlNodeParamaterizedTest, StaticTypeString) {
EXPECT_EQ(Node::GetTypeString(GetExpectedType()), GetExpectedTypeString());
}

// Check tag getting and setting.
TEST_P(YamlNodeParamaterizedTest, GetSetTag) {
Node dut = MakeEmptyDut();
EXPECT_EQ(dut.GetTag(), "");
dut.SetTag("tag");
EXPECT_EQ(dut.GetTag(), "tag");
}

// Check (non-)equality as affected by the stored type of empty nodes.
TEST_P(YamlNodeParamaterizedTest, EqualityPerType) {
Node dut = MakeEmptyDut();
EXPECT_EQ(dut == Node::MakeScalar(), dut.IsScalar());
EXPECT_EQ(dut == Node::MakeSequence(), dut.IsSequence());
EXPECT_EQ(dut == Node::MakeMapping(), dut.IsMapping());
}

// Check (non-)equality as affected by the tag.
TEST_P(YamlNodeParamaterizedTest, EqualityPerTag) {
Node dut = MakeEmptyDut();
Node dut2 = MakeEmptyDut();
EXPECT_TRUE(dut == dut2);
dut2.SetTag("tag");
EXPECT_FALSE(dut == dut2);
}

// Check Scalar-specific operations.
TEST_P(YamlNodeParamaterizedTest, ScalarOps) {
Node dut = MakeEmptyDut();
if (dut.IsScalar()) {
Node dut2 = MakeNonEmptyDut();
EXPECT_FALSE(dut == dut2);
EXPECT_EQ(dut2.GetScalar(), "foo");
EXPECT_FALSE(dut2.IsEmptyScalar());
} else {
DRAKE_EXPECT_THROWS_MESSAGE(
dut.GetScalar(), GetExpectedCannot("GetScalar"));
}
}

// Check Sequence-specific operations.
TEST_P(YamlNodeParamaterizedTest, SequenceOps) {
Node dut = MakeEmptyDut();
if (dut.IsSequence()) {
EXPECT_TRUE(dut.GetSequence().empty());
Node dut2 = MakeNonEmptyDut();
EXPECT_FALSE(dut == dut2);
ASSERT_EQ(dut2.GetSequence().size(), 1);
EXPECT_EQ(dut2.GetSequence().front().GetScalar(), "item");
} else {
DRAKE_EXPECT_THROWS_MESSAGE(
dut.GetSequence(), GetExpectedCannot("GetSequence"));
DRAKE_EXPECT_THROWS_MESSAGE(
dut.Add(Node{}), GetExpectedCannot("Add"));
}
}

// Check Mapping-specific operations.
TEST_P(YamlNodeParamaterizedTest, MappingOps) {
Node dut = MakeEmptyDut();
if (dut.IsMapping()) {
EXPECT_TRUE(dut.GetMapping().empty());
DRAKE_EXPECT_THROWS_MESSAGE(
dut.Remove("quux"),
".*No such key.*'quux'.*");
Node dut2 = MakeNonEmptyDut();
EXPECT_FALSE(dut == dut2);
ASSERT_EQ(dut2.GetMapping().size(), 1);
EXPECT_EQ(dut2.GetMapping().begin()->first, "key");
EXPECT_EQ(dut2.At("key").GetScalar(), "value");
DRAKE_EXPECT_THROWS_MESSAGE(
dut2.Add("key", Node::MakeScalar()),
"Cannot .*Add.* duplicate key 'key'");
dut2.Remove("key");
EXPECT_TRUE(dut.GetMapping().empty());
} else {
DRAKE_EXPECT_THROWS_MESSAGE(
dut.GetMapping(), GetExpectedCannot("GetMapping"));
DRAKE_EXPECT_THROWS_MESSAGE(
dut.Add("key", Node{}), GetExpectedCannot("Add"));
DRAKE_EXPECT_THROWS_MESSAGE(
dut.At("key"), GetExpectedCannot("At"));
DRAKE_EXPECT_THROWS_MESSAGE(
dut.Remove("key"), GetExpectedCannot("Remove"));
}
}

// Helper to check visiting.
struct VisitorThatCopies {
void operator()(const Node::ScalarData& data) {
scalar = data.scalar;
}
void operator()(const Node::SequenceData& data) {
sequence = data.sequence;
}
void operator()(const Node::MappingData& data) {
map = data.map;
}

std::optional<std::string> scalar;
std::optional<std::vector<Node>> sequence;
std::optional<std::map<std::string, Node>> map;
};

// Check visiting.
TEST_P(YamlNodeParamaterizedTest, Visiting) {
Node dut = MakeNonEmptyDut();
VisitorThatCopies visitor;
dut.Visit(visitor);
switch (GetExpectedType()) {
case NodeType::kScalar: {
EXPECT_EQ(visitor.scalar, std::optional<std::string>{"foo"});
EXPECT_FALSE(visitor.sequence.has_value());
EXPECT_FALSE(visitor.map.has_value());
return;
}
case NodeType::kSequence: {
ASSERT_EQ(visitor.sequence.value_or(std::vector<Node>{}).size(), 1);
EXPECT_EQ(visitor.sequence->front().GetScalar(), "item");
EXPECT_FALSE(visitor.scalar.has_value());
EXPECT_FALSE(visitor.map.has_value());
return;
}
case NodeType::kMapping: {
ASSERT_EQ(visitor.map.value_or(std::map<std::string, Node>{}).size(), 1);
EXPECT_EQ(visitor.map->begin()->first, "key");
EXPECT_EQ(visitor.map->at("key").GetScalar(), "value");
EXPECT_FALSE(visitor.scalar.has_value());
EXPECT_FALSE(visitor.sequence.has_value());
return;
}
}
DRAKE_UNREACHABLE();
}

INSTANTIATE_TEST_SUITE_P(Suite, YamlNodeParamaterizedTest, testing::Values(
Param(NodeType::kScalar, "Scalar"),
Param(NodeType::kSequence, "Sequence"),
Param(NodeType::kMapping, "Mapping")));

} // namespace
} // namespace internal
} // namespace yaml
} // namespace drake
Loading

0 comments on commit 25499ab

Please sign in to comment.