blob: 106f40e60ca0cf675d8327b44365b2db24c4dfda [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fuchsia/diagnostics/cpp/fidl.h>
#include <fuchsia/logger/cpp/fidl.h>
#include <lib/stdcompat/optional.h>
#include <gtest/gtest.h>
#include <src/lib/diagnostics/stream/cpp/log_message.h>
#include <src/lib/fsl/vmo/strings.h>
#include <src/lib/fxl/strings/string_printf.h>
using diagnostics::stream::ConvertFormattedContentToLogMessages;
using fuchsia::diagnostics::FormattedContent;
using fuchsia::logger::LogMessage;
namespace {
TEST(LogMessage, Empty) {
FormattedContent content;
ASSERT_TRUE(fsl::VmoFromString("[]", &content.json()));
auto messages = ConvertFormattedContentToLogMessages(std::move(content));
ASSERT_TRUE(messages.is_ok());
EXPECT_EQ(0u, messages.value().size());
}
TEST(LogMessage, WrongType) {
FormattedContent content;
EXPECT_TRUE(ConvertFormattedContentToLogMessages(std::move(content)).is_error());
}
struct ValidationTestCase {
std::string input;
// If set, check that the conversion function returned this error instead of a vector.
cpp17::optional<std::string> expected_overall_error = cpp17::nullopt;
// If set, assert on the exact number of messages returned.
cpp17::optional<int> expected_count = cpp17::nullopt;
// If set, assert that message has the given error.
cpp17::optional<std::string> expected_error = cpp17::nullopt;
// If set, assert that message is OK and matches the given value.
cpp17::optional<std::string> expected_message = cpp17::nullopt;
// If set, assert that message is OK and tags match the given value.
cpp17::optional<std::vector<std::string>> expected_tags = cpp17::nullopt;
// If set, assert that message is OK and dropped logs match the given value.
cpp17::optional<uint32_t> dropped_logs = cpp17::nullopt;
};
void RunValidationCases(std::vector<ValidationTestCase> cases) {
for (const auto& test_case : cases) {
FormattedContent content;
ASSERT_TRUE(fsl::VmoFromString(test_case.input, &content.json()));
auto results = ConvertFormattedContentToLogMessages(std::move(content));
EXPECT_EQ(!test_case.expected_overall_error.has_value(), results.is_ok()) << test_case.input;
if (test_case.expected_overall_error.has_value()) {
ASSERT_TRUE(results.is_error());
EXPECT_EQ(test_case.expected_overall_error.value(), results.error());
} else if (results.is_ok()) {
if (test_case.expected_count.has_value()) {
EXPECT_EQ(static_cast<size_t>(test_case.expected_count.value()), results.value().size())
<< test_case.input;
}
if (test_case.expected_message.has_value()) {
ASSERT_LE(1u, results.value().size())
<< "Must have at least one output to check expected message against";
for (const auto& res : results.value()) {
ASSERT_TRUE(res.is_ok()) << test_case.input;
EXPECT_EQ(test_case.expected_message.value(), res.value().msg) << test_case.input;
}
}
if (test_case.expected_tags.has_value()) {
ASSERT_LE(1u, results.value().size())
<< "Must have at least one output to check expected tags against";
for (const auto& res : results.value()) {
ASSERT_TRUE(res.is_ok()) << test_case.input;
ASSERT_EQ(test_case.expected_tags.value().size(), res.value().tags.size())
<< test_case.input;
for (size_t i = 0; i < res.value().tags.size(); i++) {
EXPECT_EQ(test_case.expected_tags.value()[i], res.value().tags[i])
<< "tag index " << i << " " << test_case.input;
}
}
}
if (test_case.dropped_logs.has_value()) {
ASSERT_LE(1u, results.value().size())
<< "Must have at least one output to check expected dropped logs";
for (const auto& res : results.value()) {
ASSERT_TRUE(res.is_ok()) << test_case.input;
EXPECT_EQ(test_case.dropped_logs.value(), res.value().dropped_logs) << test_case.input;
}
}
if (test_case.expected_error.has_value()) {
ASSERT_LE(1u, results.value().size())
<< "Must have at least one output to check expected errors";
for (const auto& res : results.value()) {
ASSERT_TRUE(res.is_error()) << test_case.input;
EXPECT_EQ(test_case.expected_error.value(), res.error()) << test_case.input;
}
}
}
}
}
const char PAYLOAD_TEMPLATE[] = R"JSON(
[
{
"metadata": {
"timestamp": 1000,
"severity": "Info"
},
"payload": %s
}
]
)JSON";
const char VALID_MESSAGE_FOR_SEVERITY[] = R"JSON(
[
{
"metadata": {
"timestamp": 1000,
"severity": "%s"
},
"payload": {
"root": {
"message": "Hello world",
"pid": 200,
"tid": 300,
"tag": "a",
"arbitrary_kv": 1024
}
}
}
]
)JSON";
const char TWO_FLAT_VALID_INFO_MESSAGES[] = R"JSON(
[
{
"metadata": {
"timestamp": 1000,
"severity": "Info"
},
"payload": {
"message": "Hello world",
"pid": 200,
"tid": 300,
"tag": "a",
"arbitrary_kv": 1024
}
},
{
"metadata": {
"timestamp": 1000,
"severity": "Info"
},
"payload": {
"message": "Hello world",
"pid": 200,
"tid": 300,
"tag": "a",
"arbitrary_kv": 1024
}
}
]
)JSON";
TEST(LogMessage, Valid) {
// Ensure that both flat and nested (under "root") messages work.
FormattedContent content;
ASSERT_TRUE(
fsl::VmoFromString(fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "Info"), &content.json()));
auto one_message_result = ConvertFormattedContentToLogMessages(std::move(content));
content = {};
ASSERT_TRUE(fsl::VmoFromString(TWO_FLAT_VALID_INFO_MESSAGES, &content.json()));
auto two_message_result = ConvertFormattedContentToLogMessages(std::move(content));
ASSERT_TRUE(one_message_result.is_ok());
ASSERT_TRUE(two_message_result.is_ok());
std::vector<fit::result<LogMessage, std::string>> messages;
messages.insert(messages.end(), one_message_result.value().begin(),
one_message_result.value().end());
messages.insert(messages.end(), two_message_result.value().begin(),
two_message_result.value().end());
ASSERT_EQ(3u, messages.size());
for (const auto& val : messages) {
ASSERT_TRUE(val.is_ok());
EXPECT_EQ("Hello world arbitrary_kv=1024", val.value().msg);
EXPECT_EQ(200u, val.value().pid);
EXPECT_EQ(300u, val.value().tid);
ASSERT_EQ(1u, val.value().tags.size());
EXPECT_EQ("a", val.value().tags[0]);
EXPECT_EQ(1000u, val.value().time);
EXPECT_EQ(0u, val.value().dropped_logs);
EXPECT_EQ(static_cast<int32_t>(fuchsia::logger::LogLevelFilter::INFO), val.value().severity);
}
}
TEST(LogMessage, ValidSeverityTests) {
struct TestCase {
std::string input;
fuchsia::logger::LogLevelFilter severity;
};
std::vector<TestCase> cases;
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "Info"),
.severity = fuchsia::logger::LogLevelFilter::INFO,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "info"),
.severity = fuchsia::logger::LogLevelFilter::INFO,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "INFO"),
.severity = fuchsia::logger::LogLevelFilter::INFO,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "TRACE"),
.severity = fuchsia::logger::LogLevelFilter::TRACE,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "DEBUG"),
.severity = fuchsia::logger::LogLevelFilter::DEBUG,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "WARN"),
.severity = fuchsia::logger::LogLevelFilter::WARN,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "ERROR"),
.severity = fuchsia::logger::LogLevelFilter::ERROR,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "FATAL"),
.severity = fuchsia::logger::LogLevelFilter::FATAL,
});
cases.emplace_back(TestCase{
.input = fxl::StringPrintf(VALID_MESSAGE_FOR_SEVERITY, "unknown"),
.severity = fuchsia::logger::LogLevelFilter::INFO,
});
for (const auto& test_case : cases) {
FormattedContent content;
ASSERT_TRUE(fsl::VmoFromString(test_case.input, &content.json()));
auto messages = ConvertFormattedContentToLogMessages(std::move(content));
ASSERT_TRUE(messages.is_ok());
ASSERT_EQ(1u, messages.value().size());
ASSERT_TRUE(messages.value()[0].is_ok());
EXPECT_EQ(static_cast<int32_t>(test_case.severity), messages.value()[0].value().severity);
}
}
const char META_TEMPLATE[] = R"JSON(
[
{
"metadata": {
%s
},
"payload": {
"root": {
"message": "Hello world",
"pid": 200,
"tid": 300,
"arbitrary_kv": 1024
}
}
}
]
)JSON";
const char MONIKER_TEMPLATE[] = R"JSON(
[
{
"moniker": "%s",
"metadata": {
"timestamp": 1000,
"severity": "INFO"
},
"payload": {
"root": {
"message": "Hello world",
"pid": 200,
"tid": 300,
"arbitrary_kv": 1024
}
}
}
]
)JSON";
TEST(LogMessage, MetadataValidation) {
std::vector<ValidationTestCase> cases;
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(META_TEMPLATE, R"("severity": "INFO", "timestamp": 1000)"),
.expected_count = 1});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(META_TEMPLATE, R"("severity": "INFO", "timestamp": "string")"),
.expected_error = "Expected metadata.timestamp key",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(META_TEMPLATE, R"("severity": "INFO")"),
.expected_error = "Expected metadata.timestamp key",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(META_TEMPLATE, R"("timestamp": 1000)"),
.expected_error = "Expected metadata.severity key",
});
RunValidationCases(std::move(cases));
}
const char ROOT_TEMPLATE[] = R"JSON(
[
{
"metadata": {
"timestamp": 1000,
"severity": "Info"
},
"payload": {
"root": %s
}
}
]
)JSON";
TEST(LogMessage, PayloadValidation) {
std::vector<ValidationTestCase> cases;
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(ROOT_TEMPLATE, R"({"message": "Hello"})"),
.expected_count = 1,
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(ROOT_TEMPLATE, R"("invalid type")"),
.expected_error = "Expected payload.root to be an object if present",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(ROOT_TEMPLATE, R"(1000)"),
.expected_error = "Expected payload.root to be an object if present",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"message": "Hello"})"),
.expected_count = 1,
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"("invalid type")"),
.expected_error = "Expected metadata and payload objects",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"(1000)"),
.expected_error = "Expected metadata and payload objects",
});
RunValidationCases(std::move(cases));
}
TEST(LogMessage, JsonValidation) {
std::vector<ValidationTestCase> cases;
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(ROOT_TEMPLATE, R"({"message": "Hello"})"),
.expected_count = 1,
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(ROOT_TEMPLATE, R"({"message": "Hello"},)"),
.expected_overall_error =
"Failed to parse content as JSON. Offset 139: Missing a name for object member.",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"message": "Hello"})"),
.expected_count = 1,
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"message": "Hello"},)"),
.expected_overall_error =
"Failed to parse content as JSON. Offset 121: Missing a name for object member.",
});
RunValidationCases(std::move(cases));
}
TEST(LogMessage, FileValidation) {
std::vector<ValidationTestCase> cases;
cases.emplace_back(ValidationTestCase{
.input = "[]",
.expected_count = 0,
});
cases.emplace_back(ValidationTestCase{
.input = "[3]",
.expected_count = 1,
.expected_error = "Value is not an object",
});
cases.emplace_back(ValidationTestCase{
.input = R"(["a", "b"])",
.expected_count = 2,
.expected_error = "Value is not an object",
});
cases.emplace_back(ValidationTestCase{
.input = R"({"payload": {}})",
.expected_overall_error = "Expected content to contain an array",
});
RunValidationCases(std::move(cases));
}
TEST(LogMessage, MessageFormatting) {
std::vector<ValidationTestCase> cases;
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"message": "Hello, world"})"),
.expected_message = "Hello, world",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"message": "Hello, world", "kv": "ok"})"),
.expected_message = "Hello, world kv=ok",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(
PAYLOAD_TEMPLATE,
R"({"message": "Hello, world", "int": -5, "intp": 5, "repeat": 2, "repeat": 2, "uint": 9223400000000000000, "float": 5.25})"),
.expected_message =
"Hello, world int=-5 intp=5 repeat=2 repeat=2 uint=9223400000000000000 float=5.25",
});
RunValidationCases(std::move(cases));
}
TEST(LogMessage, Tags) {
std::vector<ValidationTestCase> cases;
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tag": "hello"})"),
.expected_tags = std::vector<std::string>{"hello"},
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tags": "hello"})"),
.expected_tags = std::vector<std::string>{"hello"},
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tag": "hello", "tag": "world"})"),
.expected_tags = std::vector<std::string>{"hello", "world"},
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tags": "hello", "tags": "world"})"),
.expected_tags = std::vector<std::string>{"hello", "world"},
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tags": ["hello", "world"]})"),
.expected_tags = std::vector<std::string>{"hello", "world"},
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tag": ["hello", "world"]})"),
.expected_error = "Tag field must contain a single string value",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tags": ["hello", 3]})"),
.expected_error = "Tags array must contain strings",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tag": "hello", "tag": 3})"),
.expected_error = "Tag field must contain a single string value",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(PAYLOAD_TEMPLATE, R"({"tags": "hello", "tags": 3})"),
.expected_error = "Tags must be a string or array of strings",
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(MONIKER_TEMPLATE, "test.cmx"),
.expected_tags = std::vector<std::string>{"test.cmx"},
});
RunValidationCases(std::move(cases));
}
TEST(LogMessage, DroppedLogs) {
std::vector<ValidationTestCase> cases;
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(META_TEMPLATE,
R"("timestamp": 1000, "severity": "INFO", "errors": ["test"])"),
.dropped_logs = 0,
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(
META_TEMPLATE,
R"("timestamp": 1000, "severity": "INFO", "errors": [{"dropped_logs": {"count": 100}}])"),
.dropped_logs = 100,
});
cases.emplace_back(ValidationTestCase{
.input = fxl::StringPrintf(META_TEMPLATE,
R"(
"timestamp": 1000,
"severity": "INFO",
"errors": [
{"dropped_logs": {"count": 100}},
{"dropped_logs": {"count": 200}}
])"),
.dropped_logs = 300,
});
RunValidationCases(std::move(cases));
}
} // namespace