157 lines
4.9 KiB
C++
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);
|
|
}
|