1mod api;
57mod client;
58pub mod coco;
59mod dataset;
60mod error;
61pub mod format;
62mod retry;
63mod storage;
64
65pub use crate::{
66 api::{
67 AnnotationSetID, AppId, Artifact, DatasetID, DatasetParams, Experiment, ExperimentID,
68 ImageId, Organization, OrganizationID, Parameter, PresignedUrl, Project, ProjectID,
69 SampleID, SamplesCountResult, SamplesPopulateParams, SamplesPopulateResult, SequenceId,
70 Snapshot, SnapshotFromDatasetResult, SnapshotID, SnapshotRestoreResult, Stage, Task,
71 TaskID, TaskInfo, TrainingSession, TrainingSessionID, ValidationSession,
72 ValidationSessionID,
73 },
74 client::{Client, Progress},
75 dataset::{
76 Annotation, AnnotationSet, AnnotationType, Box2d, Box3d, Dataset, FileType, GpsData, Group,
77 ImuData, Label, Location, Mask, Sample, SampleFile,
78 },
79 error::Error,
80 retry::{RetryScope, classify_url},
81 storage::{FileTokenStorage, MemoryTokenStorage, StorageError, TokenStorage},
82};
83
84#[cfg(feature = "polars")]
85#[allow(deprecated)] pub use crate::dataset::annotations_dataframe;
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 tasks = client
644 .tasks(Some("modelpack-usermanaged"), None, None, None)
645 .await?;
646 let tasks = tasks
647 .into_iter()
648 .map(|t| client.task_info(t.id()))
649 .collect::<Vec<_>>();
650 let tasks = futures::future::try_join_all(tasks).await?;
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 mod retry_url_classification {
680 use super::*;
681
682 #[test]
683 fn test_studio_api_base_url() {
684 assert_eq!(
686 classify_url("https://edgefirst.studio/api"),
687 RetryScope::StudioApi
688 );
689 }
690
691 #[test]
692 fn test_studio_api_with_trailing_slash() {
693 assert_eq!(
695 classify_url("https://edgefirst.studio/api/"),
696 RetryScope::StudioApi
697 );
698 }
699
700 #[test]
701 fn test_studio_api_with_path() {
702 assert_eq!(
704 classify_url("https://edgefirst.studio/api/datasets"),
705 RetryScope::StudioApi
706 );
707 assert_eq!(
708 classify_url("https://edgefirst.studio/api/auth.login"),
709 RetryScope::StudioApi
710 );
711 assert_eq!(
712 classify_url("https://edgefirst.studio/api/trainer/session"),
713 RetryScope::StudioApi
714 );
715 }
716
717 #[test]
718 fn test_studio_api_with_query_params() {
719 assert_eq!(
721 classify_url("https://edgefirst.studio/api?foo=bar"),
722 RetryScope::StudioApi
723 );
724 assert_eq!(
725 classify_url("https://edgefirst.studio/api/datasets?page=1&limit=10"),
726 RetryScope::StudioApi
727 );
728 }
729
730 #[test]
731 fn test_studio_api_subdomains() {
732 assert_eq!(
734 classify_url("https://test.edgefirst.studio/api"),
735 RetryScope::StudioApi
736 );
737 assert_eq!(
738 classify_url("https://stage.edgefirst.studio/api"),
739 RetryScope::StudioApi
740 );
741 assert_eq!(
742 classify_url("https://saas.edgefirst.studio/api"),
743 RetryScope::StudioApi
744 );
745 assert_eq!(
746 classify_url("https://ocean.edgefirst.studio/api"),
747 RetryScope::StudioApi
748 );
749 }
750
751 #[test]
752 fn test_studio_api_with_standard_port() {
753 assert_eq!(
755 classify_url("https://edgefirst.studio:443/api"),
756 RetryScope::StudioApi
757 );
758 assert_eq!(
759 classify_url("https://test.edgefirst.studio:443/api"),
760 RetryScope::StudioApi
761 );
762 }
763
764 #[test]
765 fn test_studio_api_with_custom_port() {
766 assert_eq!(
768 classify_url("https://test.edgefirst.studio:8080/api"),
769 RetryScope::StudioApi
770 );
771 assert_eq!(
772 classify_url("https://edgefirst.studio:8443/api"),
773 RetryScope::StudioApi
774 );
775 }
776
777 #[test]
778 fn test_studio_api_http_protocol() {
779 assert_eq!(
781 classify_url("http://edgefirst.studio/api"),
782 RetryScope::StudioApi
783 );
784 assert_eq!(
785 classify_url("http://test.edgefirst.studio/api"),
786 RetryScope::StudioApi
787 );
788 }
789
790 #[test]
791 fn test_file_io_s3_urls() {
792 assert_eq!(
794 classify_url("https://s3.amazonaws.com/bucket/file.bin"),
795 RetryScope::FileIO
796 );
797 assert_eq!(
798 classify_url("https://s3.us-west-2.amazonaws.com/mybucket/data.zip"),
799 RetryScope::FileIO
800 );
801 }
802
803 #[test]
804 fn test_file_io_cloudfront_urls() {
805 assert_eq!(
807 classify_url("https://d123abc.cloudfront.net/file.bin"),
808 RetryScope::FileIO
809 );
810 assert_eq!(
811 classify_url("https://d456def.cloudfront.net/path/to/file.tar.gz"),
812 RetryScope::FileIO
813 );
814 }
815
816 #[test]
817 fn test_file_io_non_api_studio_paths() {
818 assert_eq!(
820 classify_url("https://edgefirst.studio/docs"),
821 RetryScope::FileIO
822 );
823 assert_eq!(
824 classify_url("https://edgefirst.studio/download_model"),
825 RetryScope::FileIO
826 );
827 assert_eq!(
828 classify_url("https://test.edgefirst.studio/download_model"),
829 RetryScope::FileIO
830 );
831 assert_eq!(
832 classify_url("https://stage.edgefirst.studio/download_checkpoint"),
833 RetryScope::FileIO
834 );
835 }
836
837 #[test]
838 fn test_file_io_generic_urls() {
839 assert_eq!(
841 classify_url("https://example.com/download"),
842 RetryScope::FileIO
843 );
844 assert_eq!(
845 classify_url("https://cdn.example.com/files/data.json"),
846 RetryScope::FileIO
847 );
848 }
849
850 #[test]
851 fn test_security_malicious_url_substring() {
852 assert_eq!(
854 classify_url("https://evil.com/test.edgefirst.studio/api"),
855 RetryScope::FileIO
856 );
857 assert_eq!(
858 classify_url("https://attacker.com/edgefirst.studio/api/fake"),
859 RetryScope::FileIO
860 );
861 }
862
863 #[test]
864 fn test_edge_case_similar_domains() {
865 assert_eq!(
867 classify_url("https://edgefirst.studio.com/api"),
868 RetryScope::FileIO
869 );
870 assert_eq!(
871 classify_url("https://notedgefirst.studio/api"),
872 RetryScope::FileIO
873 );
874 assert_eq!(
875 classify_url("https://edgefirststudio.com/api"),
876 RetryScope::FileIO
877 );
878 }
879
880 #[test]
881 fn test_edge_case_invalid_urls() {
882 assert_eq!(classify_url("not a url"), RetryScope::FileIO);
884 assert_eq!(classify_url(""), RetryScope::FileIO);
885 assert_eq!(
886 classify_url("ftp://edgefirst.studio/api"),
887 RetryScope::FileIO
888 );
889 }
890
891 #[test]
892 fn test_edge_case_url_normalization() {
893 assert_eq!(
895 classify_url("https://EDGEFIRST.STUDIO/api"),
896 RetryScope::StudioApi
897 );
898 assert_eq!(
899 classify_url("https://test.EDGEFIRST.studio/api"),
900 RetryScope::StudioApi
901 );
902 }
903
904 #[test]
905 fn test_comprehensive_subdomain_coverage() {
906 let subdomains = vec![
908 "test", "stage", "saas", "ocean", "prod", "dev", "qa", "demo",
909 ];
910
911 for subdomain in subdomains {
912 let url = format!("https://{}.edgefirst.studio/api", subdomain);
913 assert_eq!(
914 classify_url(&url),
915 RetryScope::StudioApi,
916 "Failed for subdomain: {}",
917 subdomain
918 );
919 }
920 }
921
922 #[test]
923 fn test_api_path_variations() {
924 assert_eq!(
926 classify_url("https://edgefirst.studio/api"),
927 RetryScope::StudioApi
928 );
929 assert_eq!(
930 classify_url("https://edgefirst.studio/api/"),
931 RetryScope::StudioApi
932 );
933 assert_eq!(
934 classify_url("https://edgefirst.studio/api/v1"),
935 RetryScope::StudioApi
936 );
937 assert_eq!(
938 classify_url("https://edgefirst.studio/api/v2/datasets"),
939 RetryScope::StudioApi
940 );
941
942 assert_eq!(
944 classify_url("https://edgefirst.studio/apis"),
945 RetryScope::FileIO
946 );
947 assert_eq!(
948 classify_url("https://edgefirst.studio/v1/api"),
949 RetryScope::FileIO
950 );
951 }
952
953 #[test]
954 fn test_port_range_coverage() {
955 let ports = vec![80, 443, 8080, 8443, 3000, 5000, 9000];
957
958 for port in ports {
959 let url = format!("https://test.edgefirst.studio:{}/api", port);
960 assert_eq!(
961 classify_url(&url),
962 RetryScope::StudioApi,
963 "Failed for port: {}",
964 port
965 );
966 }
967 }
968
969 #[test]
970 fn test_complex_query_strings() {
971 assert_eq!(
973 classify_url("https://edgefirst.studio/api?token=abc123&redirect=/dashboard"),
974 RetryScope::StudioApi
975 );
976 assert_eq!(
977 classify_url("https://test.edgefirst.studio/api?q=search%20term&page=1"),
978 RetryScope::StudioApi
979 );
980 }
981
982 #[test]
983 fn test_url_with_fragment() {
984 assert_eq!(
986 classify_url("https://edgefirst.studio/api#section"),
987 RetryScope::StudioApi
988 );
989 assert_eq!(
990 classify_url("https://test.edgefirst.studio/api/datasets#results"),
991 RetryScope::StudioApi
992 );
993 }
994 }
995}