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