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