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