1mod api;
57mod client;
58pub mod coco;
59mod dataset;
60mod error;
61pub mod format;
62#[cfg(feature = "profiling")]
63pub mod instrument;
64mod mask;
65mod retry;
66mod storage;
67
68pub use crate::{
69 api::{
70 AnnotationSetID, AppId, Artifact, DatasetID, DatasetParams, Experiment, ExperimentID,
71 ImageId, Job, NewValidationSession, Organization, OrganizationID, Parameter, PresignedUrl,
72 Project, ProjectID, SampleDimensionUpdate, SampleID, SamplesCountResult,
73 SamplesPopulateParams, SamplesPopulateResult, SamplesUpdateDimensionsResult, SequenceId,
74 Snapshot, SnapshotFromDatasetResult, SnapshotID, SnapshotRestoreResult, Stage,
75 StartValidationRequest, Task, TaskDataList, TaskID, TaskInfo, TrainingSession,
76 TrainingSessionID, ValidationSession, ValidationSessionID,
77 },
78 client::{Client, Progress},
79 dataset::{
80 Annotation, AnnotationSet, AnnotationType, Box2d, Box3d, Dataset, FileType, GpsData, Group,
81 ImuData, Label, Location, Polygon, Sample, SampleFile, Timing,
82 },
83 error::Error,
84 mask::MaskData,
85 retry::{RetryScope, classify_url},
86 storage::{FileTokenStorage, MemoryTokenStorage, StorageError, TokenStorage},
87};
88
89#[cfg(feature = "profiling")]
90pub use crate::client::upload_stats;
91
92#[cfg(feature = "polars")]
93pub use crate::dataset::samples_dataframe;
94
95#[cfg(feature = "polars")]
96pub use crate::dataset::unflatten_polygon_coordinates;
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::{
102 collections::HashMap,
103 env,
104 fs::{File, read_to_string},
105 io::Write,
106 path::PathBuf,
107 };
108
109 fn get_test_data_dir() -> PathBuf {
112 let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
113 .parent()
114 .expect("CARGO_MANIFEST_DIR should have parent")
115 .parent()
116 .expect("workspace root should exist")
117 .join("target")
118 .join("testdata");
119
120 std::fs::create_dir_all(&test_dir).expect("Failed to create test data directory");
121 test_dir
122 }
123
124 #[ctor::ctor]
125 fn init() {
126 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
127 }
128
129 async fn get_client() -> Result<Client, Error> {
130 let client = Client::new()?.with_token_path(None)?;
131
132 let client = match env::var("STUDIO_TOKEN") {
133 Ok(token) => client.with_token(&token)?,
134 Err(_) => client,
135 };
136
137 let client = match env::var("STUDIO_SERVER") {
138 Ok(server) => client.with_server(&server)?,
139 Err(_) => client,
140 };
141
142 let client = match (env::var("STUDIO_USERNAME"), env::var("STUDIO_PASSWORD")) {
143 (Ok(username), Ok(password)) => client.with_login(&username, &password).await?,
144 _ => client,
145 };
146
147 client.verify_token().await?;
148
149 Ok(client)
150 }
151
152 async fn get_training_session_for_artifacts() -> Result<TrainingSession, Error> {
154 let client = get_client().await?;
155 let project = client
156 .projects(Some("Unit Testing"))
157 .await?
158 .into_iter()
159 .next()
160 .ok_or_else(|| Error::InvalidParameters("Unit Testing project not found".into()))?;
161 let experiment = client
162 .experiments(project.id(), Some("Unit Testing"))
163 .await?
164 .into_iter()
165 .next()
166 .ok_or_else(|| Error::InvalidParameters("Unit Testing experiment not found".into()))?;
167 let session = client
168 .training_sessions(experiment.id(), Some("modelpack-960x540"))
169 .await?
170 .into_iter()
171 .next()
172 .ok_or_else(|| {
173 Error::InvalidParameters("modelpack-960x540 session not found".into())
174 })?;
175 Ok(session)
176 }
177
178 async fn get_training_session_for_checkpoints() -> Result<TrainingSession, Error> {
180 let client = get_client().await?;
181 let project = client
182 .projects(Some("Unit Testing"))
183 .await?
184 .into_iter()
185 .next()
186 .ok_or_else(|| Error::InvalidParameters("Unit Testing project not found".into()))?;
187 let experiment = client
188 .experiments(project.id(), Some("Unit Testing"))
189 .await?
190 .into_iter()
191 .next()
192 .ok_or_else(|| Error::InvalidParameters("Unit Testing experiment not found".into()))?;
193 let session = client
194 .training_sessions(experiment.id(), Some("modelpack-usermanaged"))
195 .await?
196 .into_iter()
197 .next()
198 .ok_or_else(|| {
199 Error::InvalidParameters("modelpack-usermanaged session not found".into())
200 })?;
201 Ok(session)
202 }
203
204 #[tokio::test]
205 async fn test_training_session() -> Result<(), Error> {
206 let client = get_client().await?;
207 let project = client.projects(Some("Unit Testing")).await?;
208 assert!(!project.is_empty());
209 let project = project
210 .first()
211 .expect("'Unit Testing' project should exist");
212 let experiment = client
213 .experiments(project.id(), Some("Unit Testing"))
214 .await?;
215 let experiment = experiment
216 .first()
217 .expect("'Unit Testing' experiment should exist");
218
219 let sessions = client
220 .training_sessions(experiment.id(), Some("modelpack-usermanaged"))
221 .await?;
222 assert_ne!(sessions.len(), 0);
223 let session = sessions
224 .first()
225 .expect("Training sessions should exist for experiment");
226
227 let metrics = HashMap::from([
228 ("epochs".to_string(), Parameter::Integer(10)),
229 ("loss".to_string(), Parameter::Real(0.05)),
230 (
231 "model".to_string(),
232 Parameter::String("modelpack".to_string()),
233 ),
234 ]);
235
236 session.set_metrics(&client, metrics).await?;
237 let updated_metrics = session.metrics(&client).await?;
238 assert_eq!(updated_metrics.len(), 3);
239 assert_eq!(updated_metrics.get("epochs"), Some(&Parameter::Integer(10)));
240 assert_eq!(updated_metrics.get("loss"), Some(&Parameter::Real(0.05)));
241 assert_eq!(
242 updated_metrics.get("model"),
243 Some(&Parameter::String("modelpack".to_string()))
244 );
245
246 println!("Updated Metrics: {:?}", updated_metrics);
247
248 let mut labels = tempfile::NamedTempFile::new()?;
249 write!(labels, "background")?;
250 labels.flush()?;
251
252 session
253 .upload(
254 &client,
255 &[(
256 "artifacts/labels.txt".to_string(),
257 labels.path().to_path_buf(),
258 )],
259 )
260 .await?;
261
262 let labels = session.download(&client, "artifacts/labels.txt").await?;
263 assert_eq!(labels, "background");
264
265 Ok(())
266 }
267
268 #[tokio::test]
269 async fn test_validate() -> Result<(), Error> {
270 let client = get_client().await?;
271 let project = client.projects(Some("Unit Testing")).await?;
272 assert!(!project.is_empty());
273 let project = project
274 .first()
275 .expect("'Unit Testing' project should exist");
276
277 let sessions = client.validation_sessions(project.id()).await?;
278 for session in &sessions {
279 let s = client.validation_session(session.id()).await?;
280 assert_eq!(s.id(), session.id());
281 assert_eq!(s.description(), session.description());
282 }
283
284 let session = sessions
285 .into_iter()
286 .find(|s| s.name() == "modelpack-usermanaged")
287 .ok_or_else(|| {
288 Error::InvalidParameters(format!(
289 "Validation session 'modelpack-usermanaged' not found in project '{}'",
290 project.name()
291 ))
292 })?;
293
294 let metrics = HashMap::from([("accuracy".to_string(), Parameter::Real(0.95))]);
295 session.set_metrics(&client, metrics).await?;
296
297 let metrics = session.metrics(&client).await?;
298 assert_eq!(metrics.get("accuracy"), Some(&Parameter::Real(0.95)));
299
300 Ok(())
301 }
302
303 #[tokio::test]
304 async fn test_download_artifact_success() -> Result<(), Error> {
305 let trainer = get_training_session_for_artifacts().await?;
306 let client = get_client().await?;
307 let artifacts = client.artifacts(trainer.id()).await?;
308 assert!(!artifacts.is_empty());
309
310 let test_dir = get_test_data_dir();
311 let artifact = &artifacts[0];
312 let output_path = test_dir.join(artifact.name());
313
314 client
315 .download_artifact(
316 trainer.id(),
317 artifact.name(),
318 Some(output_path.clone()),
319 None,
320 )
321 .await?;
322
323 assert!(output_path.exists());
324 if output_path.exists() {
325 std::fs::remove_file(&output_path)?;
326 }
327
328 Ok(())
329 }
330
331 #[tokio::test]
332 async fn test_download_artifact_not_found() -> Result<(), Error> {
333 let trainer = get_training_session_for_artifacts().await?;
334 let client = get_client().await?;
335 let test_dir = get_test_data_dir();
336 let fake_path = test_dir.join("nonexistent_artifact.txt");
337
338 let result = client
339 .download_artifact(
340 trainer.id(),
341 "nonexistent_artifact.txt",
342 Some(fake_path.clone()),
343 None,
344 )
345 .await;
346
347 assert!(result.is_err());
348 assert!(!fake_path.exists());
349
350 Ok(())
351 }
352
353 #[tokio::test]
354 async fn test_artifacts() -> Result<(), Error> {
355 let client = get_client().await?;
356 let project = client.projects(Some("Unit Testing")).await?;
357 assert!(!project.is_empty());
358 let project = project
359 .first()
360 .expect("'Unit Testing' project should exist");
361 let experiment = client
362 .experiments(project.id(), Some("Unit Testing"))
363 .await?;
364 let experiment = experiment
365 .first()
366 .expect("'Unit Testing' experiment should exist");
367 let trainer = client
368 .training_sessions(experiment.id(), Some("modelpack-960x540"))
369 .await?;
370 let trainer = trainer
371 .first()
372 .expect("'modelpack-960x540' training session should exist");
373 let artifacts = client.artifacts(trainer.id()).await?;
374 assert!(!artifacts.is_empty());
375
376 let test_dir = get_test_data_dir();
377
378 for artifact in artifacts {
379 let output_path = test_dir.join(artifact.name());
380 client
381 .download_artifact(
382 trainer.id(),
383 artifact.name(),
384 Some(output_path.clone()),
385 None,
386 )
387 .await?;
388
389 if output_path.exists() {
391 std::fs::remove_file(&output_path)?;
392 }
393 }
394
395 let fake_path = test_dir.join("fakefile.txt");
396 let res = client
397 .download_artifact(trainer.id(), "fakefile.txt", Some(fake_path.clone()), None)
398 .await;
399 assert!(res.is_err());
400 assert!(!fake_path.exists());
401
402 Ok(())
403 }
404
405 #[tokio::test]
406 async fn test_download_checkpoint_success() -> Result<(), Error> {
407 let trainer = get_training_session_for_checkpoints().await?;
408 let client = get_client().await?;
409 let test_dir = get_test_data_dir();
410
411 let checkpoint_path = test_dir.join("test_checkpoint.txt");
413 {
414 let mut f = File::create(&checkpoint_path)?;
415 f.write_all(b"Test Checkpoint Content")?;
416 }
417
418 trainer
420 .upload(
421 &client,
422 &[(
423 "checkpoints/test_checkpoint.txt".to_string(),
424 checkpoint_path.clone(),
425 )],
426 )
427 .await?;
428
429 let download_path = test_dir.join("downloaded_checkpoint.txt");
431 client
432 .download_checkpoint(
433 trainer.id(),
434 "test_checkpoint.txt",
435 Some(download_path.clone()),
436 None,
437 )
438 .await?;
439
440 let content = read_to_string(&download_path)?;
441 assert_eq!(content, "Test Checkpoint Content");
442
443 if checkpoint_path.exists() {
445 std::fs::remove_file(&checkpoint_path)?;
446 }
447 if download_path.exists() {
448 std::fs::remove_file(&download_path)?;
449 }
450
451 Ok(())
452 }
453
454 #[tokio::test]
455 async fn test_download_checkpoint_not_found() -> Result<(), Error> {
456 let trainer = get_training_session_for_checkpoints().await?;
457 let client = get_client().await?;
458 let test_dir = get_test_data_dir();
459 let fake_path = test_dir.join("nonexistent_checkpoint.txt");
460
461 let result = client
462 .download_checkpoint(
463 trainer.id(),
464 "nonexistent_checkpoint.txt",
465 Some(fake_path.clone()),
466 None,
467 )
468 .await;
469
470 assert!(result.is_err());
471 assert!(!fake_path.exists());
472
473 Ok(())
474 }
475
476 #[tokio::test]
477 async fn test_checkpoints() -> Result<(), Error> {
478 let client = get_client().await?;
479 let project = client.projects(Some("Unit Testing")).await?;
480 assert!(!project.is_empty());
481 let project = project
482 .first()
483 .expect("'Unit Testing' project should exist");
484 let experiment = client
485 .experiments(project.id(), Some("Unit Testing"))
486 .await?;
487 let experiment = experiment.first().ok_or_else(|| {
488 Error::InvalidParameters(format!(
489 "Experiment 'Unit Testing' not found in project '{}'",
490 project.name()
491 ))
492 })?;
493 let trainer = client
494 .training_sessions(experiment.id(), Some("modelpack-usermanaged"))
495 .await?;
496 let trainer = trainer
497 .first()
498 .expect("'modelpack-usermanaged' training session should exist");
499
500 let test_dir = get_test_data_dir();
501 let checkpoint_path = test_dir.join("checkpoint.txt");
502 let checkpoint2_path = test_dir.join("checkpoint2.txt");
503
504 {
505 let mut chkpt = File::create(&checkpoint_path)?;
506 chkpt.write_all(b"Test Checkpoint")?;
507 }
508
509 trainer
510 .upload(
511 &client,
512 &[(
513 "checkpoints/checkpoint.txt".to_string(),
514 checkpoint_path.clone(),
515 )],
516 )
517 .await?;
518
519 client
520 .download_checkpoint(
521 trainer.id(),
522 "checkpoint.txt",
523 Some(checkpoint2_path.clone()),
524 None,
525 )
526 .await?;
527
528 let chkpt = read_to_string(&checkpoint2_path)?;
529 assert_eq!(chkpt, "Test Checkpoint");
530
531 let fake_path = test_dir.join("fakefile.txt");
532 let res = client
533 .download_checkpoint(trainer.id(), "fakefile.txt", Some(fake_path.clone()), None)
534 .await;
535 assert!(res.is_err());
536 assert!(!fake_path.exists());
537
538 if checkpoint_path.exists() {
540 std::fs::remove_file(&checkpoint_path)?;
541 }
542 if checkpoint2_path.exists() {
543 std::fs::remove_file(&checkpoint2_path)?;
544 }
545
546 Ok(())
547 }
548
549 #[tokio::test]
550 async fn test_task_retrieval() -> Result<(), Error> {
551 let client = get_client().await?;
552
553 let tasks = client.tasks(None, None, None, None).await?;
555 assert!(!tasks.is_empty());
556
557 let task_id = tasks[0].id();
559 let task_info = client.task_info(task_id).await?;
560 assert_eq!(task_info.id(), task_id);
561
562 Ok(())
563 }
564
565 #[tokio::test]
566 async fn test_task_filtering_by_name() -> Result<(), Error> {
567 let client = get_client().await?;
568 let project = client.projects(Some("Unit Testing")).await?;
569 let project = project
570 .first()
571 .expect("'Unit Testing' project should exist");
572
573 let tasks = client
575 .tasks(Some("modelpack-usermanaged"), None, None, None)
576 .await?;
577
578 if !tasks.is_empty() {
579 let task_infos = tasks
581 .into_iter()
582 .map(|t| client.task_info(t.id()))
583 .collect::<Vec<_>>();
584 let task_infos = futures::future::try_join_all(task_infos).await?;
585
586 let filtered = task_infos
588 .into_iter()
589 .filter(|t| t.project_id() == Some(project.id()))
590 .collect::<Vec<_>>();
591
592 if !filtered.is_empty() {
593 assert_eq!(filtered[0].project_id(), Some(project.id()));
594 }
595 }
596
597 Ok(())
598 }
599
600 #[tokio::test]
601 async fn test_task_status_and_stages() -> Result<(), Error> {
602 let client = get_client().await?;
603
604 let tasks = client.tasks(None, None, None, None).await?;
606 if tasks.is_empty() {
607 return Ok(());
608 }
609
610 let task_id = tasks[0].id();
611
612 let status = client.task_status(task_id, "training").await?;
614 assert_eq!(status.id(), task_id);
615 assert_eq!(status.status(), "training");
616
617 let stages = [
619 ("download", "Downloading Dataset"),
620 ("train", "Training Model"),
621 ("export", "Exporting Model"),
622 ];
623 client.set_stages(task_id, &stages).await?;
624
625 client
627 .update_stage(task_id, "download", "running", "Downloading dataset", 50)
628 .await?;
629
630 let updated_task = client.task_info(task_id).await?;
632 assert_eq!(updated_task.id(), task_id);
633
634 Ok(())
635 }
636
637 #[tokio::test]
638 async fn test_tasks() -> Result<(), Error> {
639 let client = get_client().await?;
640 let tasks = client.tasks(None, None, None, None).await?;
641
642 for task in tasks {
643 let task_info = client.task_info(task.id()).await?;
644 println!("{} - {}", task, task_info);
645 }
646
647 let mut tasks = client
653 .tasks(Some("modelpack-usermanaged"), None, None, None)
654 .await?;
655 let was_fallback = if tasks.is_empty() {
656 tasks = client.tasks(None, None, None, None).await?;
657 true
658 } else {
659 false
660 };
661 if tasks.is_empty() {
662 println!(
663 "test_tasks: no tasks visible to the authenticated user; \
664 skipping task_info/status/stages assertions"
665 );
666 return Ok(());
667 }
668 let tasks = tasks
669 .into_iter()
670 .map(|t| client.task_info(t.id()))
671 .collect::<Vec<_>>();
672 let tasks = futures::future::try_join_all(tasks).await?;
673 assert_ne!(tasks.len(), 0);
674 let task = &tasks[0];
675
676 if was_fallback {
677 println!(
678 "test_tasks: fell back to non-fixture task {}; \
679 skipping mutation assertions (task_status/set_stages/update_stage) \
680 to avoid destructively modifying an arbitrary live task",
681 task.id()
682 );
683 return Ok(());
684 }
685
686 let t = client.task_status(task.id(), "training").await?;
687 assert_eq!(t.id(), task.id());
688 assert_eq!(t.status(), "training");
689
690 let stages = [
691 ("download", "Downloading Dataset"),
692 ("train", "Training Model"),
693 ("export", "Exporting Model"),
694 ];
695 client.set_stages(task.id(), &stages).await?;
696
697 client
698 .update_stage(task.id(), "download", "running", "Downloading dataset", 50)
699 .await?;
700
701 let task = client.task_info(task.id()).await?;
702 println!("task progress: {:?}", task.stages());
703
704 Ok(())
705 }
706
707 mod retry_url_classification {
712 use super::*;
713
714 #[test]
715 fn test_studio_api_base_url() {
716 assert_eq!(
718 classify_url("https://edgefirst.studio/api"),
719 RetryScope::StudioApi
720 );
721 }
722
723 #[test]
724 fn test_studio_api_with_trailing_slash() {
725 assert_eq!(
727 classify_url("https://edgefirst.studio/api/"),
728 RetryScope::StudioApi
729 );
730 }
731
732 #[test]
733 fn test_studio_api_with_path() {
734 assert_eq!(
736 classify_url("https://edgefirst.studio/api/datasets"),
737 RetryScope::StudioApi
738 );
739 assert_eq!(
740 classify_url("https://edgefirst.studio/api/auth.login"),
741 RetryScope::StudioApi
742 );
743 assert_eq!(
744 classify_url("https://edgefirst.studio/api/trainer/session"),
745 RetryScope::StudioApi
746 );
747 }
748
749 #[test]
750 fn test_studio_api_with_query_params() {
751 assert_eq!(
753 classify_url("https://edgefirst.studio/api?foo=bar"),
754 RetryScope::StudioApi
755 );
756 assert_eq!(
757 classify_url("https://edgefirst.studio/api/datasets?page=1&limit=10"),
758 RetryScope::StudioApi
759 );
760 }
761
762 #[test]
763 fn test_studio_api_subdomains() {
764 assert_eq!(
766 classify_url("https://test.edgefirst.studio/api"),
767 RetryScope::StudioApi
768 );
769 assert_eq!(
770 classify_url("https://stage.edgefirst.studio/api"),
771 RetryScope::StudioApi
772 );
773 assert_eq!(
774 classify_url("https://saas.edgefirst.studio/api"),
775 RetryScope::StudioApi
776 );
777 assert_eq!(
778 classify_url("https://ocean.edgefirst.studio/api"),
779 RetryScope::StudioApi
780 );
781 }
782
783 #[test]
784 fn test_studio_api_with_standard_port() {
785 assert_eq!(
787 classify_url("https://edgefirst.studio:443/api"),
788 RetryScope::StudioApi
789 );
790 assert_eq!(
791 classify_url("https://test.edgefirst.studio:443/api"),
792 RetryScope::StudioApi
793 );
794 }
795
796 #[test]
797 fn test_studio_api_with_custom_port() {
798 assert_eq!(
800 classify_url("https://test.edgefirst.studio:8080/api"),
801 RetryScope::StudioApi
802 );
803 assert_eq!(
804 classify_url("https://edgefirst.studio:8443/api"),
805 RetryScope::StudioApi
806 );
807 }
808
809 #[test]
810 fn test_studio_api_http_protocol() {
811 assert_eq!(
813 classify_url("http://edgefirst.studio/api"),
814 RetryScope::StudioApi
815 );
816 assert_eq!(
817 classify_url("http://test.edgefirst.studio/api"),
818 RetryScope::StudioApi
819 );
820 }
821
822 #[test]
823 fn test_file_io_s3_urls() {
824 assert_eq!(
826 classify_url("https://s3.amazonaws.com/bucket/file.bin"),
827 RetryScope::FileIO
828 );
829 assert_eq!(
830 classify_url("https://s3.us-west-2.amazonaws.com/mybucket/data.zip"),
831 RetryScope::FileIO
832 );
833 }
834
835 #[test]
836 fn test_file_io_cloudfront_urls() {
837 assert_eq!(
839 classify_url("https://d123abc.cloudfront.net/file.bin"),
840 RetryScope::FileIO
841 );
842 assert_eq!(
843 classify_url("https://d456def.cloudfront.net/path/to/file.tar.gz"),
844 RetryScope::FileIO
845 );
846 }
847
848 #[test]
849 fn test_file_io_non_api_studio_paths() {
850 assert_eq!(
852 classify_url("https://edgefirst.studio/docs"),
853 RetryScope::FileIO
854 );
855 assert_eq!(
856 classify_url("https://edgefirst.studio/download_model"),
857 RetryScope::FileIO
858 );
859 assert_eq!(
860 classify_url("https://test.edgefirst.studio/download_model"),
861 RetryScope::FileIO
862 );
863 assert_eq!(
864 classify_url("https://stage.edgefirst.studio/download_checkpoint"),
865 RetryScope::FileIO
866 );
867 }
868
869 #[test]
870 fn test_file_io_generic_urls() {
871 assert_eq!(
873 classify_url("https://example.com/download"),
874 RetryScope::FileIO
875 );
876 assert_eq!(
877 classify_url("https://cdn.example.com/files/data.json"),
878 RetryScope::FileIO
879 );
880 }
881
882 #[test]
883 fn test_security_malicious_url_substring() {
884 assert_eq!(
886 classify_url("https://evil.com/test.edgefirst.studio/api"),
887 RetryScope::FileIO
888 );
889 assert_eq!(
890 classify_url("https://attacker.com/edgefirst.studio/api/fake"),
891 RetryScope::FileIO
892 );
893 }
894
895 #[test]
896 fn test_edge_case_similar_domains() {
897 assert_eq!(
899 classify_url("https://edgefirst.studio.com/api"),
900 RetryScope::FileIO
901 );
902 assert_eq!(
903 classify_url("https://notedgefirst.studio/api"),
904 RetryScope::FileIO
905 );
906 assert_eq!(
907 classify_url("https://edgefirststudio.com/api"),
908 RetryScope::FileIO
909 );
910 }
911
912 #[test]
913 fn test_edge_case_invalid_urls() {
914 assert_eq!(classify_url("not a url"), RetryScope::FileIO);
916 assert_eq!(classify_url(""), RetryScope::FileIO);
917 assert_eq!(
918 classify_url("ftp://edgefirst.studio/api"),
919 RetryScope::FileIO
920 );
921 }
922
923 #[test]
924 fn test_edge_case_url_normalization() {
925 assert_eq!(
927 classify_url("https://EDGEFIRST.STUDIO/api"),
928 RetryScope::StudioApi
929 );
930 assert_eq!(
931 classify_url("https://test.EDGEFIRST.studio/api"),
932 RetryScope::StudioApi
933 );
934 }
935
936 #[test]
937 fn test_comprehensive_subdomain_coverage() {
938 let subdomains = vec![
940 "test", "stage", "saas", "ocean", "prod", "dev", "qa", "demo",
941 ];
942
943 for subdomain in subdomains {
944 let url = format!("https://{}.edgefirst.studio/api", subdomain);
945 assert_eq!(
946 classify_url(&url),
947 RetryScope::StudioApi,
948 "Failed for subdomain: {}",
949 subdomain
950 );
951 }
952 }
953
954 #[test]
955 fn test_api_path_variations() {
956 assert_eq!(
958 classify_url("https://edgefirst.studio/api"),
959 RetryScope::StudioApi
960 );
961 assert_eq!(
962 classify_url("https://edgefirst.studio/api/"),
963 RetryScope::StudioApi
964 );
965 assert_eq!(
966 classify_url("https://edgefirst.studio/api/v1"),
967 RetryScope::StudioApi
968 );
969 assert_eq!(
970 classify_url("https://edgefirst.studio/api/v2/datasets"),
971 RetryScope::StudioApi
972 );
973
974 assert_eq!(
976 classify_url("https://edgefirst.studio/apis"),
977 RetryScope::FileIO
978 );
979 assert_eq!(
980 classify_url("https://edgefirst.studio/v1/api"),
981 RetryScope::FileIO
982 );
983 }
984
985 #[test]
986 fn test_port_range_coverage() {
987 let ports = vec![80, 443, 8080, 8443, 3000, 5000, 9000];
989
990 for port in ports {
991 let url = format!("https://test.edgefirst.studio:{}/api", port);
992 assert_eq!(
993 classify_url(&url),
994 RetryScope::StudioApi,
995 "Failed for port: {}",
996 port
997 );
998 }
999 }
1000
1001 #[test]
1002 fn test_complex_query_strings() {
1003 assert_eq!(
1005 classify_url("https://edgefirst.studio/api?token=abc123&redirect=/dashboard"),
1006 RetryScope::StudioApi
1007 );
1008 assert_eq!(
1009 classify_url("https://test.edgefirst.studio/api?q=search%20term&page=1"),
1010 RetryScope::StudioApi
1011 );
1012 }
1013
1014 #[test]
1015 fn test_url_with_fragment() {
1016 assert_eq!(
1018 classify_url("https://edgefirst.studio/api#section"),
1019 RetryScope::StudioApi
1020 );
1021 assert_eq!(
1022 classify_url("https://test.edgefirst.studio/api/datasets#results"),
1023 RetryScope::StudioApi
1024 );
1025 }
1026 }
1027}