use crate::{
heap_buffer::HeapBuffer,
pb_msg::{PbMsg, PbMsgWriter},
protos::{
common::data_source_descriptor::DataSourceDescriptor, trace::trace_packet::TracePacket,
},
stream_writer::StreamWriter,
};
use perfetto_sdk_sys::*;
use std::{
cell::RefCell,
collections::HashMap,
default::Default,
marker::PhantomData,
os::raw::c_void,
ptr,
sync::{
Mutex, OnceLock,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum DataSourceError {
#[error("Data source have already been registered.")]
AlreadyRegisteredError,
#[error("Failed to register data source.")]
RegisterError,
}
pub struct OnSetupArgs {
_args: *mut PerfettoDsOnSetupArgs,
}
type OnSetupCallback = Box<dyn FnMut(u32, &[u8], &mut OnSetupArgs) + Send + Sync + 'static>;
pub struct OnStartArgs {
_args: *mut PerfettoDsOnStartArgs,
}
type OnStartCallback = Box<dyn FnMut(u32, &mut OnStartArgs) + Send + Sync + 'static>;
#[must_use = "dropping StopGuard immediately defeats its purpose"]
pub struct StopGuard {
async_stopper: *mut PerfettoDsAsyncStopper,
}
impl Drop for StopGuard {
fn drop(&mut self) {
unsafe {
PerfettoDsStopDone(self.async_stopper);
}
}
}
unsafe impl Send for StopGuard {}
unsafe impl Sync for StopGuard {}
pub struct OnStopArgs {
args: *mut PerfettoDsOnStopArgs,
}
impl OnStopArgs {
#[must_use = "StopGuard must be kept alive until the desired stop point"]
pub fn postpone(&mut self) -> StopGuard {
assert!(!self.args.is_null());
let async_stopper = unsafe { PerfettoDsOnStopArgsPostpone(self.args) };
StopGuard { async_stopper }
}
}
type OnStopCallback = Box<dyn FnMut(u32, &mut OnStopArgs) + Send + Sync + 'static>;
#[must_use = "dropping FlushGuard immediately defeats its purpose"]
pub struct FlushGuard {
async_flusher: *mut PerfettoDsAsyncFlusher,
}
impl Drop for FlushGuard {
fn drop(&mut self) {
unsafe {
PerfettoDsFlushDone(self.async_flusher);
}
}
}
unsafe impl Send for FlushGuard {}
unsafe impl Sync for FlushGuard {}
pub struct OnFlushArgs {
args: *mut PerfettoDsOnFlushArgs,
}
impl OnFlushArgs {
#[must_use = "FlushGuard must be kept alive until the desired stop point"]
pub fn postpone(&mut self) -> FlushGuard {
assert!(!self.args.is_null());
let async_flusher = unsafe { PerfettoDsOnFlushArgsPostpone(self.args) };
FlushGuard { async_flusher }
}
}
type OnFlushCallback = Box<dyn FnMut(u32, &mut OnFlushArgs) + Send + Sync + 'static>;
#[derive(Default, PartialEq)]
pub enum DataSourceBufferExhaustedPolicy {
#[default]
Drop,
StallAndAbort,
StallAndDrop,
}
pub(crate) trait ToDsBufferExhaustedPolicy {
fn to_ds_policy(&self) -> PerfettoDsBufferExhaustedPolicy;
}
impl ToDsBufferExhaustedPolicy for DataSourceBufferExhaustedPolicy {
fn to_ds_policy(&self) -> PerfettoDsBufferExhaustedPolicy {
use DataSourceBufferExhaustedPolicy::*;
match self {
Drop => PerfettoDsBufferExhaustedPolicy_PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP,
StallAndAbort => {
PerfettoDsBufferExhaustedPolicy_PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT
}
StallAndDrop => {
PerfettoDsBufferExhaustedPolicy_PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_DROP
}
}
}
}
#[derive(Default)]
struct DsCallbacks {
on_setup: Option<OnSetupCallback>,
on_start: Option<OnStartCallback>,
on_stop: Option<OnStopCallback>,
on_flush: Option<OnFlushCallback>,
}
#[derive(Default)]
pub struct DataSourceArgs {
callbacks: DsCallbacks,
buffer_exhausted_policy: DataSourceBufferExhaustedPolicy,
buffer_exhausted_policy_configurable: bool,
will_notify_on_stop: bool,
handles_incremental_state_clear: bool,
}
#[derive(Default)]
#[must_use = "This is a builder; remember to call `.build()` (or keep chaining)."]
pub struct DataSourceArgsBuilder {
args: DataSourceArgs,
}
impl DataSourceArgsBuilder {
pub fn new() -> Self {
Self::default()
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn buffer_exhausted_policy(
mut self,
buffer_exhausted_policy: DataSourceBufferExhaustedPolicy,
) -> Self {
self.args.buffer_exhausted_policy = buffer_exhausted_policy;
self
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn buffer_exhausted_policy_configurable(
mut self,
buffer_exhausted_policy_configurable: bool,
) -> Self {
self.args.buffer_exhausted_policy_configurable = buffer_exhausted_policy_configurable;
self
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn will_notify_on_stop(mut self, will_notify_on_stop: bool) -> Self {
self.args.will_notify_on_stop = will_notify_on_stop;
self
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn handles_incremental_state_clear(
mut self,
handles_incremental_state_clear: bool,
) -> Self {
self.args.handles_incremental_state_clear = handles_incremental_state_clear;
self
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn on_setup<F>(mut self, cb: F) -> Self
where
F: FnMut(u32, &[u8], &mut OnSetupArgs) + Send + Sync + 'static,
{
self.args.callbacks.on_setup = Some(Box::new(cb));
self
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn on_start<F>(mut self, cb: F) -> Self
where
F: FnMut(u32, &mut OnStartArgs) + Send + Sync + 'static,
{
self.args.callbacks.on_start = Some(Box::new(cb));
self
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn on_stop<F>(mut self, cb: F) -> Self
where
F: FnMut(u32, &mut OnStopArgs) + Send + Sync + 'static,
{
self.args.callbacks.on_stop = Some(Box::new(cb));
self
}
#[must_use = "Builder methods return an updated builder; use the returned value or keep chaining."]
pub fn on_flush<F>(mut self, cb: F) -> Self
where
F: FnMut(u32, &mut OnFlushArgs) + Send + Sync + 'static,
{
self.args.callbacks.on_flush = Some(Box::new(cb));
self
}
pub fn build(self) -> DataSourceArgs {
self.args
}
}
type FlushCallback = Box<dyn FnMut() + Send + Sync + 'static>;
static NEXT_FLUSH_ID: AtomicU64 = AtomicU64::new(1);
static FLUSH_CALLBACKS: OnceLock<Mutex<HashMap<u64, FlushCallback>>> = OnceLock::new();
fn flush_callbacks() -> &'static Mutex<HashMap<u64, FlushCallback>> {
FLUSH_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()))
}
fn register_flush_callback(cb: FlushCallback) -> u64 {
let id = NEXT_FLUSH_ID.fetch_add(1, Ordering::Relaxed);
flush_callbacks().lock().unwrap().insert(id, cb);
id
}
fn take_flush_callback(id: u64) -> Option<FlushCallback> {
flush_callbacks().lock().unwrap().remove(&id)
}
unsafe extern "C" fn flush_callback_trampoline(user_arg: *mut c_void) {
let result = std::panic::catch_unwind(|| {
let id = user_arg as usize as u64;
if let Some(mut cb) = take_flush_callback(id) {
cb();
}
});
if let Err(err) = result {
eprintln!("Fatal panic: {:?}", err);
std::process::abort();
}
}
pub struct TraceContextBase {
pub(crate) iterator: PerfettoDsImplTracerIterator,
}
impl TraceContextBase {
pub fn add_packet<F>(&mut self, mut cb: F)
where
F: FnMut(&mut TracePacket),
{
let writer = PbMsgWriter {
writer: StreamWriter {
writer: RefCell::new(unsafe {
PerfettoDsTracerImplPacketBegin(self.iterator.tracer)
}),
},
};
let mut msg = PbMsg::new(&writer).unwrap();
let mut packet = TracePacket { msg: &mut msg };
cb(&mut packet);
packet.msg.finalize();
let mut inner_writer = writer.writer.writer.borrow_mut();
unsafe {
PerfettoDsTracerImplPacketEnd(self.iterator.tracer, &mut *inner_writer as *mut _);
}
}
pub fn flush<F>(&mut self, cb: F)
where
F: FnMut() + Send + Sync + 'static,
{
let id = register_flush_callback(Box::new(cb));
let user_arg = id as usize as *mut c_void;
unsafe {
PerfettoDsTracerImplFlush(
self.iterator.tracer,
Some(flush_callback_trampoline),
user_arg,
)
};
}
pub fn instance_index(&self) -> u32 {
self.iterator.inst_id
}
}
pub trait Clear: Default {
fn clear(&mut self) {
*self = Self::default();
}
}
pub struct IncrementalState {
pub was_cleared: bool,
}
impl Default for IncrementalState {
fn default() -> Self {
Self { was_cleared: true }
}
}
impl Clear for IncrementalState {}
pub struct TraceContext<'a, IncrT: Default + Clear = IncrementalState> {
base: TraceContextBase,
pub(crate) impl_: *mut PerfettoDsImpl,
pub(crate) _marker: PhantomData<&'a IncrT>,
}
impl<IncrT: Default + Clear> TraceContext<'_, IncrT> {
pub fn with_incremental_state<F>(&mut self, mut cb: F)
where
F: FnMut(&mut Self, &mut IncrT),
{
assert!(!self.impl_.is_null());
let ptr = unsafe {
PerfettoDsImplGetIncrementalState(
self.impl_,
self.base.iterator.tracer,
self.base.iterator.inst_id,
)
};
if ptr.is_null() {
panic!("missing incremental state");
}
let state: &mut IncrT = unsafe { &mut *(ptr as *mut IncrT) };
cb(self, state);
}
}
impl<IncrT: Default + Clear> std::ops::Deref for TraceContext<'_, IncrT> {
type Target = TraceContextBase;
fn deref(&self) -> &Self::Target {
&self.base
}
}
impl<IncrT: Default + Clear> std::ops::DerefMut for TraceContext<'_, IncrT> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.base
}
}
pub struct DataSource<'a: 'static, IncrT: Default + Clear = IncrementalState> {
enabled: *mut bool,
impl_: *mut PerfettoDsImpl,
callbacks: Mutex<Option<Box<DsCallbacks>>>,
_marker: PhantomData<&'a IncrT>,
}
unsafe extern "C" fn on_setup_callback_trampoline(
_ds: *mut PerfettoDsImpl,
inst_id: PerfettoDsInstanceIndex,
ds_config: *mut c_void,
ds_config_size: usize,
user_arg: *mut c_void,
args: *mut PerfettoDsOnSetupArgs,
) -> *mut c_void {
let result = std::panic::catch_unwind(|| {
let callbacks: &mut DsCallbacks = unsafe { &mut *(user_arg as *mut _) };
if let Some(f) = &mut callbacks.on_setup {
let config =
unsafe { std::slice::from_raw_parts(ds_config as *const u8, ds_config_size) };
let mut on_setup_args = OnSetupArgs { _args: args };
f(inst_id, config, &mut on_setup_args);
}
});
if let Err(err) = result {
eprintln!("Fatal panic: {:?}", err);
std::process::abort();
}
ptr::null_mut()
}
unsafe extern "C" fn on_start_callback_trampoline(
_ds: *mut PerfettoDsImpl,
inst_id: PerfettoDsInstanceIndex,
user_arg: *mut c_void,
_inst_ctx: *mut c_void,
args: *mut PerfettoDsOnStartArgs,
) {
let result = std::panic::catch_unwind(|| {
let callbacks: &mut DsCallbacks = unsafe { &mut *(user_arg as *mut _) };
if let Some(f) = &mut callbacks.on_start {
let mut on_start_args = OnStartArgs { _args: args };
f(inst_id, &mut on_start_args);
}
});
if let Err(err) = result {
eprintln!("Fatal panic: {:?}", err);
std::process::abort();
}
}
unsafe extern "C" fn on_stop_callback_trampoline(
_ds: *mut PerfettoDsImpl,
inst_id: PerfettoDsInstanceIndex,
user_arg: *mut c_void,
_inst_ctx: *mut c_void,
args: *mut PerfettoDsOnStopArgs,
) {
let result = std::panic::catch_unwind(|| {
let callbacks: &mut DsCallbacks = unsafe { &mut *(user_arg as *mut _) };
if let Some(f) = &mut callbacks.on_stop {
let mut on_stop_args = OnStopArgs { args };
f(inst_id, &mut on_stop_args);
}
});
if let Err(err) = result {
eprintln!("Fatal panic: {:?}", err);
std::process::abort();
}
}
unsafe extern "C" fn on_flush_callback_trampoline(
_ds: *mut PerfettoDsImpl,
inst_id: PerfettoDsInstanceIndex,
user_arg: *mut c_void,
_inst_ctx: *mut c_void,
args: *mut PerfettoDsOnFlushArgs,
) {
let result = std::panic::catch_unwind(|| {
let callbacks: &mut DsCallbacks = unsafe { &mut *(user_arg as *mut _) };
if let Some(f) = &mut callbacks.on_flush {
let mut on_flush_args = OnFlushArgs { args };
f(inst_id, &mut on_flush_args);
}
});
if let Err(err) = result {
eprintln!("Fatal panic: {:?}", err);
std::process::abort();
}
}
unsafe extern "C" fn on_create_incr_trampoline<IncrT: Default + Clear>(
_ds: *mut PerfettoDsImpl,
_inst_id: PerfettoDsInstanceIndex,
_tracer: *mut PerfettoDsTracerImpl,
_user_arg: *mut c_void,
) -> *mut c_void {
let boxed = Box::new(IncrT::default());
Box::into_raw(boxed) as *mut c_void
}
unsafe extern "C" fn on_delete_incr_trampoline<IncrT: Default + Clear>(data: *mut c_void) {
unsafe { drop(Box::from_raw(data as *mut IncrT)) };
}
unsafe extern "C" fn on_clear_incr_trampoline<IncrT: Default + Clear>(
incremental_state: *mut c_void,
_user_arg: *mut c_void,
) -> bool {
let result = std::panic::catch_unwind(|| {
let state: &mut IncrT = unsafe { &mut *(incremental_state as *mut IncrT) };
state.clear();
true
});
match result {
Ok(success) => success,
Err(err) => {
eprintln!("Fatal panic: {:?}", err);
std::process::abort();
}
}
}
impl<'a: 'static, IncrT: Default + Clear> DataSource<'a, IncrT> {
pub fn new_with_incremental_state_type() -> Self {
Self::default()
}
pub fn register(&mut self, name: &str, args: DataSourceArgs) -> Result<(), DataSourceError> {
use DataSourceError::*;
let mut callbacks = self.callbacks.lock().unwrap();
if callbacks.is_some() {
return Err(AlreadyRegisteredError);
}
let mut boxed_callbacks = Box::new(args.callbacks);
let user_arg = crate::__box_as_mut_ptr(&mut boxed_callbacks) as *mut c_void;
let writer = PbMsgWriter::new();
let hb = HeapBuffer::new(&writer.writer);
let mut msg = PbMsg::new(&writer).unwrap();
{
let mut desc = DataSourceDescriptor { msg: &mut msg };
desc.set_name(name);
desc.set_will_notify_on_stop(args.will_notify_on_stop);
desc.set_handles_incremental_state_clear(args.handles_incremental_state_clear);
}
msg.finalize();
let desc_size = writer.writer.get_written_size();
let mut desc_buffer: Vec<u8> = vec![0u8; desc_size];
hb.copy_into(&mut desc_buffer);
let ds_impl = unsafe {
let ds_impl = PerfettoDsImplCreate();
PerfettoDsSetOnSetupCallback(ds_impl, Some(on_setup_callback_trampoline));
PerfettoDsSetOnStartCallback(ds_impl, Some(on_start_callback_trampoline));
PerfettoDsSetOnStopCallback(ds_impl, Some(on_stop_callback_trampoline));
PerfettoDsSetOnFlushCallback(ds_impl, Some(on_flush_callback_trampoline));
PerfettoDsSetOnCreateIncr(ds_impl, Some(on_create_incr_trampoline::<IncrT>));
PerfettoDsSetOnDeleteIncr(ds_impl, Some(on_delete_incr_trampoline::<IncrT>));
PerfettoDsSetOnClearIncr(ds_impl, Some(on_clear_incr_trampoline::<IncrT>));
PerfettoDsSetCbUserArg(ds_impl, user_arg);
PerfettoDsSetBufferExhaustedPolicy(
ds_impl,
args.buffer_exhausted_policy.to_ds_policy(),
);
PerfettoDsSetBufferExhaustedPolicyConfigurable(
ds_impl,
args.buffer_exhausted_policy_configurable,
);
let success = PerfettoDsImplRegister(
ds_impl,
&raw mut self.enabled,
desc_buffer.as_mut_ptr() as *mut c_void,
desc_size,
);
if !success {
return Err(RegisterError);
}
ds_impl
};
self.impl_ = ds_impl;
callbacks.replace(boxed_callbacks);
Ok(())
}
pub fn is_enabled(&self) -> bool {
unsafe {
let atomic_ptr = self.enabled as *const AtomicBool;
(*atomic_ptr).load(Ordering::Relaxed)
}
}
pub fn trace<F>(&self, mut cb: F)
where
F: FnMut(&mut TraceContext<'_, IncrT>),
{
if crate::__unlikely!(self.is_enabled()) {
assert!(!self.impl_.is_null());
let mut ctx = TraceContext::<'_, IncrT> {
base: TraceContextBase {
iterator: unsafe { PerfettoDsImplTraceIterateBegin(self.impl_) },
},
impl_: self.impl_,
_marker: PhantomData,
};
loop {
if ctx.base.iterator.tracer.is_null() {
break;
}
cb(&mut ctx);
unsafe { PerfettoDsImplTraceIterateNext(self.impl_, &raw mut ctx.base.iterator) };
}
}
}
}
impl<'a: 'static> DataSource<'a, IncrementalState> {
pub fn new() -> Self {
Self::default()
}
}
impl<'a: 'static, IncrT: Default + Clear> Default for DataSource<'a, IncrT> {
fn default() -> Self {
Self {
enabled: &raw mut perfetto_atomic_false,
impl_: ptr::null_mut(),
callbacks: Mutex::new(None),
_marker: PhantomData,
}
}
}
unsafe impl<'a: 'static, IncrT: Default + Clear> Send for DataSource<'a, IncrT> {}
unsafe impl<'a: 'static, IncrT: Default + Clear> Sync for DataSource<'a, IncrT> {}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{
PRODUCER_SHMEM_SIZE_HINT_KB, TracingSessionBuilder, acquire_test_environment,
};
use std::{error::Error, sync::OnceLock};
const DATA_SOURCE_NAME: &str = "com.example.custom_data_source";
static DATA_SOURCE: OnceLock<DataSource> = OnceLock::new();
fn get_data_source() -> &'static DataSource<'static> {
DATA_SOURCE.get_or_init(|| {
let data_source_args = DataSourceArgsBuilder::new()
.buffer_exhausted_policy(DataSourceBufferExhaustedPolicy::StallAndAbort);
let mut data_source = DataSource::new();
data_source
.register(DATA_SOURCE_NAME, data_source_args.build())
.expect("failed to register data source");
data_source
})
}
#[test]
fn is_enabled() -> Result<(), Box<dyn Error>> {
let _lock = acquire_test_environment();
let data_source = get_data_source();
assert!(!data_source.is_enabled());
let mut session = TracingSessionBuilder::new()
.set_data_source_name(DATA_SOURCE_NAME)
.build()?;
session.start_blocking();
assert!(data_source.is_enabled());
session.stop_blocking();
Ok(())
}
#[test]
fn trace() -> Result<(), Box<dyn Error>> {
use crate::pb_decoder::{PbDecoder, PbDecoderField};
use crate::protos::trace::{test_event::*, trace::*, trace_packet::*};
use std::sync::{Arc, Mutex};
let _lock = acquire_test_environment();
let data_source = get_data_source();
let mut session = TracingSessionBuilder::new()
.set_data_source_name(DATA_SOURCE_NAME)
.build()?;
session.start_blocking();
data_source.trace(|ctx: &mut TraceContext| {
ctx.add_packet(|packet: &mut TracePacket| {
packet.set_for_testing(|for_testing: &mut TestEvent| {
for_testing.set_str("123");
});
});
});
session.stop_blocking();
let trace_data = Arc::new(Mutex::new(vec![]));
let trace_data_for_write = Arc::clone(&trace_data);
session.read_trace_blocking(move |data, _end| {
let mut written_data = trace_data_for_write.lock().unwrap();
written_data.extend_from_slice(data);
});
let data = trace_data.lock().unwrap();
assert!(!data.is_empty());
let mut test_str = String::new();
for trace_field in PbDecoder::new(&data) {
const PACKET_ID: u32 = TraceFieldNumber::Packet as u32;
if let (PACKET_ID, PbDecoderField::Delimited(data)) = trace_field.unwrap() {
for packet_field in PbDecoder::new(data) {
const FOR_TESTING_ID: u32 = TracePacketFieldNumber::ForTesting as u32;
if let (FOR_TESTING_ID, PbDecoderField::Delimited(data)) = packet_field.unwrap()
{
for test_event_field in PbDecoder::new(data) {
const STR_ID: u32 = TestEventFieldNumber::Str as u32;
if let (STR_ID, PbDecoderField::Delimited(value)) =
test_event_field.unwrap()
{
test_str = String::from_utf8(value.to_vec()).unwrap()
}
}
}
}
}
}
assert_eq!(&test_str, "123");
Ok(())
}
#[test]
fn trace_large_packet() -> Result<(), Box<dyn Error>> {
use crate::pb_decoder::{PbDecoder, PbDecoderField};
use crate::protos::trace::{test_event::*, trace::*, trace_packet::*};
use std::sync::{Arc, Mutex};
let _lock = acquire_test_environment();
let data_source = get_data_source();
let mut session = TracingSessionBuilder::new()
.set_data_source_name(DATA_SOURCE_NAME)
.build()?;
session.start_blocking();
let super_long_test_string = "a".repeat(1024 * (PRODUCER_SHMEM_SIZE_HINT_KB as usize + 10));
data_source.trace(|ctx: &mut TraceContext| {
ctx.add_packet(|packet: &mut TracePacket| {
packet.set_for_testing(|for_testing: &mut TestEvent| {
for_testing.set_str(&super_long_test_string);
});
});
});
session.stop_blocking();
let trace_data = Arc::new(Mutex::new(vec![]));
let trace_data_for_write = Arc::clone(&trace_data);
session.read_trace_blocking(move |data, _end| {
let mut written_data = trace_data_for_write.lock().unwrap();
written_data.extend_from_slice(data);
});
let data = trace_data.lock().unwrap();
assert!(!data.is_empty());
let mut test_str = String::new();
for trace_field in PbDecoder::new(&data) {
const PACKET_ID: u32 = TraceFieldNumber::Packet as u32;
if let (PACKET_ID, PbDecoderField::Delimited(data)) = trace_field.unwrap() {
for packet_field in PbDecoder::new(data) {
const FOR_TESTING_ID: u32 = TracePacketFieldNumber::ForTesting as u32;
if let (FOR_TESTING_ID, PbDecoderField::Delimited(data)) = packet_field.unwrap()
{
for test_event_field in PbDecoder::new(data) {
const STR_ID: u32 = TestEventFieldNumber::Str as u32;
if let (STR_ID, PbDecoderField::Delimited(value)) =
test_event_field.unwrap()
{
test_str = String::from_utf8(value.to_vec()).unwrap();
}
}
}
}
}
}
assert_eq!(&test_str, &super_long_test_string);
Ok(())
}
}