1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use semver::{Version, VersionReq};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use snafu::OptionExt;
10
11use crate::{
12 Result,
13 cache::Cache,
14 cargo::{CargoMetadataOptions, CargoRunner},
15 config::Config,
16 cratespec::{CrateSpec, Forge, RegistrySource},
17 error,
18 git::{GitClient, GitSelector},
19 http::HttpClient,
20 registry::RegistryClient,
21};
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct ResolvedCrate {
34 pub name: String,
36
37 pub version: Version,
39
40 pub source: ResolvedSource,
42}
43
44pub trait CrateResolver: std::fmt::Debug + Send + Sync + 'static {
50 fn resolve(&self, spec: &CrateSpec) -> Result<ResolvedCrate>;
63}
64
65#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum ResolvedSource {
73 CratesIo,
75
76 Registry {
78 source: RegistrySource,
80 },
81
82 Git {
84 repo: String,
86
87 commit: String,
89 },
90
91 Forge {
93 forge: Forge,
95
96 commit: String,
98 },
99
100 LocalDir {
102 path: PathBuf,
104 },
105}
106
107impl ResolvedSource {
108 pub(crate) fn source_hash(&self) -> String {
110 let json = serde_json::to_string(self).expect("BUG: serializing a ResolvedSource cannot fail");
111 let mut hasher = Sha256::new();
112 hasher.update(json.as_bytes());
113 crate::helpers::format_hex_lower(hasher.finalize())
114 }
115}
116
117pub(crate) fn create_resolver(
120 config: Config,
121 cache: Cache,
122 git_client: GitClient,
123 cargo: Arc<dyn CargoRunner>,
124 http_client: HttpClient,
125) -> impl CrateResolver {
126 let inner = DefaultCrateResolver::new(config, git_client, cargo, http_client);
127 CachingResolver::new(inner, cache)
128}
129
130#[derive(Debug, Clone)]
133struct DefaultCrateResolver {
134 config: Config,
135 git_client: GitClient,
136 cargo: Arc<dyn CargoRunner>,
137 http_client: HttpClient,
138}
139
140impl DefaultCrateResolver {
141 pub(crate) fn new(
143 config: Config,
144 git_client: GitClient,
145 cargo: Arc<dyn CargoRunner>,
146 http_client: HttpClient,
147 ) -> Self {
148 Self {
149 config,
150 git_client,
151 cargo,
152 http_client,
153 }
154 }
155
156 fn resolve_local_dir(
158 &self,
159 path: &Path,
160 name: &Option<String>,
161 version: &Option<VersionReq>,
162 ) -> Result<ResolvedCrate> {
163 let metadata = self.cargo.metadata(
164 path,
165 &CargoMetadataOptions {
166 no_deps: true,
167 ..Default::default()
168 },
169 )?;
170
171 let package = if let Some(n) = name {
172 metadata
173 .packages
174 .iter()
175 .find(|p| p.name.as_str() == n)
176 .with_context(|| error::PackageNotFoundInWorkspaceSnafu {
177 name: n.clone(),
178 available: metadata
179 .packages
180 .iter()
181 .map(|p| p.name.to_string())
182 .collect::<Vec<_>>(),
183 })?
184 } else {
185 if metadata.packages.len() != 1 {
186 return error::AmbiguousPackageNameSnafu {
187 count: metadata.packages.len(),
188 }
189 .fail();
190 }
191 &metadata.packages[0]
192 };
193
194 if let Some(req) = version {
195 if !req.matches(&package.version) {
196 return error::VersionMismatchSnafu {
197 requirement: req.to_string(),
198 found: package.version.clone(),
199 }
200 .fail();
201 }
202 }
203
204 Ok(ResolvedCrate {
205 name: package.name.to_string(),
206 version: package.version.clone(),
207 source: ResolvedSource::LocalDir {
208 path: path.to_path_buf(),
209 },
210 })
211 }
212
213 fn resolve_registry(
217 &self,
218 name: &str,
219 version: Option<&VersionReq>,
220 source: Option<&RegistrySource>,
221 ) -> Result<ResolvedCrate> {
222 let version = version.cloned().unwrap_or(VersionReq::STAR);
224 let registry = RegistryClient::new(source, &self.http_client, &self.config.http)?;
225 let versions = match registry.crate_versions(name, self.config.offline)? {
226 Some(versions) => versions,
227 None if self.config.offline => {
228 return error::OfflineModeSnafu {
229 name: name.to_string(),
230 version: version.to_string(),
231 }
232 .fail();
233 }
234 None => {
235 return error::CrateNotFoundInRegistrySnafu {
236 name: name.to_string(),
237 }
238 .fail();
239 }
240 };
241
242 let best_version = versions
245 .iter()
246 .filter(|v| !v.yanked)
247 .filter_map(|v| {
248 Version::parse(&v.version)
249 .ok()
250 .filter(|ver| version.matches(ver))
251 .map(|ver| (v.version.clone(), ver))
252 })
253 .max_by(|(_, a), (_, b)| a.cmp(b))
254 .map(|(_, best)| best)
255 .with_context(|| error::NoMatchingVersionSnafu {
256 name: name.to_string(),
257 requirement: version.to_string(),
258 })?;
259
260 let resolved_source = match source {
263 None => ResolvedSource::CratesIo,
264 Some(custom_registry) => ResolvedSource::Registry {
265 source: custom_registry.clone(),
266 },
267 };
268
269 Ok(ResolvedCrate {
270 name: name.to_string(),
271 version: best_version,
272 source: resolved_source,
273 })
274 }
275
276 fn resolve_git(
278 &self,
279 repo: &str,
280 selector: &GitSelector,
281 name: &Option<String>,
282 version: &Option<VersionReq>,
283 ) -> Result<ResolvedCrate> {
284 let (checkout_path, commit_hash) = self.git_client.checkout_ref(repo, selector.clone())?;
286
287 let metadata = self.cargo.metadata(
289 &checkout_path,
290 &CargoMetadataOptions {
291 no_deps: true,
292 ..Default::default()
293 },
294 )?;
295
296 let package = if let Some(n) = name {
297 metadata
298 .packages
299 .iter()
300 .find(|p| p.name.as_str() == n)
301 .with_context(|| error::PackageNotFoundInWorkspaceSnafu {
302 name: n.clone(),
303 available: metadata
304 .packages
305 .iter()
306 .map(|p| p.name.to_string())
307 .collect::<Vec<_>>(),
308 })?
309 } else {
310 if metadata.packages.len() != 1 {
311 return error::AmbiguousPackageNameSnafu {
312 count: metadata.packages.len(),
313 }
314 .fail();
315 }
316 &metadata.packages[0]
317 };
318
319 if let Some(req) = version {
320 if !req.matches(&package.version) {
321 return error::VersionMismatchSnafu {
322 requirement: req.to_string(),
323 found: package.version.clone(),
324 }
325 .fail();
326 }
327 }
328
329 Ok(ResolvedCrate {
330 name: package.name.to_string(),
331 version: package.version.clone(),
332 source: ResolvedSource::Git {
333 repo: repo.to_string(),
334 commit: commit_hash,
335 },
336 })
337 }
338
339 fn resolve_forge(
341 &self,
342 forge: &Forge,
343 selector: &GitSelector,
344 name: &Option<String>,
345 version: &Option<VersionReq>,
346 ) -> Result<ResolvedCrate> {
347 let git_url = forge.git_url();
349
350 let mut resolved = self.resolve_git(&git_url, selector, name, version)?;
352
353 if let ResolvedSource::Git { commit, .. } = resolved.source {
355 resolved.source = ResolvedSource::Forge {
356 forge: forge.clone(),
357 commit,
358 };
359 } else {
360 unreachable!("BUG: Expected ResolvedSource::Git from resolve_git");
361 }
362
363 Ok(resolved)
364 }
365}
366
367impl CrateResolver for DefaultCrateResolver {
368 fn resolve(&self, spec: &CrateSpec) -> Result<ResolvedCrate> {
369 match spec {
370 CrateSpec::CratesIo { name, version } => self.resolve_registry(name, version.as_ref(), None),
371 CrateSpec::Registry {
372 source,
373 name,
374 version,
375 } => self.resolve_registry(name, version.as_ref(), Some(source)),
376 CrateSpec::Git {
377 repo,
378 selector,
379 name,
380 version,
381 } => self.resolve_git(repo, selector, name, version),
382 CrateSpec::Forge {
383 forge,
384 selector,
385 name,
386 version,
387 } => self.resolve_forge(forge, selector, name, version),
388 CrateSpec::LocalDir { path, name, version } => self.resolve_local_dir(path, name, version),
389 }
390 }
391}
392
393#[derive(Debug)]
399pub(crate) struct CachingResolver<R: CrateResolver> {
400 inner: R,
401 cache: Cache,
402}
403
404impl<R: CrateResolver> CachingResolver<R> {
405 pub(crate) fn new(inner: R, cache: Cache) -> Self {
407 Self { inner, cache }
408 }
409}
410
411impl<R: CrateResolver> CrateResolver for CachingResolver<R> {
412 fn resolve(&self, spec: &CrateSpec) -> Result<ResolvedCrate> {
413 if matches!(spec, CrateSpec::LocalDir { .. }) {
414 return self.inner.resolve(spec);
415 }
416
417 self.cache.get_or_resolve_crate(spec, || self.inner.resolve(spec))
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use std::time::Duration;
424
425 use assert_matches::assert_matches;
426
427 use super::*;
428 use crate::testdata::CrateTestCase;
429
430 fn test_resolver() -> (CachingResolver<DefaultCrateResolver>, tempfile::TempDir) {
434 crate::logging::init_test_logging();
435
436 let (temp_dir, config) = crate::config::create_test_env();
437 let reporter = crate::messages::MessageReporter::null();
438 let cache = Cache::new(config.clone(), reporter.clone());
439 let git_client = GitClient::new(cache.clone(), reporter.clone(), config.http.clone());
440 let http_client = HttpClient::new(&config.http).unwrap();
441 let resolver = DefaultCrateResolver::new(
442 config.clone(),
443 git_client,
444 Arc::new(crate::cargo::create_cargo_runner(config.clone(), reporter).unwrap()),
445 http_client,
446 );
447 (CachingResolver::new(resolver, cache), temp_dir)
448 }
449
450 fn test_resolver_offline() -> (CachingResolver<DefaultCrateResolver>, tempfile::TempDir) {
452 let (resolver, temp_dir) = test_resolver();
453 let mut config = resolver.inner.config;
454 config.offline = true;
455 let reporter = crate::messages::MessageReporter::null();
456 let cache = Cache::new(config.clone(), reporter.clone());
457 let git_client = GitClient::new(cache.clone(), reporter, config.http.clone());
458 let http_client = HttpClient::new(&config.http).unwrap();
459 let resolver = DefaultCrateResolver::new(config, git_client, resolver.inner.cargo, http_client);
460 (CachingResolver::new(resolver, cache), temp_dir)
461 }
462
463 mod local_dir {
465 use super::*;
466 use crate::error::Error;
467
468 #[test]
469 fn single_package_auto_name() {
470 let (resolver, _temp_dir) = test_resolver();
471 let testcase = CrateTestCase::simple_bin_no_deps();
472
473 let spec = CrateSpec::LocalDir {
474 path: testcase.path().to_path_buf(),
475 name: None,
476 version: None,
477 };
478
479 let resolved = resolver.resolve(&spec).unwrap();
480 assert_eq!(resolved.name, "simple-bin-no-deps");
481 assert_matches!(resolved.source, ResolvedSource::LocalDir { .. });
482 }
483
484 #[test]
485 fn single_package_explicit_name() {
486 let (resolver, _temp_dir) = test_resolver();
487 let testcase = CrateTestCase::simple_bin_no_deps();
488
489 let spec = CrateSpec::LocalDir {
490 path: testcase.path().to_path_buf(),
491 name: Some("simple-bin-no-deps".to_string()),
492 version: None,
493 };
494
495 let resolved = resolver.resolve(&spec).unwrap();
496 assert_eq!(resolved.name, "simple-bin-no-deps");
497 }
498
499 #[test]
500 fn single_package_wrong_name() {
501 let (resolver, _temp_dir) = test_resolver();
502 let testcase = CrateTestCase::simple_bin_no_deps();
503
504 let spec = CrateSpec::LocalDir {
505 path: testcase.path().to_path_buf(),
506 name: Some("wrong-name".to_string()),
507 version: None,
508 };
509
510 let result = resolver.resolve(&spec);
511 assert_matches!(result.unwrap_err(), Error::PackageNotFoundInWorkspace { .. });
512 }
513
514 #[test]
515 fn single_package_version_req_match() {
516 let (resolver, _temp_dir) = test_resolver();
517 let testcase = CrateTestCase::simple_bin_no_deps();
518
519 let spec = CrateSpec::LocalDir {
520 path: testcase.path().to_path_buf(),
521 name: None,
522 version: Some(VersionReq::parse(">=0.1.0").unwrap()),
523 };
524
525 let resolved = resolver.resolve(&spec).unwrap();
526 assert_eq!(resolved.version, Version::parse("0.1.0").unwrap());
527 }
528
529 #[test]
530 fn single_package_version_req_mismatch() {
531 let (resolver, _temp_dir) = test_resolver();
532 let testcase = CrateTestCase::simple_bin_no_deps();
533
534 let spec = CrateSpec::LocalDir {
535 path: testcase.path().to_path_buf(),
536 name: None,
537 version: Some(VersionReq::parse(">=999.0.0").unwrap()),
538 };
539
540 let result = resolver.resolve(&spec);
541 assert_matches!(result.unwrap_err(), Error::VersionMismatch { .. });
542 }
543
544 #[test]
545 fn invalid_path() {
546 let (resolver, _temp_dir) = test_resolver();
547 let invalid_path = PathBuf::from("/nonexistent/path/to/nowhere");
548
549 let spec = CrateSpec::LocalDir {
550 path: invalid_path,
551 name: None,
552 version: None,
553 };
554
555 let result = resolver.resolve(&spec);
556 assert_matches!(result.unwrap_err(), Error::CargoMetadata { .. });
557 }
558
559 #[test]
560 fn workspace_ambiguous_without_name() {
561 let (resolver, _temp_dir) = test_resolver();
562 let testcase = CrateTestCase::workspace_all_libs();
563
564 let spec = CrateSpec::LocalDir {
565 path: testcase.path().to_path_buf(),
566 name: None,
567 version: None,
568 };
569
570 let result = resolver.resolve(&spec);
571 assert_matches!(result.unwrap_err(), Error::AmbiguousPackageName { .. });
572 }
573
574 #[test]
575 fn workspace_with_valid_package_name() {
576 let (resolver, _temp_dir) = test_resolver();
577 let testcase = CrateTestCase::workspace_all_libs();
578
579 let spec = CrateSpec::LocalDir {
580 path: testcase.path().to_path_buf(),
581 name: Some("lib1".to_string()),
582 version: None,
583 };
584
585 let resolved = resolver.resolve(&spec).unwrap();
586 assert_eq!(resolved.name, "lib1");
587 assert_eq!(resolved.version, Version::parse("0.1.0").unwrap());
588 }
589
590 #[test]
591 fn workspace_with_nonexistent_package() {
592 let (resolver, _temp_dir) = test_resolver();
593 let testcase = CrateTestCase::workspace_all_libs();
594
595 let spec = CrateSpec::LocalDir {
596 path: testcase.path().to_path_buf(),
597 name: Some("nonexistent".to_string()),
598 version: None,
599 };
600
601 let result = resolver.resolve(&spec);
602 assert_matches!(result.unwrap_err(), Error::PackageNotFoundInWorkspace { .. });
603 }
604
605 #[test]
606 fn workspace_package_with_version() {
607 let (resolver, _temp_dir) = test_resolver();
608 let testcase = CrateTestCase::workspace_all_libs();
609
610 let spec = CrateSpec::LocalDir {
611 path: testcase.path().to_path_buf(),
612 name: Some("lib2".to_string()),
613 version: Some(VersionReq::parse("=0.1.0").unwrap()),
614 };
615
616 let resolved = resolver.resolve(&spec).unwrap();
617 assert_eq!(resolved.name, "lib2");
618 assert_eq!(resolved.version, Version::parse("0.1.0").unwrap());
619 }
620
621 #[test]
622 fn library_package_auto_name() {
623 let (resolver, _temp_dir) = test_resolver();
624 let testcase = CrateTestCase::simple_lib_no_deps();
625
626 let spec = CrateSpec::LocalDir {
627 path: testcase.path().to_path_buf(),
628 name: None,
629 version: None,
630 };
631
632 let resolved = resolver.resolve(&spec).unwrap();
633 assert_eq!(resolved.name, "simple-lib-no-deps");
634 assert_matches!(resolved.source, ResolvedSource::LocalDir { .. });
635 }
636 }
637
638 mod registry {
643 use std::thread;
644
645 use rand::seq::SliceRandom;
646
647 use super::*;
648 use crate::error::Error;
649
650 #[test]
651 fn serde_latest() {
652 let (resolver, _temp_dir) = test_resolver();
653
654 let spec = CrateSpec::CratesIo {
655 name: "serde".to_string(),
656 version: None,
657 };
658
659 let resolved = resolver.resolve(&spec).unwrap();
660 assert_eq!(resolved.name, "serde");
661 assert_matches!(resolved.source, ResolvedSource::CratesIo);
662 }
663
664 #[test]
665 fn with_version() {
666 let (resolver, _temp_dir) = test_resolver();
667 let version_req = VersionReq::parse("^1.0").unwrap();
668
669 let spec = CrateSpec::CratesIo {
670 name: "serde".to_string(),
671 version: Some(version_req.clone()),
672 };
673
674 let resolved = resolver.resolve(&spec).unwrap();
675 assert_eq!(resolved.name, "serde");
676 assert!(version_req.matches(&resolved.version));
677 }
678
679 #[test]
680 fn star_version() {
681 let (resolver, _temp_dir) = test_resolver();
682
683 let spec = CrateSpec::CratesIo {
684 name: "tokio".to_string(),
685 version: Some(VersionReq::STAR),
686 };
687
688 let resolved = resolver.resolve(&spec).unwrap();
689 assert_eq!(resolved.name, "tokio");
690 }
691
692 #[test]
693 fn nonexistent() {
694 let (resolver, _temp_dir) = test_resolver();
695
696 let spec = CrateSpec::CratesIo {
697 name: "definitely-not-a-real-crate-xyzzy-12345".to_string(),
698 version: None,
699 };
700
701 let result = resolver.resolve(&spec);
702
703 assert_matches!(result.unwrap_err(), Error::CrateNotFoundInRegistry { .. });
704 }
705
706 #[test]
709 fn non_existent_version() {
710 let (resolver, _temp_dir) = test_resolver();
711 let version_req = VersionReq::parse(">=999.0.0").unwrap();
712
713 let spec = CrateSpec::CratesIo {
714 name: "serde".to_string(),
715 version: Some(version_req),
716 };
717
718 let result = resolver.resolve(&spec);
719
720 assert_matches!(result.unwrap_err(), Error::NoMatchingVersion { .. });
721 }
722
723 #[test]
724 fn selects_highest_version() {
725 let (resolver, _temp_dir) = test_resolver();
726 let version_req = VersionReq::parse(">=1.0.0").unwrap();
727
728 let spec = CrateSpec::CratesIo {
729 name: "serde".to_string(),
730 version: Some(version_req.clone()),
731 };
732
733 let resolved = resolver.resolve(&spec).unwrap();
734 assert!(resolved.version.major >= 1);
735 assert!(version_req.matches(&resolved.version));
736 }
737
738 #[test]
745 fn offline_without_cached_fails() {
746 let (resolver, _temp_dir) = test_resolver_offline();
747
748 let spec = CrateSpec::CratesIo {
749 name: "definitely-not-real-crate-xyzzy-offline-99999".to_string(),
750 version: None,
751 };
752
753 let result = resolver.resolve(&spec);
754
755 assert_matches!(result.unwrap_err(), Error::OfflineMode { .. });
756 }
757
758 #[test]
765 fn offline_with_cached_works() {
766 let (online_resolver, _temp_dir) = test_resolver();
767
768 let version_req = VersionReq::parse("^1.0").unwrap();
770 let spec = CrateSpec::CratesIo {
771 name: "serde".to_string(),
772 version: Some(version_req),
773 };
774
775 let online_resolved = online_resolver.resolve(&spec).unwrap();
776
777 let offline_config = Config {
779 offline: true,
780 ..online_resolver.inner.config.clone()
781 };
782 let git_client = GitClient::new(
783 online_resolver.cache.clone(),
784 crate::messages::MessageReporter::null(),
785 offline_config.http.clone(),
786 );
787 let http_client = HttpClient::new(&offline_config.http).unwrap();
788 let offline_resolver = CachingResolver::new(
789 DefaultCrateResolver::new(
790 offline_config,
791 git_client,
792 online_resolver.inner.cargo.clone(),
793 http_client,
794 ),
795 online_resolver.cache.clone(),
796 );
797
798 let offline_resolved = offline_resolver.resolve(&spec).unwrap();
799
800 assert_eq!(online_resolved.name, offline_resolved.name);
801 assert_eq!(online_resolved.version, offline_resolved.version);
802 }
803
804 #[test]
812 fn stale_invalid_crate_returned_in_offline_mode() {
813 let (resolver, _temp_dir) = test_resolver();
814 let cache_timeout = resolver.inner.config.resolve_cache_timeout;
815
816 let invalid_spec = CrateSpec::CratesIo {
818 name: "+invalid-crate-name".to_string(),
819 version: None,
820 };
821
822 let fake_resolved = ResolvedCrate {
824 name: "+invalid-crate-name".to_string(),
825 version: Version::parse("1.0.0").unwrap(),
826 source: ResolvedSource::CratesIo,
827 };
828
829 resolver
831 .cache
832 .insert_stale_resolve_entry(
833 &invalid_spec,
834 &fake_resolved,
835 cache_timeout + Duration::from_secs(1),
836 )
837 .unwrap();
838
839 let offline_config = Config {
842 offline: true,
843 ..resolver.inner.config.clone()
844 };
845 let git_client = GitClient::new(
846 resolver.cache.clone(),
847 crate::messages::MessageReporter::null(),
848 offline_config.http.clone(),
849 );
850 let http_client = HttpClient::new(&offline_config.http).unwrap();
851 let offline_resolver = CachingResolver::new(
852 DefaultCrateResolver::new(
853 offline_config,
854 git_client,
855 resolver.inner.cargo.clone(),
856 http_client,
857 ),
858 resolver.cache,
859 );
860
861 let resolved = offline_resolver.resolve(&invalid_spec).unwrap();
863
864 assert_eq!(resolved.name, fake_resolved.name);
865 assert_eq!(resolved.version, fake_resolved.version);
866 }
867
868 #[test]
875 fn cache_serves_non_stale_entry_without_registry_lookup() {
876 let (resolver, _temp_dir) = test_resolver();
877
878 let spec = CrateSpec::CratesIo {
880 name: "serde".to_string(),
881 version: None,
882 };
883
884 let fake_resolved = ResolvedCrate {
886 name: "serde".to_string(),
887 version: Version::parse("999.99.99").unwrap(),
888 source: ResolvedSource::CratesIo,
889 };
890
891 resolver
893 .cache
894 .insert_stale_resolve_entry(&spec, &fake_resolved, Duration::from_secs(1))
895 .unwrap();
896
897 let resolved = resolver.resolve(&spec).unwrap();
899
900 assert_eq!(resolved.name, "serde");
901 assert_eq!(resolved.version, Version::parse("999.99.99").unwrap());
902 }
903
904 #[test]
912 fn stale_cache_not_used_for_permanent_errors() {
913 let (resolver, _temp_dir) = test_resolver();
914 let cache_timeout = resolver.inner.config.resolve_cache_timeout;
915
916 let spec = CrateSpec::CratesIo {
918 name: "serde".to_string(),
919 version: Some(VersionReq::parse("=999.99.99").unwrap()),
920 };
921
922 let fake_resolved = ResolvedCrate {
924 name: "serde".to_string(),
925 version: Version::parse("999.99.99").unwrap(),
926 source: ResolvedSource::CratesIo,
927 };
928
929 resolver
931 .cache
932 .insert_stale_resolve_entry(&spec, &fake_resolved, cache_timeout + Duration::from_secs(1))
933 .unwrap();
934
935 let result = resolver.resolve(&spec);
938
939 assert_matches!(result.unwrap_err(), Error::NoMatchingVersion { .. });
940 }
941
942 #[test]
954 fn lock_contention_stress_test() {
955 let crates = vec![
956 "serde",
957 "tokio",
958 "reqwest",
959 "clap",
960 "anyhow",
961 "thiserror",
962 "tracing",
963 "syn",
964 "quote",
965 "proc-macro2",
966 "serde_json",
967 "regex",
968 "rayon",
969 "bytes",
970 "http",
971 "futures",
972 "async-trait",
973 "rand",
974 "chrono",
975 "log",
976 ];
977
978 let (resolver, _temp_dir) = test_resolver();
979 let resolver = Arc::new(resolver);
980
981 let mut handles = vec![];
982
983 for thread_id in 0..20 {
984 let resolver = Arc::clone(&resolver);
985 let mut thread_crates = crates.clone();
986
987 let handle = thread::spawn(move || {
988 let mut rng = rand::rng();
989 thread_crates.shuffle(&mut rng);
990
991 for krate_name in thread_crates.iter().take(10) {
992 let spec = CrateSpec::CratesIo {
993 name: (*krate_name).to_string(),
994 version: None,
995 };
996
997 match resolver.resolve(&spec) {
998 Ok(resolved) => {
999 assert_eq!(resolved.name, *krate_name);
1000 }
1001 Err(e) => {
1002 panic!("[Thread {}] Failed to resolve {}: {:?}", thread_id, krate_name, e);
1003 }
1004 }
1005 }
1006 });
1007
1008 handles.push(handle);
1009 }
1010
1011 for (i, handle) in handles.into_iter().enumerate() {
1012 handle
1013 .join()
1014 .unwrap_or_else(|e| panic!("Thread {} panicked: {:?}", i, e));
1015 }
1016 }
1017 }
1018
1019 mod git {
1021 use super::*;
1022 use crate::error::Error;
1023
1024 #[test]
1026 fn default_branch() {
1027 let (resolver, _temp_dir) = test_resolver();
1028 let repo = "https://github.com/rust-lang/rustlings.git";
1029
1030 let spec = CrateSpec::Git {
1031 repo: repo.to_string(),
1032 selector: GitSelector::DefaultBranch,
1033 name: Some("rustlings".to_string()),
1034 version: None,
1035 };
1036
1037 let resolved = resolver.resolve(&spec).unwrap();
1038 assert_eq!(resolved.name, "rustlings");
1039 if let ResolvedSource::Git { repo: r, commit } = &resolved.source {
1040 assert_eq!(r, repo);
1041 assert!(!commit.is_empty());
1042 } else {
1043 panic!("Expected Git source, got {:?}", resolved.source);
1044 }
1045 }
1046
1047 #[test]
1048 fn with_branch() {
1049 let (resolver, _temp_dir) = test_resolver();
1050
1051 let spec = CrateSpec::Git {
1052 repo: "https://github.com/rust-lang/rustlings.git".to_string(),
1053 selector: GitSelector::Branch("main".to_string()),
1054 name: Some("rustlings".to_string()),
1055 version: None,
1056 };
1057
1058 let resolved = resolver.resolve(&spec).unwrap();
1059 if let ResolvedSource::Git { commit, .. } = &resolved.source {
1060 assert!(!commit.is_empty());
1061 } else {
1062 panic!("Expected Git source, got {:?}", resolved.source);
1063 }
1064 }
1065
1066 #[test]
1067 fn with_tag() {
1068 let (resolver, _temp_dir) = test_resolver();
1069
1070 let spec = CrateSpec::Git {
1071 repo: "https://github.com/rust-lang/rustlings.git".to_string(),
1072 selector: GitSelector::Tag("v6.0.0".to_string()),
1073 name: Some("rustlings".to_string()),
1074 version: None,
1075 };
1076
1077 let resolved = resolver.resolve(&spec).unwrap();
1078 if let ResolvedSource::Git { commit, .. } = &resolved.source {
1079 assert!(!commit.is_empty());
1080 } else {
1081 panic!("Expected Git source, got {:?}", resolved.source);
1082 }
1083 }
1084
1085 #[test]
1086 fn with_commit() {
1087 let (resolver, _temp_dir) = test_resolver();
1088
1089 let spec = CrateSpec::Git {
1092 repo: "https://github.com/rust-lang/rustlings.git".to_string(),
1093 selector: GitSelector::Commit("28d2bb04326d7036514245d73f10fb72b9ed108c".to_string()),
1094 name: Some("rustlings".to_string()),
1095 version: None,
1096 };
1097
1098 let resolved = resolver.resolve(&spec).unwrap();
1099 assert_eq!(resolved.name, "rustlings");
1100
1101 if let ResolvedSource::Git { repo: r, commit } = &resolved.source {
1102 assert_eq!(r, "https://github.com/rust-lang/rustlings.git");
1103 assert_eq!(commit, "28d2bb04326d7036514245d73f10fb72b9ed108c");
1104 } else {
1105 panic!("Expected Git source, got {:?}", resolved.source);
1106 }
1107 }
1108
1109 #[test]
1110 fn nonexistent_branch() {
1111 let (resolver, _temp_dir) = test_resolver();
1112
1113 let spec = CrateSpec::Git {
1114 repo: "https://github.com/rust-lang/rustlings.git".to_string(),
1115 selector: GitSelector::Branch("nonexistent-branch-xyzzy-99999".to_string()),
1116 name: Some("rustlings".to_string()),
1117 version: None,
1118 };
1119
1120 let result = resolver.resolve(&spec);
1121
1122 assert_matches!(result.unwrap_err(), Error::Git { .. });
1123 }
1124
1125 #[test]
1126 fn nonexistent_tag() {
1127 let (resolver, _temp_dir) = test_resolver();
1128
1129 let spec = CrateSpec::Git {
1130 repo: "https://github.com/rust-lang/rustlings.git".to_string(),
1131 selector: GitSelector::Tag("999.999.999".to_string()),
1132 name: Some("rustlings".to_string()),
1133 version: None,
1134 };
1135
1136 let result = resolver.resolve(&spec);
1137
1138 assert_matches!(result.unwrap_err(), Error::Git { .. });
1139 }
1140
1141 #[test]
1142 fn invalid_url() {
1143 let (resolver, _temp_dir) = test_resolver();
1144
1145 let spec = CrateSpec::Git {
1146 repo: "https://[invalid-url".to_string(),
1147 selector: GitSelector::DefaultBranch,
1148 name: None,
1149 version: None,
1150 };
1151
1152 let result = resolver.resolve(&spec);
1153
1154 assert_matches!(result.unwrap_err(), Error::Git { .. });
1155 }
1156
1157 #[test]
1160 fn version_mismatch() {
1161 let (resolver, _temp_dir) = test_resolver();
1162 let version_req = VersionReq::parse(">=999.0.0").unwrap();
1165
1166 let spec = CrateSpec::Git {
1167 repo: "https://github.com/rust-lang/rustlings.git".to_string(),
1168 selector: GitSelector::DefaultBranch,
1169 name: Some("rustlings".to_string()),
1170 version: Some(version_req),
1171 };
1172
1173 let result = resolver.resolve(&spec);
1174
1175 assert_matches!(result.unwrap_err(), Error::VersionMismatch { .. });
1176 }
1177 }
1178
1179 mod forge {
1185 use super::*;
1186
1187 #[test]
1188 fn github() {
1189 let (resolver, _temp_dir) = test_resolver();
1190 let spec = CrateSpec::Forge {
1191 forge: Forge::GitHub {
1192 custom_url: None,
1193 owner: "rust-lang".to_string(),
1194 repo: "rustlings".to_string(),
1195 },
1196 selector: GitSelector::DefaultBranch,
1197 name: Some("rustlings".to_string()),
1198 version: None,
1199 };
1200
1201 let resolved = resolver.resolve(&spec).unwrap();
1202 assert_eq!(resolved.name, "rustlings");
1203 if let ResolvedSource::Forge { forge: f, commit } = &resolved.source {
1204 assert_matches!(f, Forge::GitHub { .. });
1205 assert!(!commit.is_empty());
1206 } else {
1207 panic!("Expected Forge source, got {:?}", resolved.source);
1208 }
1209 }
1210
1211 #[test]
1212 fn github_with_branch() {
1213 let (resolver, _temp_dir) = test_resolver();
1214
1215 let spec = CrateSpec::Forge {
1216 forge: Forge::GitHub {
1217 custom_url: None,
1218 owner: "rust-lang".to_string(),
1219 repo: "rustlings".to_string(),
1220 },
1221 selector: GitSelector::Branch("main".to_string()),
1222 name: Some("rustlings".to_string()),
1223 version: None,
1224 };
1225
1226 let resolved = resolver.resolve(&spec).unwrap();
1227 if let ResolvedSource::Forge { forge: f, commit, .. } = &resolved.source {
1228 assert_matches!(f, Forge::GitHub { .. });
1229 assert!(!commit.is_empty());
1230 } else {
1231 panic!("Expected Forge source, got {:?}", resolved.source);
1232 }
1233 }
1234
1235 #[test]
1236 fn github_with_tag() {
1237 let (resolver, _temp_dir) = test_resolver();
1238
1239 let spec = CrateSpec::Forge {
1240 forge: Forge::GitHub {
1241 custom_url: None,
1242 owner: "rust-lang".to_string(),
1243 repo: "rustlings".to_string(),
1244 },
1245 selector: GitSelector::Tag("v6.0.0".to_string()),
1246 name: Some("rustlings".to_string()),
1247 version: None,
1248 };
1249
1250 let resolved = resolver.resolve(&spec).unwrap();
1251 if let ResolvedSource::Forge { forge: f, commit, .. } = &resolved.source {
1252 assert_matches!(f, Forge::GitHub { .. });
1253 assert!(!commit.is_empty());
1254 } else {
1255 panic!("Expected Forge source, got {:?}", resolved.source);
1256 }
1257 }
1258 }
1259}