#![cfg(feature = "analytics")]
use g2g_core::memory::SystemSlice;
use g2g_core::property::{PropError, PropKind};
use g2g_core::{
AnalyticsMeta, AsyncElement, BlobMeta, Caps, Dim, Frame, FrameTiming, G2gError, MemoryDomain,
OutputSink, PipelinePacket, PropValue, PushOutcome, Rate, RawVideoFormat,
};
use g2g_python::PyTransform;
#[derive(Default)]
struct CollectSink {
packets: Vec<PipelinePacket>,
}
impl OutputSink for CollectSink {
fn poll_push(
&mut self,
_cx: &mut core::task::Context<'_>,
packet_slot: &mut Option<PipelinePacket>,
) -> core::task::Poll<Result<PushOutcome, G2gError>> {
let packet = packet_slot.take().expect("poll_push without a packet");
self.packets.push(packet);
core::task::Poll::Ready(Ok(PushOutcome::Accepted))
}
}
fn frame_2x1_rgba() -> Frame {
Frame {
domain: MemoryDomain::System(SystemSlice::from_boxed(vec![0u8; 8].into_boxed_slice())),
timing: FrameTiming {
pts_ns: 0,
dts_ns: 0,
duration_ns: 0,
capture_ns: 0,
arrival_ns: 0,
keyframe: false,
},
sequence: 0,
meta: Default::default(),
}
}
#[test]
fn element_properties_reach_the_python_instance() {
std::env::set_var(
"PYTHONPATH",
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures"),
);
let mut el = PyTransform::new("echo_element", "PropEcho");
el.set_property("model-name", PropValue::Str("yolo11m.onnx".into()))
.unwrap();
el.set_property("device", PropValue::Str("cuda:0".into()))
.unwrap();
el.set_property("batch-size", PropValue::Int(4)).unwrap();
let caps = Caps::RawVideo {
format: RawVideoFormat::Rgba8,
width: Dim::Fixed(2),
height: Dim::Fixed(1),
framerate: Rate::Fixed(30),
interlace: g2g_core::Interlace::Any,
};
el.configure_pipeline(&caps).unwrap();
let mut sink = CollectSink::default();
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
rt.block_on(el.process(PipelinePacket::DataFrame(frame_2x1_rgba()), &mut sink))
.unwrap();
let PipelinePacket::DataFrame(frame) = &sink.packets[0] else {
panic!("expected a DataFrame downstream");
};
let analytics = frame
.meta
.get::<AnalyticsMeta>()
.expect("PropEcho should attach a detection labelled by batch-size");
let dets: Vec<_> = analytics.detections().collect();
assert_eq!(dets.len(), 1);
assert_eq!(
dets[0].label, 4,
"batch-size=4 forwarded as an int and used as the label"
);
let blobs = frame
.meta
.get::<BlobMeta>()
.expect("PropEcho should attach blobs");
let by_header = |h: &str| {
blobs
.iter()
.find(|b| b.header == h)
.map(|b| b.payload.clone())
.unwrap_or_default()
};
assert_eq!(
by_header("model_name"),
b"yolo11m.onnx",
"model-name -> self.model_name"
);
assert_eq!(by_header("device"), b"cuda:0", "device -> self.device");
}
fn rgba_2x1() -> Caps {
Caps::RawVideo {
format: RawVideoFormat::Rgba8,
width: Dim::Fixed(2),
height: Dim::Fixed(1),
framerate: Rate::Fixed(30),
interlace: g2g_core::Interlace::Any,
}
}
#[test]
fn a_property_the_hosted_class_does_not_declare_is_refused() {
std::env::set_var(
"PYTHONPATH",
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures"),
);
let mut el = PyTransform::new("echo_element", "DeclaredProps");
el.set_property("model-name", PropValue::Str("yolo11m.onnx".into()))
.unwrap();
el.set_property("speaker", PropValue::Str("Andrew".into()))
.unwrap();
assert!(
el.configure_pipeline(&rgba_2x1()).is_err(),
"a detector has no speaker"
);
}
#[test]
fn a_property_the_hosted_class_declares_is_forwarded() {
std::env::set_var(
"PYTHONPATH",
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures"),
);
let mut el = PyTransform::new("echo_element", "DeclaredProps");
el.set_property("model-name", PropValue::Str("yolo11m.onnx".into()))
.unwrap();
el.set_property("device", PropValue::Str("cuda:0".into()))
.unwrap();
el.configure_pipeline(&rgba_2x1()).unwrap();
}
#[test]
fn every_declared_property_is_settable() {
let mut el = PyTransform::new("echo_element", "PropEcho");
for spec in el.properties() {
if spec.name == g2g_core::UNDECLARED_PROPERTIES {
continue;
}
let value = match spec.kind {
PropKind::Bool => PropValue::Bool(true),
PropKind::Int => PropValue::Int(1),
PropKind::Uint => PropValue::Uint(1),
PropKind::Double => PropValue::Double(1.0),
PropKind::Fraction => PropValue::Fraction(30, 1),
PropKind::Str if spec.name == "format" => PropValue::Str("RGBA".into()),
PropKind::Str => PropValue::Str("x".into()),
_ => continue,
};
assert_ne!(
el.set_property(spec.name, value),
Err(PropError::Unknown),
"declared property '{}' has no set_property arm",
spec.name
);
}
}
#[test]
fn a_per_element_property_reaches_the_python_instance() {
std::env::set_var(
"PYTHONPATH",
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures"),
);
let mut el = PyTransform::new("echo_element", "PropEcho");
el.set_property("language", PropValue::Str("ko".into()))
.unwrap();
let caps = Caps::RawVideo {
format: RawVideoFormat::Rgba8,
width: Dim::Fixed(2),
height: Dim::Fixed(1),
framerate: Rate::Fixed(30),
interlace: g2g_core::Interlace::Any,
};
el.configure_pipeline(&caps).unwrap();
let mut sink = CollectSink::default();
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
rt.block_on(el.process(PipelinePacket::DataFrame(frame_2x1_rgba()), &mut sink))
.unwrap();
let PipelinePacket::DataFrame(frame) = &sink.packets[0] else {
panic!("expected a DataFrame downstream");
};
let blobs = frame
.meta
.get::<BlobMeta>()
.expect("PropEcho should attach blobs");
let language = blobs
.iter()
.find(|b| b.header == "language")
.map(|b| b.payload.clone())
.unwrap_or_default();
assert_eq!(language, b"ko", "language -> self.language");
}