1mod api;
57mod client;
58mod dataset;
59mod error;
60
61pub use crate::{
62 api::{
63 AnnotationSetID, AppId, Artifact, DatasetID, DatasetParams, Experiment, ExperimentID,
64 ImageId, Organization, OrganizationID, Parameter, PresignedUrl, Project, ProjectID,
65 SampleID, SamplesPopulateParams, SamplesPopulateResult, SequenceId, SnapshotID, Stage,
66 Task, TaskID, TaskInfo, TrainingSession, TrainingSessionID, ValidationSession,
67 ValidationSessionID,
68 },
69 client::{Client, Progress},
70 dataset::{
71 Annotation, AnnotationSet, AnnotationType, Box2d, Box3d, Dataset, FileType, GpsData,
72 ImuData, Label, Location, Mask, Sample, SampleFile,
73 },
74 error::Error,
75};
76
77#[cfg(feature = "polars")]
78pub use crate::dataset::annotations_dataframe;
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use polars::frame::UniqueKeepStrategy;
84 use std::{
85 collections::HashMap,
86 env,
87 fs::{File, read_to_string},
88 io::Write,
89 path::PathBuf,
90 };
91 use tokio::time::{Duration, sleep};
92
93 fn get_test_data_dir() -> PathBuf {
96 let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
97 .parent()
98 .unwrap()
99 .parent()
100 .unwrap()
101 .join("target")
102 .join("testdata");
103
104 std::fs::create_dir_all(&test_dir).expect("Failed to create test data directory");
105 test_dir
106 }
107
108 #[ctor::ctor]
109 fn init() {
110 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
111 }
112
113 #[tokio::test]
114 async fn test_version() -> Result<(), Error> {
115 let client = match env::var("STUDIO_SERVER") {
116 Ok(server) => Client::new()?.with_server(&server)?,
117 Err(_) => Client::new()?,
118 };
119 let result = client.version().await?;
120 println!("EdgeFirst Studio Version: {}", result);
121 Ok(())
122 }
123
124 async fn get_client() -> Result<Client, Error> {
125 let client = Client::new()?.with_token_path(None)?;
126
127 let client = match env::var("STUDIO_TOKEN") {
128 Ok(token) => client.with_token(&token)?,
129 Err(_) => client,
130 };
131
132 let client = match env::var("STUDIO_SERVER") {
133 Ok(server) => client.with_server(&server)?,
134 Err(_) => client,
135 };
136
137 let client = match (env::var("STUDIO_USERNAME"), env::var("STUDIO_PASSWORD")) {
138 (Ok(username), Ok(password)) => client.with_login(&username, &password).await?,
139 _ => client,
140 };
141
142 client.verify_token().await?;
143
144 Ok(client)
145 }
146
147 #[tokio::test]
148 async fn test_token() -> Result<(), Error> {
149 let client = get_client().await?;
150 let token = client.token().await;
151 assert!(!token.is_empty());
152 println!("Token: {}", token);
153
154 let exp = client.token_expiration().await?;
155 println!("Token Expiration: {}", exp);
156
157 let username = client.username().await?;
158 assert!(!username.is_empty());
159 println!("Username: {}", username);
160
161 sleep(Duration::from_secs(2)).await;
163
164 client.renew_token().await?;
165 let new_token = client.token().await;
166 assert!(!new_token.is_empty());
167 assert_ne!(token, new_token);
168 println!("New Token Expiration: {}", client.token_expiration().await?);
169
170 Ok(())
171 }
172
173 #[tokio::test]
174 async fn test_organization() -> Result<(), Error> {
175 let client = get_client().await?;
176 let org = client.organization().await?;
177 println!(
178 "Organization: {}\nID: {}\nCredits: {}",
179 org.name(),
180 org.id(),
181 org.credits()
182 );
183 Ok(())
184 }
185
186 #[tokio::test]
187 async fn test_projects() -> Result<(), Error> {
188 let client = get_client().await?;
189 let project = client.projects(Some("Unit Testing")).await?;
190 assert!(!project.is_empty());
191 Ok(())
192 }
193
194 #[tokio::test]
195 async fn test_datasets() -> Result<(), Error> {
196 let client = get_client().await?;
197 let project = client.projects(Some("Unit Testing")).await?;
198 assert!(!project.is_empty());
199 let project = project.first().unwrap();
200 let datasets = client.datasets(project.id(), None).await?;
201
202 for dataset in datasets {
203 let dataset_id = dataset.id();
204 let result = client.dataset(dataset_id).await?;
205 assert_eq!(result.id(), dataset_id);
206 }
207
208 Ok(())
209 }
210
211 #[tokio::test]
212 async fn test_labels() -> Result<(), Error> {
213 let client = get_client().await?;
214 let project = client.projects(Some("Unit Testing")).await?;
215 assert!(!project.is_empty());
216 let project = project.first().unwrap();
217 let datasets = client.datasets(project.id(), Some("Test Labels")).await?;
218 let dataset = datasets.first().unwrap_or_else(|| {
219 panic!(
220 "Dataset 'Test Labels' not found in project '{}'",
221 project.name()
222 )
223 });
224
225 let labels = dataset.labels(&client).await?;
226 for label in labels {
227 label.remove(&client).await?;
228 }
229
230 let labels = dataset.labels(&client).await?;
231 assert_eq!(labels.len(), 0);
232
233 dataset.add_label(&client, "test").await?;
234 let labels = dataset.labels(&client).await?;
235 assert_eq!(labels.len(), 1);
236 assert_eq!(labels[0].name(), "test");
237
238 dataset.remove_label(&client, "test").await?;
239 let labels = dataset.labels(&client).await?;
240 assert_eq!(labels.len(), 0);
241
242 Ok(())
243 }
244
245 #[tokio::test]
246 async fn test_coco() -> Result<(), Error> {
247 let coco_labels = HashMap::from([
248 (0, "person"),
249 (1, "bicycle"),
250 (2, "car"),
251 (3, "motorcycle"),
252 (4, "airplane"),
253 (5, "bus"),
254 (6, "train"),
255 (7, "truck"),
256 (8, "boat"),
257 (9, "traffic light"),
258 (10, "fire hydrant"),
259 (11, "stop sign"),
260 (12, "parking meter"),
261 (13, "bench"),
262 (14, "bird"),
263 (15, "cat"),
264 (16, "dog"),
265 (17, "horse"),
266 (18, "sheep"),
267 (19, "cow"),
268 (20, "elephant"),
269 (21, "bear"),
270 (22, "zebra"),
271 (23, "giraffe"),
272 (24, "backpack"),
273 (25, "umbrella"),
274 (26, "handbag"),
275 (27, "tie"),
276 (28, "suitcase"),
277 (29, "frisbee"),
278 (30, "skis"),
279 (31, "snowboard"),
280 (32, "sports ball"),
281 (33, "kite"),
282 (34, "baseball bat"),
283 (35, "baseball glove"),
284 (36, "skateboard"),
285 (37, "surfboard"),
286 (38, "tennis racket"),
287 (39, "bottle"),
288 (40, "wine glass"),
289 (41, "cup"),
290 (42, "fork"),
291 (43, "knife"),
292 (44, "spoon"),
293 (45, "bowl"),
294 (46, "banana"),
295 (47, "apple"),
296 (48, "sandwich"),
297 (49, "orange"),
298 (50, "broccoli"),
299 (51, "carrot"),
300 (52, "hot dog"),
301 (53, "pizza"),
302 (54, "donut"),
303 (55, "cake"),
304 (56, "chair"),
305 (57, "couch"),
306 (58, "potted plant"),
307 (59, "bed"),
308 (60, "dining table"),
309 (61, "toilet"),
310 (62, "tv"),
311 (63, "laptop"),
312 (64, "mouse"),
313 (65, "remote"),
314 (66, "keyboard"),
315 (67, "cell phone"),
316 (68, "microwave"),
317 (69, "oven"),
318 (70, "toaster"),
319 (71, "sink"),
320 (72, "refrigerator"),
321 (73, "book"),
322 (74, "clock"),
323 (75, "vase"),
324 (76, "scissors"),
325 (77, "teddy bear"),
326 (78, "hair drier"),
327 (79, "toothbrush"),
328 ]);
329
330 let client = get_client().await?;
331 let project = client.projects(Some("Sample Project")).await?;
332 assert!(!project.is_empty());
333 let project = project.first().unwrap();
334 let datasets = client.datasets(project.id(), Some("COCO")).await?;
335 assert!(!datasets.is_empty());
336 let dataset = datasets.iter().find(|d| d.name() == "COCO").unwrap();
338
339 let labels = dataset.labels(&client).await?;
340 assert_eq!(labels.len(), 80);
341
342 for label in &labels {
343 assert_eq!(label.name(), coco_labels[&label.index()]);
344 }
345
346 let n_samples = client
347 .samples_count(dataset.id(), None, &[], &["val".to_string()], &[])
348 .await?;
349 assert_eq!(n_samples.total, 5000);
350
351 let samples = client
352 .samples(dataset.id(), None, &[], &["val".to_string()], &[], None)
353 .await?;
354 assert_eq!(samples.len(), 5000);
355
356 Ok(())
357 }
358
359 #[cfg(feature = "polars")]
360 #[tokio::test]
361 async fn test_coco_dataframe() -> Result<(), Error> {
362 let client = get_client().await?;
363 let project = client.projects(Some("Sample Project")).await?;
364 assert!(!project.is_empty());
365 let project = project.first().unwrap();
366 let datasets = client.datasets(project.id(), Some("COCO")).await?;
367 assert!(!datasets.is_empty());
368 let dataset = datasets.iter().find(|d| d.name() == "COCO").unwrap();
370
371 let annotation_set_id = dataset
372 .annotation_sets(&client)
373 .await?
374 .first()
375 .unwrap()
376 .id();
377
378 let annotations = client
379 .annotations(annotation_set_id, &["val".to_string()], &[], None)
380 .await?;
381 let df = annotations_dataframe(&annotations);
382 let df = df
383 .unique_stable(Some(&["name".to_string()]), UniqueKeepStrategy::First, None)
384 .unwrap();
385 assert_eq!(df.height(), 5000);
386
387 Ok(())
388 }
389
390 #[tokio::test]
391 async fn test_snapshots() -> Result<(), Error> {
392 let client = get_client().await?;
393 let snapshots = client.snapshots(None).await?;
394
395 for snapshot in snapshots {
396 let snapshot_id = snapshot.id();
397 let result = client.snapshot(snapshot_id).await?;
398 assert_eq!(result.id(), snapshot_id);
399 }
400
401 Ok(())
402 }
403
404 #[tokio::test]
405 async fn test_experiments() -> Result<(), Error> {
406 let client = get_client().await?;
407 let project = client.projects(Some("Unit Testing")).await?;
408 assert!(!project.is_empty());
409 let project = project.first().unwrap();
410 let experiments = client.experiments(project.id(), None).await?;
411
412 for experiment in experiments {
413 let experiment_id = experiment.id();
414 let result = client.experiment(experiment_id).await?;
415 assert_eq!(result.id(), experiment_id);
416 }
417
418 Ok(())
419 }
420
421 #[tokio::test]
422 async fn test_training_session() -> Result<(), Error> {
423 let client = get_client().await?;
424 let project = client.projects(Some("Unit Testing")).await?;
425 assert!(!project.is_empty());
426 let project = project.first().unwrap();
427 let experiment = client
428 .experiments(project.id(), Some("Unit Testing"))
429 .await?;
430 let experiment = experiment.first().unwrap();
431
432 let sessions = client
433 .training_sessions(experiment.id(), Some("modelpack-usermanaged"))
434 .await?;
435 assert_ne!(sessions.len(), 0);
436 let session = sessions.first().unwrap();
437
438 let metrics = HashMap::from([
439 ("epochs".to_string(), Parameter::Integer(10)),
440 ("loss".to_string(), Parameter::Real(0.05)),
441 (
442 "model".to_string(),
443 Parameter::String("modelpack".to_string()),
444 ),
445 ]);
446
447 session.set_metrics(&client, metrics).await?;
448 let updated_metrics = session.metrics(&client).await?;
449 assert_eq!(updated_metrics.len(), 3);
450 assert_eq!(updated_metrics.get("epochs"), Some(&Parameter::Integer(10)));
451 assert_eq!(updated_metrics.get("loss"), Some(&Parameter::Real(0.05)));
452 assert_eq!(
453 updated_metrics.get("model"),
454 Some(&Parameter::String("modelpack".to_string()))
455 );
456
457 println!("Updated Metrics: {:?}", updated_metrics);
458
459 let mut labels = tempfile::NamedTempFile::new()?;
460 write!(labels, "background")?;
461 labels.flush()?;
462
463 session
464 .upload(
465 &client,
466 &[(
467 "artifacts/labels.txt".to_string(),
468 labels.path().to_path_buf(),
469 )],
470 )
471 .await?;
472
473 let labels = session.download(&client, "artifacts/labels.txt").await?;
474 assert_eq!(labels, "background");
475
476 Ok(())
477 }
478
479 #[tokio::test]
480 async fn test_validate() -> Result<(), Error> {
481 let client = get_client().await?;
482 let project = client.projects(Some("Unit Testing")).await?;
483 assert!(!project.is_empty());
484 let project = project.first().unwrap();
485
486 let sessions = client.validation_sessions(project.id()).await?;
487 for session in &sessions {
488 let s = client.validation_session(session.id()).await?;
489 assert_eq!(s.id(), session.id());
490 assert_eq!(s.description(), session.description());
491 }
492
493 let session = sessions
494 .into_iter()
495 .find(|s| s.name() == "modelpack-usermanaged")
496 .unwrap_or_else(|| {
497 panic!(
498 "Validation session 'modelpack-usermanaged' not found in project '{}'",
499 project.name()
500 )
501 });
502
503 let metrics = HashMap::from([("accuracy".to_string(), Parameter::Real(0.95))]);
504 session.set_metrics(&client, metrics).await?;
505
506 let metrics = session.metrics(&client).await?;
507 assert_eq!(metrics.get("accuracy"), Some(&Parameter::Real(0.95)));
508
509 Ok(())
510 }
511
512 #[tokio::test]
513 async fn test_artifacts() -> Result<(), Error> {
514 let client = get_client().await?;
515 let project = client.projects(Some("Unit Testing")).await?;
516 assert!(!project.is_empty());
517 let project = project.first().unwrap();
518 let experiment = client
519 .experiments(project.id(), Some("Unit Testing"))
520 .await?;
521 let experiment = experiment.first().unwrap();
522 let trainer = client
523 .training_sessions(experiment.id(), Some("modelpack-960x540"))
524 .await?;
525 let trainer = trainer.first().unwrap();
526 let artifacts = client.artifacts(trainer.id()).await?;
527 assert!(!artifacts.is_empty());
528
529 let test_dir = get_test_data_dir();
530
531 for artifact in artifacts {
532 let output_path = test_dir.join(artifact.name());
533 client
534 .download_artifact(
535 trainer.id(),
536 artifact.name(),
537 Some(output_path.clone()),
538 None,
539 )
540 .await?;
541
542 if output_path.exists() {
544 std::fs::remove_file(&output_path)?;
545 }
546 }
547
548 let fake_path = test_dir.join("fakefile.txt");
549 let res = client
550 .download_artifact(trainer.id(), "fakefile.txt", Some(fake_path.clone()), None)
551 .await;
552 assert!(res.is_err());
553 assert!(!fake_path.exists());
554
555 Ok(())
556 }
557
558 #[tokio::test]
559 async fn test_checkpoints() -> Result<(), Error> {
560 let client = get_client().await?;
561 let project = client.projects(Some("Unit Testing")).await?;
562 assert!(!project.is_empty());
563 let project = project.first().unwrap();
564 let experiment = client
565 .experiments(project.id(), Some("Unit Testing"))
566 .await?;
567 let experiment = experiment.first().unwrap_or_else(|| {
568 panic!(
569 "Experiment 'Unit Testing' not found in project '{}'",
570 project.name()
571 )
572 });
573 let trainer = client
574 .training_sessions(experiment.id(), Some("modelpack-usermanaged"))
575 .await?;
576 let trainer = trainer.first().unwrap();
577
578 let test_dir = get_test_data_dir();
579 let checkpoint_path = test_dir.join("checkpoint.txt");
580 let checkpoint2_path = test_dir.join("checkpoint2.txt");
581
582 {
583 let mut chkpt = File::create(&checkpoint_path)?;
584 chkpt.write_all(b"Test Checkpoint")?;
585 }
586
587 trainer
588 .upload(
589 &client,
590 &[(
591 "checkpoints/checkpoint.txt".to_string(),
592 checkpoint_path.clone(),
593 )],
594 )
595 .await?;
596
597 client
598 .download_checkpoint(
599 trainer.id(),
600 "checkpoint.txt",
601 Some(checkpoint2_path.clone()),
602 None,
603 )
604 .await?;
605
606 let chkpt = read_to_string(&checkpoint2_path)?;
607 assert_eq!(chkpt, "Test Checkpoint");
608
609 let fake_path = test_dir.join("fakefile.txt");
610 let res = client
611 .download_checkpoint(trainer.id(), "fakefile.txt", Some(fake_path.clone()), None)
612 .await;
613 assert!(res.is_err());
614 assert!(!fake_path.exists());
615
616 if checkpoint_path.exists() {
618 std::fs::remove_file(&checkpoint_path)?;
619 }
620 if checkpoint2_path.exists() {
621 std::fs::remove_file(&checkpoint2_path)?;
622 }
623
624 Ok(())
625 }
626
627 #[tokio::test]
628 async fn test_tasks() -> Result<(), Error> {
629 let client = get_client().await?;
630 let project = client.projects(Some("Unit Testing")).await?;
631 let project = project.first().unwrap();
632 let tasks = client.tasks(None, None, None, None).await?;
633
634 for task in tasks {
635 let task_info = client.task_info(task.id()).await?;
636 println!("{} - {}", task, task_info);
637 }
638
639 let tasks = client
640 .tasks(Some("modelpack-usermanaged"), None, None, None)
641 .await?;
642 let tasks = tasks
643 .into_iter()
644 .map(|t| client.task_info(t.id()))
645 .collect::<Vec<_>>();
646 let tasks = futures::future::try_join_all(tasks).await?;
647 let tasks = tasks
648 .into_iter()
649 .filter(|t| t.project_id() == Some(project.id()))
650 .collect::<Vec<_>>();
651 assert_ne!(tasks.len(), 0);
652 let task = &tasks[0];
653
654 let t = client.task_status(task.id(), "training").await?;
655 assert_eq!(t.id(), task.id());
656 assert_eq!(t.status(), "training");
657
658 let stages = [
659 ("download", "Downloading Dataset"),
660 ("train", "Training Model"),
661 ("export", "Exporting Model"),
662 ];
663 client.set_stages(task.id(), &stages).await?;
664
665 client
666 .update_stage(task.id(), "download", "running", "Downloading dataset", 50)
667 .await?;
668
669 let task = client.task_info(task.id()).await?;
670 println!("task progress: {:?}", task.stages());
671
672 Ok(())
673 }
674
675 fn generate_test_image_with_circle_format(format: &str) -> (Vec<u8>, (f32, f32, f32, f32)) {
681 use image::{ImageBuffer, Rgb, RgbImage};
682 use std::io::Cursor;
683
684 let width = 640u32;
685 let height = 480u32;
686
687 let mut img: RgbImage = ImageBuffer::from_pixel(width, height, Rgb([255u8, 255u8, 255u8]));
689
690 let center_x = 150.0;
692 let center_y = 120.0;
693 let radius = 50.0;
694
695 for y in 0..height {
696 for x in 0..width {
697 let dx = x as f32 - center_x;
698 let dy = y as f32 - center_y;
699 let distance = (dx * dx + dy * dy).sqrt();
700
701 if distance <= radius {
702 img.put_pixel(x, y, Rgb([255u8, 0u8, 0u8])); }
704 }
705 }
706
707 let mut image_data = Vec::new();
709 let mut cursor = Cursor::new(&mut image_data);
710
711 match format {
712 "jpeg" | "jpg" => {
713 img.write_to(&mut cursor, image::ImageFormat::Jpeg).unwrap();
714 }
715 "png" => {
716 img.write_to(&mut cursor, image::ImageFormat::Png).unwrap();
717 }
718 _ => panic!("Unsupported format: {}", format),
719 }
720
721 let bbox_x = center_x - radius - 5.0;
723 let bbox_y = center_y - radius - 5.0;
724 let bbox_w = (radius * 2.0) + 10.0;
725 let bbox_h = (radius * 2.0) + 10.0;
726
727 (image_data, (bbox_x, bbox_y, bbox_w, bbox_h))
728 }
729
730 #[tokio::test]
731 async fn test_populate_samples() -> Result<(), Error> {
732 let client = get_client().await?;
733
734 let projects = client.projects(Some("Unit Testing")).await?;
736 let project = projects.first().unwrap();
737
738 let datasets = client.datasets(project.id(), Some("Test Labels")).await?;
739 let dataset = datasets.first().unwrap();
740
741 let annotation_sets = client.annotation_sets(dataset.id()).await?;
743 let annotation_set = annotation_sets.first().unwrap();
744
745 let test_format = "png";
748 let file_extension = "png";
749
750 let (image_data, circle_bbox) = generate_test_image_with_circle_format(test_format);
752 eprintln!(
753 "Generated {} image with circle at bbox: ({:.1}, {:.1}, {:.1}, {:.1})",
754 test_format, circle_bbox.0, circle_bbox.1, circle_bbox.2, circle_bbox.3
755 );
756
757 let timestamp = std::time::SystemTime::now()
759 .duration_since(std::time::UNIX_EPOCH)
760 .unwrap()
761 .as_secs();
762 let temp_dir = std::env::temp_dir();
763 let test_image_path =
764 temp_dir.join(format!("test_populate_{}.{}", timestamp, file_extension));
765 std::fs::write(&test_image_path, &image_data)?;
766
767 let testdata_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
769 .parent()
770 .unwrap()
771 .parent()
772 .unwrap()
773 .join("target")
774 .join("testdata");
775 std::fs::create_dir_all(&testdata_dir).ok();
776 let local_copy = testdata_dir.join(format!(
777 "test_populate_circle_{}.{}",
778 timestamp, file_extension
779 ));
780 std::fs::write(&local_copy, &image_data)?;
781 eprintln!("Test image saved to: {:?}", local_copy);
782
783 let mut sample = Sample::new();
785 let img_width = 640.0;
786 let img_height = 480.0;
787 sample.group = Some("train".to_string());
789 sample.files = vec![SampleFile::with_filename(
793 "image".to_string(),
794 test_image_path.to_str().unwrap().to_string(),
795 )];
796
797 let mut annotation = Annotation::new();
799 annotation.set_label(Some("circle".to_string()));
800 annotation.set_object_id(Some("circle-obj-1".to_string()));
801
802 let normalized_x = circle_bbox.0 / img_width;
804 let normalized_y = circle_bbox.1 / img_height;
805 let normalized_w = circle_bbox.2 / img_width;
806 let normalized_h = circle_bbox.3 / img_height;
807
808 eprintln!(
809 "Normalized bbox: ({:.3}, {:.3}, {:.3}, {:.3})",
810 normalized_x, normalized_y, normalized_w, normalized_h
811 );
812
813 let bbox = Box2d::new(normalized_x, normalized_y, normalized_w, normalized_h);
814 annotation.set_box2d(Some(bbox));
815 sample.annotations = vec![annotation];
816
817 let results = client
819 .populate_samples(dataset.id(), Some(annotation_set.id()), vec![sample], None)
820 .await?;
821
822 assert_eq!(results.len(), 1);
823 let result = &results[0];
824 assert_eq!(result.urls.len(), 1);
825
826 let image_filename = format!("test_populate_{}.{}", timestamp, file_extension);
828
829 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
831
832 let samples = client
834 .samples(
835 dataset.id(),
836 Some(annotation_set.id()),
837 &[],
838 &[], &[],
840 None,
841 )
842 .await?;
843
844 eprintln!("Looking for image: {}", image_filename);
845 eprintln!("Found {} samples total", samples.len());
846
847 let created_sample = samples
849 .iter()
850 .find(|s| s.image_name.as_deref() == Some(&image_filename));
851
852 assert!(
853 created_sample.is_some(),
854 "Sample with image_name '{}' should exist in dataset",
855 image_filename
856 );
857 let created_sample = created_sample.unwrap();
858
859 eprintln!("✓ Found sample by image_name: {}", image_filename);
860
861 assert_eq!(
863 created_sample.image_name.as_deref(),
864 Some(&image_filename[..])
865 );
866 assert_eq!(created_sample.group, Some("train".to_string()));
867
868 eprintln!("\nSample verification:");
869 eprintln!(" ✓ image_name: {:?}", created_sample.image_name);
870 eprintln!(" ✓ group: {:?}", created_sample.group);
871 eprintln!(
872 " ✓ annotations: {} item(s)",
873 created_sample.annotations.len()
874 );
875
876 eprintln!(
880 " ⚠ uuid: {:?} (not returned by server)",
881 created_sample.uuid
882 );
883 eprintln!(
884 " ⚠ width: {:?} (not returned by server)",
885 created_sample.width
886 );
887 eprintln!(
888 " ⚠ height: {:?} (not returned by server)",
889 created_sample.height
890 );
891
892 let annotations = &created_sample.annotations;
894 assert_eq!(annotations.len(), 1, "Should have exactly one annotation");
895
896 let annotation = &annotations[0];
897 assert_eq!(annotation.label(), Some(&"circle".to_string()));
898 assert!(
899 annotation.box2d().is_some(),
900 "Bounding box should be present"
901 );
902
903 let returned_bbox = annotation.box2d().unwrap();
904 eprintln!("\nAnnotation verification:");
905 eprintln!(" ✓ label: {:?}", annotation.label());
906 eprintln!(
907 " ✓ bbox: x={:.3}, y={:.3}, w={:.3}, h={:.3}",
908 returned_bbox.left(),
909 returned_bbox.top(),
910 returned_bbox.width(),
911 returned_bbox.height()
912 );
913
914 assert!(
916 (returned_bbox.left() - normalized_x).abs() < 0.01,
917 "bbox.x should match (sent: {:.3}, got: {:.3})",
918 normalized_x,
919 returned_bbox.left()
920 );
921 assert!(
922 (returned_bbox.top() - normalized_y).abs() < 0.01,
923 "bbox.y should match (sent: {:.3}, got: {:.3})",
924 normalized_y,
925 returned_bbox.top()
926 );
927 assert!(
928 (returned_bbox.width() - normalized_w).abs() < 0.01,
929 "bbox.w should match (sent: {:.3}, got: {:.3})",
930 normalized_w,
931 returned_bbox.width()
932 );
933 assert!(
934 (returned_bbox.height() - normalized_h).abs() < 0.01,
935 "bbox.h should match (sent: {:.3}, got: {:.3})",
936 normalized_h,
937 returned_bbox.height()
938 );
939
940 eprintln!("\nImage verification:");
942 let downloaded_image = created_sample.download(&client, FileType::Image).await?;
943 assert!(
944 downloaded_image.is_some(),
945 "Should be able to download the image"
946 );
947 let downloaded_data = downloaded_image.unwrap();
948
949 assert_eq!(
950 image_data.len(),
951 downloaded_data.len(),
952 "Downloaded image should have same size as uploaded"
953 );
954 assert_eq!(
955 image_data, downloaded_data,
956 "Downloaded image should match uploaded image byte-for-byte"
957 );
958 eprintln!(" ✓ Image data matches ({} bytes)", image_data.len());
959
960 let _ = std::fs::remove_file(&test_image_path);
962
963 Ok(())
964 }
965}