blob: 79309b0cde43ebd9d5fa27d2fc197bc2cf9968d0 [file] [log] [blame]
#include "aos/util/config_validator_lib.h"
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <initializer_list>
#include <map>
#include <memory>
#include <ostream>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "absl/flags/declare.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "flatbuffers/buffer.h"
#include "flatbuffers/detached_buffer.h"
#include "flatbuffers/string.h"
#include "flatbuffers/vector.h"
#include "gtest/gtest.h"
#include "aos/events/event_loop.h"
#include "aos/events/logging/log_reader.h"
#include "aos/events/logging/logfile_sorting.h"
#include "aos/events/logging/logfile_utils.h"
#include "aos/events/simulated_event_loop.h"
#include "aos/flatbuffers/builder.h"
#include "aos/flatbuffers/static_vector.h"
#include "aos/json_to_flatbuffer.h"
#include "aos/network/remote_message_generated.h"
#include "aos/network/timestamp_channel.h"
#include "aos/testing/tmpdir.h"
#include "aos/util/config_validator_config_static.h"
#include "aos/util/file.h"
#include "aos/util/simulation_logger.h"
ABSL_DECLARE_FLAG(bool, validate_timestamp_logger_nodes);
namespace aos::util {
namespace {
void RunSimulationAndExit(const aos::Configuration *config) {
aos::SimulatedEventLoopFactory factory(config);
factory.RunFor(std::chrono::seconds(1));
std::exit(EXIT_SUCCESS);
}
// Checks if either the node is in the specified list of node names or if the
// list is empty (in which case it is treated as matching all nodes).
bool NodeInList(
const flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>> *list,
const aos::Node *node) {
if (list == nullptr || list->size() == 0) {
return true;
}
for (const flatbuffers::String *name : *list) {
if (name->string_view() == node->name()->string_view()) {
return true;
}
}
return false;
}
} // namespace
void ConfigIsValid(const aos::Configuration *config,
const ConfigValidatorConfig *validation_config_raw) {
ASSERT_TRUE(config->has_channels())
<< "An AOS config must have channels. If you have a valid use-case for "
"channels with no channels, please write a design proposal.";
aos::fbs::Builder<ConfigValidatorConfigStatic> validation_config;
CHECK(validation_config->FromFlatbuffer(validation_config_raw));
if (validation_config_raw->has_logging() &&
validation_config_raw->logging()->validate_individual_node_loggers() &&
configuration::MultiNode(config)) {
if (!validation_config->logging()->has_logger_sets()) {
validation_config->mutable_logging()->add_logger_sets();
}
auto logger_sets =
validation_config->mutable_logging()->mutable_logger_sets();
for (const aos::Node *node : configuration::GetNodes(config)) {
CHECK(logger_sets->reserve(logger_sets->size() + 1));
auto logger_set = logger_sets->emplace_back();
CHECK(logger_set->add_loggers()->FromFlatbuffer({node->name()->str()}));
CHECK(logger_set->add_replay_nodes()->FromFlatbuffer(
{node->name()->str()}));
}
}
// First, we do some sanity checks--these are likely to indicate a malformed
// config, and so catching them early with a clear error message is likely to
// help.
// The set of all channels that are required by the channels that are
// configured--these are the remote timestamp channels that *must* be present,
// and ideally there are no other channels present.
std::set<const Channel *> required_timestamp_channels;
// The set of all channels that *look* like remote timestamp channels. This
// may include channels that are improperly configured and thus have typos &
// aren't actually going to do anything at runtime.
std::set<const Channel *> configured_timestamp_channels;
bool validation_failed = false;
for (size_t channel_index = 0; channel_index < config->channels()->size();
++channel_index) {
const aos::Channel *channel = config->channels()->Get(channel_index);
ASSERT_TRUE(channel->has_name()) << "All AOS channels must have a name.";
ASSERT_TRUE(channel->has_type()) << "All AOS channels must have a type.";
const bool channel_looks_like_remote_message_channel =
channel->type()->string_view() ==
message_bridge::RemoteMessage::GetFullyQualifiedName();
const bool check_for_not_logged_channels =
!validation_config->has_logging() ||
validation_config->AsFlatbuffer().logging()->all_channels_logged();
const bool channel_is_not_logged =
channel->logger() == aos::LoggerConfig::NOT_LOGGED;
if (check_for_not_logged_channels) {
if (channel_looks_like_remote_message_channel != channel_is_not_logged) {
LOG(WARNING)
<< "Channel " << configuration::StrippedChannelToString(channel)
<< " is " << EnumNameLoggerConfig(channel->logger()) << " but "
<< (channel_looks_like_remote_message_channel ? "is" : "is not")
<< " a remote timestamp channel. This is almost certainly wrong.";
validation_failed = true;
}
}
if (channel_looks_like_remote_message_channel) {
configured_timestamp_channels.insert(channel);
} else {
if (channel->has_destination_nodes()) {
// TODO(james): Technically the timestamp finder should receive a
// non-empty application name. However, there are no known users that
// care at this moment.
message_bridge::ChannelTimestampFinder timestamp_finder(
config, "",
configuration::GetNode(config,
channel->source_node()->string_view()));
for (const Connection *connection : *channel->destination_nodes()) {
switch (connection->timestamp_logger()) {
case LoggerConfig::NOT_LOGGED:
case LoggerConfig::LOCAL_LOGGER:
if (connection->has_timestamp_logger_nodes()) {
LOG(WARNING)
<< "Connections that are "
<< EnumNameLoggerConfig(connection->timestamp_logger())
<< " should not have remote timestamp logger nodes "
"populated. This is for the connection to "
<< connection->name()->string_view() << " on "
<< configuration::StrippedChannelToString(channel);
validation_failed = true;
}
break;
case LoggerConfig::REMOTE_LOGGER:
case LoggerConfig::LOCAL_AND_REMOTE_LOGGER:
if (!connection->has_timestamp_logger_nodes() ||
connection->timestamp_logger_nodes()->size() != 1 ||
connection->timestamp_logger_nodes()->Get(0)->string_view() !=
channel->source_node()->string_view()) {
LOG(WARNING)
<< "Connections that are "
<< EnumNameLoggerConfig(connection->timestamp_logger())
<< " should have exactly 1 remote timestamp logger node "
"populated, and that node should be the source_node ("
<< channel->source_node()->string_view()
<< "). This is for the connection to "
<< connection->name()->string_view() << " on "
<< configuration::StrippedChannelToString(channel);
validation_failed = true;
}
// TODO(james): This will be overly noisy, as it ends up
// CHECK-failing.
const Channel *found_channel =
timestamp_finder.ForChannel(channel, connection);
CHECK(found_channel != nullptr);
required_timestamp_channels.insert(found_channel);
break;
}
}
}
}
}
// Check that all of the things that look like timestamp channels are indeed
// required.
// Note: Because ForChannel() will die if a required channel is not present,
// we do not do a separate check that all the required channels exist.
for (const auto &channel : configured_timestamp_channels) {
if (required_timestamp_channels.count(channel) == 0) {
LOG(WARNING) << "Timestamp channel "
<< configuration::StrippedChannelToString(channel)
<< " was specified in the config but is not used.";
validation_failed = true;
}
}
if (validation_failed) {
FAIL() << "Remote timestamp linting failed.";
return;
}
// Because the most common way for simulation to fail involves it dying, force
// it to fail in a slightly more controlled manner.
ASSERT_EXIT(RunSimulationAndExit(config),
::testing::ExitedWithCode(EXIT_SUCCESS), "");
if (!validation_config->has_logging() || !configuration::MultiNode(config)) {
return;
}
// We will run all the logger configs in two modes:
// 1) We don't send any data on any non-infrastructure channels; this confirms
// that the logs are readable in the absence of any user applications being
// present.
// 2) We confirm that we can generate a good logfile that actually has data
// on every channel (some checks in the LogReader may not get hit if there
// is no data on a given channel).
const std::string log_path = aos::testing::TestTmpDir() + "/logs/";
for (const bool send_data_on_channels : {false, true}) {
SCOPED_TRACE(send_data_on_channels);
// Single nodes (multi-nodes with node count = 1) will not produce readable
// logs in the absense of data.
if (!send_data_on_channels && (configuration::NodesCount(config) == 1u)) {
continue;
}
// Send timing report when we are sending data.
const bool do_skip_timing_report = !send_data_on_channels;
for (const LoggerNodeSetValidationStatic &logger_set :
*validation_config->logging()->logger_sets()) {
SCOPED_TRACE(aos::FlatbufferToJson(&logger_set.AsFlatbuffer()));
aos::SimulatedEventLoopFactory factory(config);
std::vector<std::unique_ptr<LoggerState>> loggers;
if (logger_set.has_loggers() && logger_set.loggers()->size() > 0) {
std::vector<std::string> logger_nodes;
for (const auto &node : *logger_set.loggers()) {
logger_nodes.push_back(node.str());
}
loggers = MakeLoggersForNodes(&factory, logger_nodes, log_path,
do_skip_timing_report);
} else {
loggers =
MakeLoggersForAllNodes(&factory, log_path, do_skip_timing_report);
}
std::vector<std::unique_ptr<EventLoop>> test_loops;
std::map<std::string, std::vector<std::unique_ptr<RawSender>>>
test_senders;
if (send_data_on_channels) {
// Make a sender on every non-infrastructure channel on every node
// (including channels that may not be observable by the current logger
// set).
for (const aos::Node *node : configuration::GetNodes(config)) {
test_loops.emplace_back(factory.MakeEventLoop("", node));
for (const aos::Channel *channel : *config->channels()) {
// TODO(james): Make a more sophisticated check for "infrastructure"
// channels than just looking for a "/aos" in the channel--we don't
// accidentally want to spam nonsense data onto any timestamp
// channels, though.
if (configuration::ChannelIsSendableOnNode(channel, node) &&
channel->name()->str().find("/aos") == std::string::npos &&
channel->logger() != LoggerConfig::NOT_LOGGED) {
test_senders[node->name()->str()].emplace_back(
test_loops.back()->MakeRawSender(channel));
RawSender *sender =
test_senders[node->name()->str()].back().get();
test_loops.back()->OnRun([sender, channel]() {
flatbuffers::DetachedBuffer buffer =
JsonToFlatbuffer("{}", channel->schema());
sender->CheckOk(sender->Send(buffer.data(), buffer.size()));
});
}
}
}
}
factory.RunFor(std::chrono::seconds(2));
// Get all of the loggers to close before trying to read the logfiles.
loggers.clear();
// Confirm that we can read the log, and that if we put data in it that we
// can find data on all the nodes that the user cares about.
logger::LogReader reader(logger::SortParts(logger::FindLogs(log_path)));
SimulatedEventLoopFactory replay_factory(reader.configuration());
reader.RegisterWithoutStarting(&replay_factory);
// Find every channel we deliberately sent data on, and if it is for a
// node that we care about, confirm that we get it during replay.
std::vector<std::unique_ptr<EventLoop>> replay_loops;
for (const aos::Node *node :
configuration::GetNodes(replay_factory.configuration())) {
// If the user doesn't care about this node, don't check it.
if (!NodeInList(logger_set.has_replay_nodes()
? logger_set.replay_nodes()->AsFlatbufferVector()
: nullptr,
node)) {
continue;
}
replay_loops.emplace_back(replay_factory.MakeEventLoop("", node));
}
std::vector<std::pair<const aos::Node *, std::unique_ptr<RawFetcher>>>
fetchers;
for (const auto &node_senders : test_senders) {
for (const auto &sender : node_senders.second) {
for (auto &loop : replay_loops) {
if (configuration::ChannelIsReadableOnNode(sender->channel(),
loop->node())) {
fetchers.push_back(std::make_pair(
loop->node(),
loop->MakeRawFetcher(configuration::GetChannel(
replay_factory.configuration(), sender->channel(),
loop->name(), loop->node()))));
}
}
}
}
replay_factory.Run();
for (auto &pair : fetchers) {
EXPECT_TRUE(pair.second->Fetch())
<< "Failed to log or replay any data on "
<< configuration::StrippedChannelToString(pair.second->channel())
<< " reading from " << logger::MaybeNodeName(pair.first)
<< " with source node "
<< (pair.second->channel()->has_source_node()
? pair.second->channel()->source_node()->string_view()
: "")
<< ".";
}
reader.Deregister();
// Clean up the logs.
UnlinkRecursive(log_path);
}
}
}
} // namespace aos::util