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