Files
cvmmap-streamer/src/testers/mcap_reader_tester.cpp
T

157 lines
4.9 KiB
C++

#define MCAP_IMPLEMENTATION
#include <mcap/reader.hpp>
#include <foxglove/CompressedVideo.pb.h>
#include <CLI/CLI.hpp>
#include <cstdint>
#include <expected>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <spdlog/spdlog.h>
namespace {
enum class TesterExitCode : int {
Success = 0,
OpenError = 2,
SchemaError = 3,
TopicMismatch = 4,
TimestampError = 5,
ParseError = 6,
FormatMismatch = 7,
EmptyPayload = 8,
ThresholdError = 9,
DumpError = 10,
};
[[nodiscard]]
constexpr int exit_code(TesterExitCode code) {
return static_cast<int>(code);
}
struct Config {
std::string input_path{};
std::optional<std::string> expected_topic{};
std::optional<std::string> expected_format{};
std::optional<std::string> dump_annexb_output{};
std::uint32_t min_messages{1};
};
[[nodiscard]]
std::expected<Config, int> parse_args(int argc, char **argv) {
Config config{};
CLI::App app{"mcap_reader_tester - validate foxglove.CompressedVideo MCAP output"};
app.add_option("input", config.input_path, "Input MCAP path")->required();
app.add_option("--expect-topic", config.expected_topic, "Expected MCAP topic");
app.add_option("--expect-format", config.expected_format, "Expected CompressedVideo format");
app.add_option("--dump-annexb-output", config.dump_annexb_output, "Write concatenated CompressedVideo.data payloads to a file");
app.add_option("--min-messages", config.min_messages, "Minimum expected message count")->check(CLI::PositiveNumber);
try {
app.parse(argc, argv);
} catch (const CLI::ParseError &e) {
return std::unexpected(app.exit(e));
}
return config;
}
}
int main(int argc, char **argv) {
auto config = parse_args(argc, argv);
if (!config) {
return config.error();
}
mcap::McapReader reader{};
const auto open_status = reader.open(config->input_path);
if (!open_status.ok()) {
spdlog::error("failed to open MCAP file '{}': {}", config->input_path, open_status.message);
return exit_code(TesterExitCode::OpenError);
}
std::uint64_t message_count{0};
std::uint64_t previous_log_time{0};
bool saw_log_time{false};
std::optional<std::ofstream> dump_stream{};
if (config->dump_annexb_output) {
dump_stream.emplace(*config->dump_annexb_output, std::ios::binary | std::ios::trunc);
if (!dump_stream->is_open()) {
spdlog::error("failed to open dump output '{}'", *config->dump_annexb_output);
reader.close();
return exit_code(TesterExitCode::DumpError);
}
}
auto message_view = reader.readMessages();
for (auto it = message_view.begin(); it != message_view.end(); ++it) {
if (it->schema == nullptr || it->channel == nullptr) {
spdlog::error("MCAP message missing schema or channel metadata");
reader.close();
return exit_code(TesterExitCode::SchemaError);
}
if (it->schema->encoding != "protobuf" || it->schema->name != "foxglove.CompressedVideo") {
continue;
}
if (it->channel->messageEncoding != "protobuf") {
spdlog::error("unexpected MCAP message encoding: {}", it->channel->messageEncoding);
reader.close();
return exit_code(TesterExitCode::SchemaError);
}
if (config->expected_topic && it->channel->topic != *config->expected_topic) {
spdlog::error("unexpected topic: expected '{}' got '{}'", *config->expected_topic, it->channel->topic);
reader.close();
return exit_code(TesterExitCode::TopicMismatch);
}
if (saw_log_time && it->message.logTime < previous_log_time) {
spdlog::error("non-monotonic logTime detected: {} < {}", it->message.logTime, previous_log_time);
reader.close();
return exit_code(TesterExitCode::TimestampError);
}
foxglove::CompressedVideo message{};
if (!message.ParseFromArray(it->message.data, static_cast<int>(it->message.dataSize))) {
spdlog::error("failed to parse foxglove.CompressedVideo payload");
reader.close();
return exit_code(TesterExitCode::ParseError);
}
if (config->expected_format && message.format() != *config->expected_format) {
spdlog::error("unexpected format: expected '{}' got '{}'", *config->expected_format, message.format());
reader.close();
return exit_code(TesterExitCode::FormatMismatch);
}
if (message.data().empty()) {
spdlog::error("compressed video payload is empty");
reader.close();
return exit_code(TesterExitCode::EmptyPayload);
}
if (dump_stream) {
dump_stream->write(message.data().data(), static_cast<std::streamsize>(message.data().size()));
if (!dump_stream->good()) {
spdlog::error("failed to write Annex B dump to '{}'", *config->dump_annexb_output);
reader.close();
return exit_code(TesterExitCode::DumpError);
}
}
previous_log_time = it->message.logTime;
saw_log_time = true;
message_count += 1;
}
reader.close();
if (message_count < config->min_messages) {
spdlog::error("message threshold not met: {} < {}", message_count, config->min_messages);
return exit_code(TesterExitCode::ThresholdError);
}
spdlog::info("validated {} foxglove.CompressedVideo MCAP messages", message_count);
return exit_code(TesterExitCode::Success);
}