1use std::collections::BTreeMap;
6use std::collections::BTreeSet;
7use std::collections::HashMap;
8use std::collections::HashSet;
9use std::fs::File;
10use std::io::Seek;
11use std::io::SeekFrom;
12use std::time::Duration;
13
14use annotate_snippets::Level;
15use anyhow::Context as _;
16use anyhow::bail;
17use cargo_credential::Operation;
18use cargo_credential::Secret;
19use cargo_util::paths;
20use crates_io::NewCrate;
21use crates_io::NewCrateDependency;
22use crates_io::Registry;
23use itertools::Itertools;
24
25use crate::CargoResult;
26use crate::GlobalContext;
27use crate::core::Dependency;
28use crate::core::Package;
29use crate::core::PackageId;
30use crate::core::PackageIdSpecQuery;
31use crate::core::SourceId;
32use crate::core::Workspace;
33use crate::core::dependency::DepKind;
34use crate::core::manifest::ManifestMetadata;
35use crate::core::resolver::CliFeatures;
36use crate::ops;
37use crate::ops::PackageOpts;
38use crate::ops::Packages;
39use crate::ops::RegistryOrIndex;
40use crate::ops::registry::RegistrySourceIds;
41use crate::sources::CRATES_IO_REGISTRY;
42use crate::sources::RegistrySource;
43use crate::sources::SourceConfigMap;
44use crate::sources::source::QueryKind;
45use crate::sources::source::Source;
46use crate::util::Graph;
47use crate::util::Progress;
48use crate::util::ProgressStyle;
49use crate::util::VersionExt as _;
50use crate::util::auth;
51use crate::util::cache_lock::CacheLockMode;
52use crate::util::context::JobsConfig;
53use crate::util::errors::ManifestError;
54use crate::util::toml::prepare_for_publish;
55
56use super::super::check_dep_has_version;
57
58pub struct PublishOpts<'gctx> {
59 pub gctx: &'gctx GlobalContext,
60 pub token: Option<Secret<String>>,
61 pub reg_or_index: Option<RegistryOrIndex>,
62 pub verify: bool,
63 pub allow_dirty: bool,
64 pub jobs: Option<JobsConfig>,
65 pub keep_going: bool,
66 pub to_publish: ops::Packages,
67 pub targets: Vec<String>,
68 pub dry_run: bool,
69 pub cli_features: CliFeatures,
70}
71
72pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
73 let specs = opts.to_publish.to_package_id_specs(ws)?;
74
75 let member_ids: Vec<_> = ws.members().map(|p| p.package_id()).collect();
76 for spec in &specs {
78 spec.query(member_ids.clone())?;
79 }
80 let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
81 pkgs.retain(|(m, _)| specs.iter().any(|spec| spec.matches(m.package_id())));
84
85 let (unpublishable, pkgs): (Vec<_>, Vec<_>) = pkgs
86 .into_iter()
87 .partition(|(pkg, _)| pkg.publish() == &Some(vec![]));
88 let allow_unpublishable = match &opts.to_publish {
92 Packages::Default => ws.is_virtual(),
93 Packages::All(_) => true,
94 Packages::OptOut(_) => true,
95 Packages::Packages(_) => false,
96 };
97 if !unpublishable.is_empty() && !allow_unpublishable {
98 bail!(
99 "{} cannot be published.\n\
100 `package.publish` must be set to `true` or a non-empty list in Cargo.toml to publish.",
101 unpublishable
102 .iter()
103 .map(|(pkg, _)| format!("`{}`", pkg.name()))
104 .join(", "),
105 );
106 }
107
108 if pkgs.is_empty() {
109 if allow_unpublishable {
110 let n = unpublishable.len();
111 let plural = if n == 1 { "" } else { "s" };
112 ws.gctx().shell().print_report(
113 &[Level::WARNING
114 .secondary_title(format!(
115 "nothing to publish, but found {n} unpublishable package{plural}"
116 ))
117 .element(Level::HELP.message(
118 "to publish packages, set `package.publish` to `true` or a non-empty list",
119 ))],
120 false,
121 )?;
122 return Ok(());
123 } else {
124 unreachable!("must have at least one publishable package");
125 }
126 }
127
128 let just_pkgs: Vec<_> = pkgs.iter().map(|p| p.0).collect();
129 let reg_or_index = match opts.reg_or_index.clone() {
130 Some(r) => {
131 validate_registry(&just_pkgs, Some(&r))?;
132 Some(r)
133 }
134 None => {
135 let reg = super::infer_registry(&just_pkgs)?;
136 validate_registry(&just_pkgs, reg.as_ref())?;
137 if let Some(RegistryOrIndex::Registry(registry)) = ® {
138 if registry != CRATES_IO_REGISTRY {
139 opts.gctx.shell().note(&format!(
141 "found `{}` as only allowed registry. Publishing to it automatically.",
142 registry
143 ))?;
144 }
145 }
146 reg
147 }
148 };
149
150 let source_ids = super::get_source_id(opts.gctx, reg_or_index.as_ref())?;
153 let (mut registry, mut source) = super::registry(
154 opts.gctx,
155 &source_ids,
156 opts.token.as_ref().map(Secret::as_deref),
157 reg_or_index.as_ref(),
158 true,
159 Some(Operation::Read).filter(|_| !opts.dry_run),
160 )?;
161
162 {
163 let _lock = opts
164 .gctx
165 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
166
167 for (pkg, _) in &pkgs {
168 verify_unpublished(pkg, &mut source, &source_ids, opts.dry_run, opts.gctx)?;
169 verify_dependencies(pkg, ®istry, source_ids.original).map_err(|err| {
170 ManifestError::new(
171 err.context(format!(
172 "failed to verify manifest at `{}`",
173 pkg.manifest_path().display()
174 )),
175 pkg.manifest_path().into(),
176 )
177 })?;
178 }
179 }
180
181 let pkg_dep_graph = ops::cargo_package::package_with_dep_graph(
182 ws,
183 &PackageOpts {
184 gctx: opts.gctx,
185 verify: opts.verify,
186 list: false,
187 fmt: ops::PackageMessageFormat::Human,
188 check_metadata: true,
189 allow_dirty: opts.allow_dirty,
190 include_lockfile: true,
191 to_package: ops::Packages::Default,
194 targets: opts.targets.clone(),
195 jobs: opts.jobs.clone(),
196 keep_going: opts.keep_going,
197 cli_features: opts.cli_features.clone(),
198 reg_or_index: reg_or_index.clone(),
199 dry_run: opts.dry_run,
200 },
201 pkgs,
202 )?;
203
204 let mut plan = PublishPlan::new(&pkg_dep_graph.graph);
205 let mut to_confirm = BTreeSet::new();
211
212 while !plan.is_empty() {
213 let mut ready = plan.take_ready();
219 while let Some(pkg_id) = ready.pop_first() {
220 let (pkg, (_features, tarball)) = &pkg_dep_graph.packages[&pkg_id];
221 opts.gctx.shell().status("Uploading", pkg.package_id())?;
222
223 if !opts.dry_run {
224 let ver = pkg.version().to_string();
225
226 tarball.file().seek(SeekFrom::Start(0))?;
227 let hash = cargo_util::Sha256::new()
228 .update_file(tarball.file())?
229 .finish_hex();
230 let operation = Operation::Publish {
231 name: pkg.name().as_str(),
232 vers: &ver,
233 cksum: &hash,
234 };
235 registry.set_token(Some(auth::auth_token(
236 &opts.gctx,
237 &source_ids.original,
238 None,
239 operation,
240 vec![],
241 false,
242 )?));
243 }
244
245 let workspace_context = || {
246 let mut remaining = ready.clone();
247 remaining.extend(plan.iter());
248 if !remaining.is_empty() {
249 format!(
250 "\n\nnote: the following crates have not been published yet:\n {}",
251 remaining.into_iter().join("\n ")
252 )
253 } else {
254 String::new()
255 }
256 };
257
258 transmit(
259 opts.gctx,
260 ws,
261 pkg,
262 tarball.file(),
263 &mut registry,
264 source_ids.original,
265 opts.dry_run,
266 workspace_context,
267 )?;
268 to_confirm.insert(pkg_id);
269
270 if !opts.dry_run {
271 let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version());
273 let source_description = source_ids.original.to_string();
274 ws.gctx().shell().status(
275 "Uploaded",
276 format!("{short_pkg_description} to {source_description}"),
277 )?;
278 }
279 }
280
281 let confirmed = if opts.dry_run {
282 to_confirm.clone()
283 } else {
284 const DEFAULT_TIMEOUT: u64 = 60;
285 let timeout = if opts.gctx.cli_unstable().publish_timeout {
286 let timeout: Option<u64> = opts.gctx.get("publish.timeout")?;
287 timeout.unwrap_or(DEFAULT_TIMEOUT)
288 } else {
289 DEFAULT_TIMEOUT
290 };
291 if 0 < timeout {
292 let source_description = source.source_id().to_string();
293 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
294 if plan.is_empty() {
295 let report = &[
296 annotate_snippets::Group::with_title(
297 annotate_snippets::Level::NOTE
298 .secondary_title(format!(
299 "waiting for {short_pkg_descriptions} to be available at {source_description}"
300 ))),
301 annotate_snippets::Group::with_title(annotate_snippets::Level::HELP.secondary_title(format!(
302 "you may press ctrl-c to skip waiting; the {crate} should be available shortly",
303 crate = if to_confirm.len() == 1 { "crate" } else {"crates"}
304 ))),
305 ];
306 opts.gctx.shell().print_report(report, false)?;
307 } else {
308 opts.gctx.shell().note(format!(
309 "waiting for {short_pkg_descriptions} to be available at {source_description}.\n\
310 {count} remaining {crate} to be published",
311 count = plan.len(),
312 crate = if plan.len() == 1 { "crate" } else {"crates"}
313 ))?;
314 }
315
316 let timeout = Duration::from_secs(timeout);
317 let confirmed = wait_for_any_publish_confirmation(
318 opts.gctx,
319 source_ids.original,
320 &to_confirm,
321 timeout,
322 )?;
323 if !confirmed.is_empty() {
324 let short_pkg_description = package_list(confirmed.iter().copied(), "and");
325 opts.gctx.shell().status(
326 "Published",
327 format!("{short_pkg_description} at {source_description}"),
328 )?;
329 } else {
330 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
331 let krate = if to_confirm.len() == 1 {
332 "crate"
333 } else {
334 "crates"
335 };
336 opts.gctx.shell().print_report(
337 &[Level::WARNING
338 .secondary_title(format!(
339 "timed out waiting for {short_pkg_descriptions} \
340 to be available in {source_description}",
341 ))
342 .element(Level::NOTE.message(format!(
343 "the registry may have a backlog that is delaying making the \
344 {krate} available. The {krate} should be available soon.",
345 )))],
346 false,
347 )?;
348 }
349 confirmed
350 } else {
351 BTreeSet::new()
352 }
353 };
354 if confirmed.is_empty() {
355 if plan.is_empty() {
358 break;
361 } else {
362 let failed_list = package_list(plan.iter(), "and");
363 bail!(
364 "unable to publish {failed_list} due to a timeout while waiting for published dependencies to be available."
365 );
366 }
367 }
368 for id in &confirmed {
369 to_confirm.remove(id);
370 }
371 plan.mark_confirmed(confirmed);
372 }
373
374 Ok(())
375}
376
377fn wait_for_any_publish_confirmation(
382 gctx: &GlobalContext,
383 registry_src: SourceId,
384 pkgs: &BTreeSet<PackageId>,
385 timeout: Duration,
386) -> CargoResult<BTreeSet<PackageId>> {
387 let mut source = SourceConfigMap::empty(gctx)?.load(registry_src, &HashSet::new())?;
388 source.set_quiet(true);
392
393 let now = std::time::Instant::now();
394 let sleep_time = Duration::from_secs(1);
395 let max = timeout.as_secs() as usize;
396 let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, gctx);
397 progress.tick_now(0, max, "")?;
398 let available = loop {
399 {
400 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
401 gctx.updated_sources().remove(&source.replaced_source_id());
407 source.invalidate_cache();
408 let mut available = BTreeSet::new();
409 for pkg in pkgs {
410 if poll_one_package(registry_src, pkg, &mut source)? {
411 available.insert(*pkg);
412 }
413 }
414
415 if !available.is_empty() {
418 break available;
419 }
420 }
421
422 let elapsed = now.elapsed();
423 if timeout < elapsed {
424 break BTreeSet::new();
425 }
426
427 progress.tick_now(elapsed.as_secs() as usize, max, "")?;
428 std::thread::sleep(sleep_time);
429 };
430
431 Ok(available)
432}
433
434fn poll_one_package(
435 registry_src: SourceId,
436 pkg_id: &PackageId,
437 source: &mut dyn Source,
438) -> CargoResult<bool> {
439 let version_req = format!("={}", pkg_id.version());
440 let query = Dependency::parse(pkg_id.name(), Some(&version_req), registry_src)?;
441 let summaries = loop {
442 match source.query_vec(&query, QueryKind::Exact) {
444 std::task::Poll::Ready(res) => {
445 break res?;
446 }
447 std::task::Poll::Pending => source.block_until_ready()?,
448 }
449 };
450 Ok(!summaries.is_empty())
451}
452
453fn verify_unpublished(
454 pkg: &Package,
455 source: &mut RegistrySource<'_>,
456 source_ids: &RegistrySourceIds,
457 dry_run: bool,
458 gctx: &GlobalContext,
459) -> CargoResult<()> {
460 let query = Dependency::parse(
461 pkg.name(),
462 Some(&pkg.version().to_exact_req().to_string()),
463 source_ids.replacement,
464 )?;
465 let duplicate_query = loop {
466 match source.query_vec(&query, QueryKind::Exact) {
467 std::task::Poll::Ready(res) => {
468 break res?;
469 }
470 std::task::Poll::Pending => source.block_until_ready()?,
471 }
472 };
473 if !duplicate_query.is_empty() {
474 if dry_run {
478 gctx.shell().warn(format!(
479 "crate {}@{} already exists on {}",
480 pkg.name(),
481 pkg.version(),
482 source.describe()
483 ))?;
484 } else {
485 bail!(
486 "crate {}@{} already exists on {}",
487 pkg.name(),
488 pkg.version(),
489 source.describe()
490 );
491 }
492 }
493
494 Ok(())
495}
496
497fn verify_dependencies(
498 pkg: &Package,
499 registry: &Registry,
500 registry_src: SourceId,
501) -> CargoResult<()> {
502 for dep in pkg.dependencies().iter() {
503 if check_dep_has_version(dep, true)? {
504 continue;
505 }
506 if dep.source_id() != registry_src {
509 if !dep.source_id().is_registry() {
510 panic!("unexpected source kind for dependency {:?}", dep);
514 }
515 if registry_src.is_crates_io() || registry.host_is_crates_io() {
520 bail!(
521 "crates cannot be published to crates.io with dependencies sourced from other\n\
522 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
523 (crate `{}` is pulled from {})",
524 dep.package_name(),
525 dep.package_name(),
526 dep.source_id()
527 );
528 }
529 }
530 }
531 Ok(())
532}
533
534pub(crate) fn prepare_transmit(
535 gctx: &GlobalContext,
536 ws: &Workspace<'_>,
537 local_pkg: &Package,
538 registry_id: SourceId,
539) -> CargoResult<NewCrate> {
540 let included = None; let publish_pkg = prepare_for_publish(local_pkg, ws, included)?;
542
543 let deps = publish_pkg
544 .dependencies()
545 .iter()
546 .map(|dep| {
547 let dep_registry_id = match dep.registry_id() {
550 Some(id) => id,
551 None => SourceId::crates_io(gctx)?,
552 };
553 let dep_registry = if dep_registry_id != registry_id {
556 Some(dep_registry_id.url().to_string())
557 } else {
558 None
559 };
560
561 Ok(NewCrateDependency {
562 optional: dep.is_optional(),
563 default_features: dep.uses_default_features(),
564 name: dep.package_name().to_string(),
565 features: dep.features().iter().map(|s| s.to_string()).collect(),
566 version_req: dep.version_req().to_string(),
567 target: dep.platform().map(|s| s.to_string()),
568 kind: match dep.kind() {
569 DepKind::Normal => "normal",
570 DepKind::Build => "build",
571 DepKind::Development => "dev",
572 }
573 .to_string(),
574 registry: dep_registry,
575 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
576 artifact: dep.artifact().map(|artifact| {
577 artifact
578 .kinds()
579 .iter()
580 .map(|x| x.as_str().into_owned())
581 .collect()
582 }),
583 bindep_target: dep.artifact().and_then(|artifact| {
584 artifact.target().map(|target| target.as_str().to_owned())
585 }),
586 lib: dep.artifact().map_or(false, |artifact| artifact.is_lib()),
587 })
588 })
589 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
590 let manifest = publish_pkg.manifest();
591 let ManifestMetadata {
592 ref authors,
593 ref description,
594 ref homepage,
595 ref documentation,
596 ref keywords,
597 ref readme,
598 ref repository,
599 ref license,
600 ref license_file,
601 ref categories,
602 ref badges,
603 ref links,
604 ref rust_version,
605 } = *manifest.metadata();
606 let rust_version = rust_version.as_ref().map(ToString::to_string);
607 let readme_content = local_pkg
608 .manifest()
609 .metadata()
610 .readme
611 .as_ref()
612 .map(|readme| {
613 paths::read(&local_pkg.root().join(readme)).with_context(|| {
614 format!("failed to read `readme` file for package `{}`", local_pkg)
615 })
616 })
617 .transpose()?;
618 if let Some(ref file) = local_pkg.manifest().metadata().license_file {
619 if !local_pkg.root().join(file).exists() {
620 bail!("the license file `{}` does not exist", file)
621 }
622 }
623
624 let string_features = match manifest.normalized_toml().features() {
625 Some(features) => features
626 .iter()
627 .map(|(feat, values)| {
628 (
629 feat.to_string(),
630 values.iter().map(|fv| fv.to_string()).collect(),
631 )
632 })
633 .collect::<BTreeMap<String, Vec<String>>>(),
634 None => BTreeMap::new(),
635 };
636
637 Ok(NewCrate {
638 name: publish_pkg.name().to_string(),
639 vers: publish_pkg.version().to_string(),
640 deps,
641 features: string_features,
642 authors: authors.clone(),
643 description: description.clone(),
644 homepage: homepage.clone(),
645 documentation: documentation.clone(),
646 keywords: keywords.clone(),
647 categories: categories.clone(),
648 readme: readme_content,
649 readme_file: readme.clone(),
650 repository: repository.clone(),
651 license: license.clone(),
652 license_file: license_file.clone(),
653 badges: badges.clone(),
654 links: links.clone(),
655 rust_version,
656 })
657}
658
659fn transmit(
660 gctx: &GlobalContext,
661 ws: &Workspace<'_>,
662 pkg: &Package,
663 tarball: &File,
664 registry: &mut Registry,
665 registry_id: SourceId,
666 dry_run: bool,
667 workspace_context: impl Fn() -> String,
668) -> CargoResult<()> {
669 let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
670
671 if dry_run {
673 gctx.shell().warn("aborting upload due to dry run")?;
674 return Ok(());
675 }
676
677 let warnings = registry.publish(&new_crate, tarball).with_context(|| {
678 format!(
679 "failed to publish {} v{} to registry at {}{}",
680 pkg.name(),
681 pkg.version(),
682 registry.host(),
683 workspace_context()
684 )
685 })?;
686
687 if !warnings.invalid_categories.is_empty() {
688 let msg = format!(
689 "the following are not valid category slugs and were ignored: {}",
690 warnings.invalid_categories.join(", ")
691 );
692 gctx.shell().print_report(
693 &[Level::WARNING
694 .secondary_title(msg)
695 .element(Level::HELP.message(
696 "please see <https://crates.io/category_slugs> for the list of all category slugs",
697 ))],
698 false,
699 )?;
700 }
701
702 if !warnings.invalid_badges.is_empty() {
703 let msg = format!(
704 "the following are not valid badges and were ignored: {}",
705 warnings.invalid_badges.join(", ")
706 );
707 gctx.shell().print_report(
708 &[Level::WARNING.secondary_title(msg).elements([
709 Level::NOTE.message(
710 "either the badge type specified is unknown or a required \
711 attribute is missing",
712 ),
713 Level::HELP.message(
714 "please see \
715 <https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata> \
716 for valid badge types and their required attributes",
717 ),
718 ])],
719 false,
720 )?;
721 }
722
723 if !warnings.other.is_empty() {
724 for msg in warnings.other {
725 gctx.shell().warn(&msg)?;
726 }
727 }
728
729 Ok(())
730}
731
732struct PublishPlan {
734 dependents: Graph<PackageId, ()>,
736 dependencies_count: HashMap<PackageId, usize>,
738}
739
740impl PublishPlan {
741 fn new(graph: &Graph<PackageId, ()>) -> Self {
743 let dependents = graph.reversed();
744
745 let dependencies_count: HashMap<_, _> = dependents
746 .iter()
747 .map(|id| (*id, graph.edges(id).count()))
748 .collect();
749 Self {
750 dependents,
751 dependencies_count,
752 }
753 }
754
755 fn iter(&self) -> impl Iterator<Item = PackageId> + '_ {
756 self.dependencies_count.iter().map(|(id, _)| *id)
757 }
758
759 fn is_empty(&self) -> bool {
760 self.dependencies_count.is_empty()
761 }
762
763 fn len(&self) -> usize {
764 self.dependencies_count.len()
765 }
766
767 fn take_ready(&mut self) -> BTreeSet<PackageId> {
771 let ready: BTreeSet<_> = self
772 .dependencies_count
773 .iter()
774 .filter_map(|(id, weight)| (*weight == 0).then_some(*id))
775 .collect();
776 for pkg in &ready {
777 self.dependencies_count.remove(pkg);
778 }
779 ready
780 }
781
782 fn mark_confirmed(&mut self, published: impl IntoIterator<Item = PackageId>) {
785 for id in published {
786 for (dependent_id, _) in self.dependents.edges(&id) {
787 if let Some(weight) = self.dependencies_count.get_mut(dependent_id) {
788 *weight = weight.saturating_sub(1);
789 }
790 }
791 }
792 }
793}
794
795fn package_list(pkgs: impl IntoIterator<Item = PackageId>, final_sep: &str) -> String {
801 let mut names: Vec<_> = pkgs
802 .into_iter()
803 .map(|pkg| format!("{} v{}", pkg.name(), pkg.version()))
804 .collect();
805 names.sort();
806
807 match &names[..] {
808 [] => String::new(),
809 [a] => a.clone(),
810 [a, b] => format!("{a} {final_sep} {b}"),
811 [names @ .., last] => {
812 format!("{}, {final_sep} {last}", names.join(", "))
813 }
814 }
815}
816
817fn validate_registry(pkgs: &[&Package], reg_or_index: Option<&RegistryOrIndex>) -> CargoResult<()> {
818 let reg_name = match reg_or_index {
819 Some(RegistryOrIndex::Registry(r)) => Some(r.as_str()),
820 None => Some(CRATES_IO_REGISTRY),
821 Some(RegistryOrIndex::Index(_)) => None,
822 };
823 if let Some(reg_name) = reg_name {
824 for pkg in pkgs {
825 if let Some(allowed) = pkg.publish().as_ref() {
826 if !allowed.iter().any(|a| a == reg_name) {
827 bail!(
828 "`{}` cannot be published.\n\
829 The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
830 pkg.name(),
831 reg_name
832 );
833 }
834 }
835 }
836 }
837
838 Ok(())
839}
840
841#[cfg(test)]
842mod tests {
843 use crate::{
844 core::{PackageId, SourceId},
845 sources::CRATES_IO_INDEX,
846 util::{Graph, IntoUrl},
847 };
848
849 use super::PublishPlan;
850
851 fn pkg_id(name: &str) -> PackageId {
852 let loc = CRATES_IO_INDEX.into_url().unwrap();
853 PackageId::try_new(name, "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap()
854 }
855
856 #[test]
857 fn parallel_schedule() {
858 let mut graph: Graph<PackageId, ()> = Graph::new();
859 let a = pkg_id("a");
860 let b = pkg_id("b");
861 let c = pkg_id("c");
862 let d = pkg_id("d");
863 let e = pkg_id("e");
864
865 graph.add(a);
866 graph.add(b);
867 graph.add(c);
868 graph.add(d);
869 graph.add(e);
870 graph.link(a, c);
871 graph.link(b, c);
872 graph.link(c, d);
873 graph.link(c, e);
874
875 let mut order = PublishPlan::new(&graph);
876 let ready: Vec<_> = order.take_ready().into_iter().collect();
877 assert_eq!(ready, vec![d, e]);
878
879 order.mark_confirmed(vec![d]);
880 let ready: Vec<_> = order.take_ready().into_iter().collect();
881 assert!(ready.is_empty());
882
883 order.mark_confirmed(vec![e]);
884 let ready: Vec<_> = order.take_ready().into_iter().collect();
885 assert_eq!(ready, vec![c]);
886
887 order.mark_confirmed(vec![c]);
888 let ready: Vec<_> = order.take_ready().into_iter().collect();
889 assert_eq!(ready, vec![a, b]);
890
891 order.mark_confirmed(vec![a, b]);
892 let ready: Vec<_> = order.take_ready().into_iter().collect();
893 assert!(ready.is_empty());
894 }
895}