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