570 lines
22 KiB
C++
570 lines
22 KiB
C++
#include <array>
|
|
#include <filesystem>
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#include <arrow/array/array_base.h>
|
|
#include <arrow/buffer.h>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/generators/catch_generators.hpp>
|
|
#include <rerun.hpp>
|
|
|
|
#include <rerun/c/rerun.h>
|
|
|
|
#include "error_check.hpp"
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
#define TEST_TAG "[recording_stream]"
|
|
|
|
struct BadComponent {};
|
|
|
|
// Not making this static makes lsan_suppressions.supp miss this.
|
|
// Output of use counter for this shared_ptr indicates that we're not leaking the shared ptr itself.
|
|
// If we do leak it, it's very unclear how that would be happening - somewhere in the FFI transition?
|
|
// But then why would it not show up for anything else? More likely a false positive.
|
|
static std::shared_ptr<arrow::Array> null_arrow_array() {
|
|
return std::make_shared<arrow::NullArray>(1);
|
|
}
|
|
|
|
template <>
|
|
struct rerun::Loggable<BadComponent> {
|
|
static constexpr rerun::ComponentDescriptor Descriptor = "bad!";
|
|
static rerun::Error error;
|
|
|
|
static const std::shared_ptr<arrow::DataType>& arrow_datatype() {
|
|
return rerun::Loggable<rerun::components::Position2D>::arrow_datatype();
|
|
}
|
|
|
|
static rerun::Result<std::shared_ptr<arrow::Array>> to_arrow(const BadComponent*, size_t) {
|
|
return error;
|
|
}
|
|
};
|
|
|
|
rerun::Error rerun::Loggable<BadComponent>::error =
|
|
rerun::Error(rerun::ErrorCode::Unknown, "BadComponent");
|
|
|
|
struct BadArchetype {
|
|
size_t num_instances() const {
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
namespace rerun {
|
|
template <>
|
|
struct AsComponents<BadArchetype> {
|
|
static rerun::Result<Collection<rerun::ComponentBatch>> as_batches(const BadArchetype&) {
|
|
return Loggable<BadComponent>::error;
|
|
}
|
|
};
|
|
} // namespace rerun
|
|
|
|
namespace rerun {
|
|
std::ostream& operator<<(std::ostream& os, StoreKind kind) {
|
|
switch (kind) {
|
|
case rerun::StoreKind::Recording:
|
|
os << "StoreKind::Recording";
|
|
break;
|
|
case rerun::StoreKind::Blueprint:
|
|
os << "StoreKind::Blueprint";
|
|
break;
|
|
default:
|
|
FAIL("Unknown StoreKind");
|
|
break;
|
|
}
|
|
return os;
|
|
}
|
|
} // namespace rerun
|
|
|
|
SCENARIO("RecordingStream can be created, destroyed and lists correct properties", TEST_TAG) {
|
|
const auto kind = GENERATE(rerun::StoreKind::Recording, rerun::StoreKind::Blueprint);
|
|
|
|
GIVEN("recording stream kind" << kind) {
|
|
AND_GIVEN("a valid application id") {
|
|
THEN("creating a new stream does not log an error") {
|
|
rerun::RecordingStream stream = check_logged_error([&] {
|
|
return rerun::RecordingStream("rerun_example_test", std::string_view(), kind);
|
|
});
|
|
|
|
AND_THEN("it does not crash on destruction") {}
|
|
|
|
AND_THEN("it reports the correct kind") {
|
|
CHECK(stream.kind() == kind);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We changed to taking std::string_view instead of const char* and constructing such from nullptr crashes
|
|
// at least on some C++ implementations.
|
|
// If we'd want to support this in earnest we'd have to create out own string_view type.
|
|
//
|
|
// AND_GIVEN("a nullptr for the application id") {
|
|
// THEN("creating a new stream logs a null argument error") {
|
|
// check_logged_error(
|
|
// [&] { rerun::RecordingStream stream(nullptr, kind); },
|
|
// rerun::ErrorCode::UnexpectedNullArgument
|
|
// );
|
|
// }
|
|
// }
|
|
AND_GIVEN("invalid utf8 character sequence for the application id") {
|
|
THEN("creating a new stream logs an invalid string argument error") {
|
|
check_logged_error(
|
|
[&] { rerun::RecordingStream stream("\xc3\x28", std::string_view(), kind); },
|
|
rerun::ErrorCode::InvalidStringArgument
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("RecordingStream can be set as global and thread local", TEST_TAG) {
|
|
for (auto kind : std::array{rerun::StoreKind::Recording, rerun::StoreKind::Blueprint}) {
|
|
GIVEN("a store kind" << kind) {
|
|
WHEN("querying the current one") {
|
|
auto& stream = rerun::RecordingStream::current(kind);
|
|
|
|
THEN("it reports the correct kind") {
|
|
CHECK(stream.kind() == kind);
|
|
}
|
|
|
|
THEN("it is not enabled") {
|
|
CHECK(!stream.is_enabled());
|
|
}
|
|
}
|
|
|
|
WHEN("creating a new stream") {
|
|
rerun::RecordingStream stream("test", std::string_view(), kind);
|
|
|
|
THEN("it can be set as global") {
|
|
stream.set_global();
|
|
}
|
|
THEN("it can be set as thread local") {
|
|
stream.set_thread_local();
|
|
}
|
|
|
|
// TODO(andreas): There's no way of telling right now if the set stream is
|
|
// functional.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("RecordingStream can be used for logging archetypes and components", TEST_TAG) {
|
|
for (auto kind : std::array{rerun::StoreKind::Recording, rerun::StoreKind::Blueprint}) {
|
|
GIVEN("a store kind" << kind) {
|
|
WHEN("creating a new stream") {
|
|
rerun::RecordingStream stream("test", std::string_view(), kind);
|
|
|
|
GIVEN("component batches") {
|
|
auto batch0 = rerun::ComponentBatch::from_loggable<rerun::Position2D>(
|
|
{{1.0, 2.0}, {4.0, 5.0}},
|
|
rerun::Points2D::Descriptor_positions
|
|
)
|
|
.value_or_throw();
|
|
auto batch1 = rerun::ComponentBatch::from_loggable<rerun::Color>(
|
|
{rerun::Color(0xFF0000FF)},
|
|
rerun::Points2D::Descriptor_colors
|
|
)
|
|
.value_or_throw();
|
|
THEN("single component batch can be logged") {
|
|
stream.log("log_archetype-splat", batch0);
|
|
stream.log_static("log_archetype-splat", batch0);
|
|
}
|
|
THEN("component batches can be listed out") {
|
|
stream.log("log_archetype-splat", batch0, batch1);
|
|
stream.log_static("log_archetype-splat", batch0, batch1);
|
|
}
|
|
THEN("a collection of component batches can be logged") {
|
|
rerun::Collection<rerun::ComponentBatch> batches = {batch0, batch1};
|
|
stream.log("log_archetype-splat", batches);
|
|
stream.log_static("log_archetype-splat", batches);
|
|
}
|
|
}
|
|
GIVEN("component batches wrapped in `rerun::Result`") {
|
|
auto batch0 = rerun::ComponentBatch::from_loggable<rerun::Position2D>(
|
|
{{1.0, 2.0}, {4.0, 5.0}},
|
|
rerun::Points2D::Descriptor_positions
|
|
);
|
|
auto batch1 = rerun::ComponentBatch::from_loggable<rerun::Color>(
|
|
{rerun::Color(0xFF0000FF)},
|
|
rerun::Points2D::Descriptor_colors
|
|
);
|
|
THEN("single component batch can be logged") {
|
|
stream.log("log_archetype-splat", batch0);
|
|
stream.log_static("log_archetype-splat", batch0);
|
|
}
|
|
THEN("component batches can be listed out") {
|
|
stream.log("log_archetype-splat", batch0, batch1);
|
|
stream.log_static("log_archetype-splat", batch0, batch1);
|
|
}
|
|
THEN("collection of component batch results can be logged") {
|
|
rerun::Collection<rerun::Result<rerun::ComponentBatch>> batches = {
|
|
batch0,
|
|
batch1,
|
|
};
|
|
stream.log("log_archetype-splat", batches);
|
|
stream.log_static("log_archetype-splat", batches);
|
|
}
|
|
}
|
|
|
|
THEN("an archetype can be logged") {
|
|
stream.log(
|
|
"log_archetype-splat",
|
|
rerun::Points2D({rerun::Vec2D{1.0, 2.0}, rerun::Vec2D{4.0, 5.0}}
|
|
).with_colors(rerun::Color(0xFF0000FF))
|
|
);
|
|
stream.log_static(
|
|
"log_archetype-splat",
|
|
rerun::Points2D({rerun::Vec2D{1.0, 2.0}, rerun::Vec2D{4.0, 5.0}}
|
|
).with_colors(rerun::Color(0xFF0000FF))
|
|
);
|
|
}
|
|
THEN("several archetypes can be logged") {
|
|
stream.log(
|
|
"log_archetype-splat",
|
|
rerun::Points2D({rerun::Vec2D{1.0, 2.0}, rerun::Vec2D{4.0, 5.0}}
|
|
).with_colors(rerun::Color(0xFF0000FF)),
|
|
rerun::Points2D({rerun::Vec2D{1.0, 2.0}, rerun::Vec2D{4.0, 5.0}}
|
|
).with_colors(rerun::Color(0xFF0000FF))
|
|
);
|
|
stream.log_static(
|
|
"log_archetype-splat",
|
|
rerun::Points2D({rerun::Vec2D{1.0, 2.0}, rerun::Vec2D{4.0, 5.0}}
|
|
).with_colors(rerun::Color(0xFF0000FF)),
|
|
rerun::Points2D({rerun::Vec2D{1.0, 2.0}, rerun::Vec2D{4.0, 5.0}}
|
|
).with_colors(rerun::Color(0xFF0000FF))
|
|
);
|
|
}
|
|
|
|
// TODO(andreas): There's no way of telling right now if the set stream is
|
|
// functional and where those messages went.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("RecordingStream can log to file", TEST_TAG) {
|
|
const char* test_path = "build/test_output";
|
|
fs::create_directories(test_path);
|
|
|
|
std::string test_rrd0 = std::string(test_path) + "test-file-0.rrd";
|
|
std::string test_rrd1 = std::string(test_path) + "test-file-1.rrd";
|
|
|
|
fs::remove(test_rrd0);
|
|
fs::remove(test_rrd1);
|
|
|
|
GIVEN("a new RecordingStream") {
|
|
auto stream0 = std::make_unique<rerun::RecordingStream>("test");
|
|
|
|
// We changed to taking std::string_view instead of const char* and constructing such from nullptr crashes
|
|
// at least on some C++ implementations.
|
|
// If we'd want to support this in earnest we'd have to create out own string_view type.
|
|
//
|
|
// AND_GIVEN("a nullptr for the save path") {
|
|
// THEN("then the save call returns a null argument error") {
|
|
// CHECK(stream0->save(nullptr).code == rerun::ErrorCode::UnexpectedNullArgument);
|
|
// }
|
|
// }
|
|
AND_GIVEN("valid save path " << test_rrd0) {
|
|
AND_GIVEN("a directory already existing at this path") {
|
|
fs::create_directory(test_rrd0);
|
|
THEN("then the save call fails") {
|
|
CHECK(
|
|
stream0->save(test_rrd0).code ==
|
|
rerun::ErrorCode::RecordingStreamSaveFailure
|
|
);
|
|
}
|
|
}
|
|
THEN("save call returns no error") {
|
|
REQUIRE(stream0->save(test_rrd0).is_ok());
|
|
|
|
THEN("a new file got immediately created") {
|
|
CHECK(fs::exists(test_rrd0));
|
|
}
|
|
|
|
WHEN("creating a second stream") {
|
|
auto stream1 = std::make_unique<rerun::RecordingStream>("test2");
|
|
|
|
WHEN("saving that one to a different file " << test_rrd1) {
|
|
REQUIRE(stream1->save(test_rrd1).is_ok());
|
|
|
|
WHEN("logging an archetype to the second stream") {
|
|
check_logged_error([&] {
|
|
stream1->log(
|
|
"archetype",
|
|
rerun::Points2D({
|
|
rerun::Vec2D{1.0, 2.0},
|
|
rerun::Vec2D{4.0, 5.0},
|
|
})
|
|
);
|
|
});
|
|
|
|
THEN("after destruction, the second stream produced a bigger file") {
|
|
stream0.reset();
|
|
stream1.reset();
|
|
CHECK(fs::file_size(test_rrd0) < fs::file_size(test_rrd1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void test_logging_to_grpc_connection(const char* url, const rerun::RecordingStream& stream) {
|
|
AND_GIVEN("an invalid url") {
|
|
THEN("connect call fails") {
|
|
CHECK(
|
|
stream.connect_grpc("definitely not valid!").code ==
|
|
rerun::ErrorCode::InvalidServerUrl
|
|
);
|
|
}
|
|
}
|
|
AND_GIVEN("a valid socket url " << url) {
|
|
THEN("connect call returns no error") {
|
|
CHECK(stream.connect_grpc(url).code == rerun::ErrorCode::Ok);
|
|
|
|
WHEN("logging an archetype and then flushing") {
|
|
check_logged_error([&] {
|
|
stream.log(
|
|
"archetype",
|
|
rerun::Points2D({rerun::Vec2D{1.0, 2.0}, rerun::Vec2D{4.0, 5.0}})
|
|
);
|
|
});
|
|
|
|
// The flush should fail, because there is no server on the other side:
|
|
CHECK(
|
|
stream.flush_blocking().code == rerun::ErrorCode::RecordingStreamFlushFailure
|
|
);
|
|
|
|
THEN("does not crash") {
|
|
// No easy way to see if it got sent.
|
|
}
|
|
|
|
THEN("the stream is still valid and we can log more things") {
|
|
// Regression test for https://github.com/rerun-io/rerun/issues/10884
|
|
check_logged_error([&] {
|
|
stream.log("archetype", rerun::Points2D(rerun::Vec2D{1.0, 2.0}));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("RecordingStream can construct LogSinks", TEST_TAG) {
|
|
const char* url = "rerun+http://127.0.0.1:9876/proxy";
|
|
const char* invalid_url = "definitely not valid!";
|
|
const char* test_path = "build/test_output";
|
|
fs::create_directories(test_path);
|
|
|
|
std::string test_rrd0 = std::string(test_path) + "test-file-log-sink-0.rrd";
|
|
|
|
fs::remove_all(test_rrd0);
|
|
|
|
GIVEN("a new RecordingStream") {
|
|
rerun::RecordingStream stream("test-local");
|
|
|
|
AND_GIVEN("valid save path " << test_rrd0) {
|
|
AND_GIVEN("a directory already existing at this path") {
|
|
fs::create_directory(test_rrd0);
|
|
THEN("set_sinks(FileSink) call fails") {
|
|
CHECK(
|
|
stream.set_sinks(rerun::FileSink{test_rrd0}).code ==
|
|
rerun::ErrorCode::RecordingStreamSaveFailure
|
|
);
|
|
}
|
|
fs::remove_all(test_rrd0);
|
|
}
|
|
THEN("set_sinks(FileSink) call returns no error") {
|
|
CHECK(stream.set_sinks(rerun::FileSink{test_rrd0}).code == rerun::ErrorCode::Ok);
|
|
}
|
|
}
|
|
|
|
AND_GIVEN("an invalid url " << invalid_url) {
|
|
THEN("set_sinks(GrpcSink) call fails") {
|
|
CHECK(
|
|
stream.set_sinks(rerun::GrpcSink{invalid_url}).code ==
|
|
rerun::ErrorCode::InvalidServerUrl
|
|
);
|
|
}
|
|
}
|
|
AND_GIVEN("a valid url " << url) {
|
|
THEN("set_sinks(GrpcSink) call returns no error") {
|
|
CHECK(stream.set_sinks(rerun::GrpcSink{url}).code == rerun::ErrorCode::Ok);
|
|
}
|
|
}
|
|
|
|
AND_GIVEN("both a url " << url << " and a save path " << test_rrd0) {
|
|
auto error = stream.set_sinks(rerun::GrpcSink{url}, rerun::FileSink{test_rrd0});
|
|
AND_GIVEN("Error: " << error.description) {
|
|
THEN("set_sinks(GrpcSink, FileSink) call returns no error") {
|
|
CHECK(error.code == rerun::ErrorCode::Ok);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("RecordingStream can connect over grpc", TEST_TAG) {
|
|
const char* url = "rerun+http://127.0.0.1:9876/proxy";
|
|
GIVEN("a new RecordingStream") {
|
|
rerun::RecordingStream stream("test-local");
|
|
test_logging_to_grpc_connection(url, stream);
|
|
}
|
|
WHEN("setting a global RecordingStream and then discarding it") {
|
|
{
|
|
rerun::RecordingStream stream("test-global");
|
|
stream.set_global();
|
|
}
|
|
GIVEN("the current recording stream") {
|
|
test_logging_to_grpc_connection(url, rerun::RecordingStream::current());
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("RecordingStream can serve grpc", TEST_TAG) {
|
|
GIVEN("a new serving RecordingStream") {
|
|
rerun::RecordingStream stream("test-local");
|
|
THEN("serve_grpc call succeeds") {
|
|
CHECK(
|
|
stream.serve_grpc("0.0.0.0", 21521).value_or_throw() ==
|
|
"rerun+http://0.0.0.0:21521/proxy"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("Recording stream handles invalid logging gracefully", TEST_TAG) {
|
|
GIVEN("a new RecordingStream") {
|
|
rerun::RecordingStream stream("test");
|
|
|
|
AND_GIVEN("a valid path") {
|
|
const char* path = "valid";
|
|
|
|
AND_GIVEN("a cell with a null buffer") {
|
|
rerun::ComponentBatch cell = {};
|
|
cell.component_type = 0;
|
|
|
|
THEN("try_log_data_row fails with UnexpectedNullArgument") {
|
|
CHECK(
|
|
stream.try_log_data_row(path, 1, &cell, true).code ==
|
|
rerun::ErrorCode::UnexpectedNullArgument
|
|
);
|
|
}
|
|
}
|
|
AND_GIVEN("a cell with an invalid component type") {
|
|
rerun::ComponentBatch cell = {};
|
|
cell.component_type = RR_COMPONENT_TYPE_HANDLE_INVALID;
|
|
cell.array = null_arrow_array();
|
|
|
|
THEN("try_log_data_row fails with InvalidComponentTypeHandle") {
|
|
CHECK(
|
|
stream.try_log_data_row(path, 1, &cell, true).code ==
|
|
rerun::ErrorCode::InvalidComponentTypeHandle
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("Recording stream handles serialization failure during logging gracefully", TEST_TAG) {
|
|
GIVEN("a new RecordingStream and a valid entity path") {
|
|
rerun::RecordingStream stream("test");
|
|
const char* path = "valid";
|
|
auto& expected_error = rerun::Loggable<BadComponent>::error;
|
|
|
|
AND_GIVEN("an component batch result that failed serialization") {
|
|
const auto component = BadComponent();
|
|
|
|
expected_error.code =
|
|
GENERATE(rerun::ErrorCode::Unknown, rerun::ErrorCode::ArrowStatusCode_TypeError);
|
|
|
|
auto batch_result = rerun::ComponentBatch::from_loggable(
|
|
component,
|
|
rerun::Loggable<BadComponent>::Descriptor
|
|
);
|
|
|
|
THEN("calling log with that batch logs the serialization error") {
|
|
check_logged_error([&] { stream.log(path, batch_result); }, expected_error.code);
|
|
}
|
|
THEN("calling log with a collection wrapping that batch logs the serialization error") {
|
|
check_logged_error(
|
|
[&] { stream.log(path, rerun::Collection{batch_result}); },
|
|
expected_error.code
|
|
);
|
|
}
|
|
}
|
|
AND_GIVEN("an archetype that fails serialization") {
|
|
auto archetype = BadArchetype();
|
|
expected_error.code =
|
|
GENERATE(rerun::ErrorCode::Unknown, rerun::ErrorCode::ArrowStatusCode_TypeError);
|
|
|
|
THEN("calling log_archetype logs the serialization error") {
|
|
check_logged_error([&] { stream.log(path, archetype); }, expected_error.code);
|
|
}
|
|
THEN("calling log_archetype forwards the serialization error") {
|
|
CHECK(stream.try_log(path, archetype) == expected_error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SCENARIO("RecordingStream can set time without errors", TEST_TAG) {
|
|
rerun::RecordingStream stream("test");
|
|
|
|
SECTION("set_time_sequence does not log errors") {
|
|
check_logged_error([&] { stream.set_time_sequence("sequence", 1); });
|
|
}
|
|
|
|
SECTION("set_time_duration does not log errors") {
|
|
using namespace std::chrono_literals;
|
|
check_logged_error([&] { stream.set_time_duration("duration", 1.0s); });
|
|
check_logged_error([&] { stream.set_time_duration("duration", 1000ms); });
|
|
}
|
|
SECTION("set_time_duration_secs does not log errors") {
|
|
check_logged_error([&] { stream.set_time_duration_secs("duration", 1.0); });
|
|
}
|
|
SECTION("set_time_duration_nanos does not log errors") {
|
|
check_logged_error([&] { stream.set_time_duration_nanos("duration", 1); });
|
|
}
|
|
SECTION("set_time_timestamp_secs_since_epoch does not log errors") {
|
|
check_logged_error([&] { stream.set_time_timestamp_secs_since_epoch("capture_time", 1.0); }
|
|
);
|
|
}
|
|
|
|
SECTION("set_time_timestamp_nanos_since_epoch does not log errors") {
|
|
check_logged_error([&] { stream.set_time_timestamp_nanos_since_epoch("capture_time", 1); });
|
|
}
|
|
SECTION("set_time_timestamp does not log errors") {
|
|
check_logged_error([&] {
|
|
stream.set_time_timestamp("timepoint", std::chrono::system_clock::now());
|
|
});
|
|
}
|
|
|
|
SECTION("Resetting time does not log errors") {
|
|
check_logged_error([&] { stream.reset_time(); });
|
|
}
|
|
SECTION("Can set time again after resetting the time") {
|
|
check_logged_error([&] { stream.reset_time(); });
|
|
check_logged_error([&] { stream.set_time_duration_secs("duration", 1.0f); });
|
|
}
|
|
|
|
SECTION("Disabling timeline does not log errors") {
|
|
check_logged_error([&] { stream.disable_timeline("doesn't exist"); });
|
|
check_logged_error([&] { stream.set_time_sequence("exists!", 123); });
|
|
check_logged_error([&] { stream.disable_timeline("exists"); });
|
|
}
|
|
}
|
|
|
|
SCENARIO("Global RecordingStream doesn't cause crashes", TEST_TAG) {
|
|
// This caused a crash on Mac & Linux due to issues with cleanup order of global variables
|
|
// in Rust vs C++.
|
|
// See:
|
|
// * https://github.com/rerun-io/rerun/issues/5697
|
|
// * https://github.com/rerun-io/rerun/issues/5260
|
|
static rerun::RecordingStream global_stream("global");
|
|
}
|