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