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 for pkg_id in plan.take_ready() {
214 let (pkg, (_features, tarball)) = &pkg_dep_graph.packages[&pkg_id];
215 opts.gctx.shell().status("Uploading", pkg.package_id())?;
216
217 if !opts.dry_run {
218 let ver = pkg.version().to_string();
219
220 tarball.file().seek(SeekFrom::Start(0))?;
221 let hash = cargo_util::Sha256::new()
222 .update_file(tarball.file())?
223 .finish_hex();
224 let operation = Operation::Publish {
225 name: pkg.name().as_str(),
226 vers: &ver,
227 cksum: &hash,
228 };
229 registry.set_token(Some(auth::auth_token(
230 &opts.gctx,
231 &source_ids.original,
232 None,
233 operation,
234 vec![],
235 false,
236 )?));
237 }
238
239 transmit(
240 opts.gctx,
241 ws,
242 pkg,
243 tarball.file(),
244 &mut registry,
245 source_ids.original,
246 opts.dry_run,
247 )?;
248 to_confirm.insert(pkg_id);
249
250 if !opts.dry_run {
251 let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version());
253 let source_description = source_ids.original.to_string();
254 ws.gctx().shell().status(
255 "Uploaded",
256 format!("{short_pkg_description} to {source_description}"),
257 )?;
258 }
259 }
260
261 let confirmed = if opts.dry_run {
262 to_confirm.clone()
263 } else {
264 const DEFAULT_TIMEOUT: u64 = 60;
265 let timeout = if opts.gctx.cli_unstable().publish_timeout {
266 let timeout: Option<u64> = opts.gctx.get("publish.timeout")?;
267 timeout.unwrap_or(DEFAULT_TIMEOUT)
268 } else {
269 DEFAULT_TIMEOUT
270 };
271 if 0 < timeout {
272 let source_description = source.source_id().to_string();
273 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
274 if plan.is_empty() {
275 opts.gctx.shell().note(format!(
276 "waiting for {short_pkg_descriptions} to be available at {source_description}.\n\
277 You may press ctrl-c to skip waiting; the {crate} should be available shortly.",
278 crate = if to_confirm.len() == 1 { "crate" } else {"crates"}
279 ))?;
280 } else {
281 opts.gctx.shell().note(format!(
282 "waiting for {short_pkg_descriptions} to be available at {source_description}.\n\
283 {count} remaining {crate} to be published",
284 count = plan.len(),
285 crate = if plan.len() == 1 { "crate" } else {"crates"}
286 ))?;
287 }
288
289 let timeout = Duration::from_secs(timeout);
290 let confirmed = wait_for_any_publish_confirmation(
291 opts.gctx,
292 source_ids.original,
293 &to_confirm,
294 timeout,
295 )?;
296 if !confirmed.is_empty() {
297 let short_pkg_description = package_list(confirmed.iter().copied(), "and");
298 opts.gctx.shell().status(
299 "Published",
300 format!("{short_pkg_description} at {source_description}"),
301 )?;
302 } else {
303 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
304 opts.gctx.shell().warn(format!(
305 "timed out waiting for {short_pkg_descriptions} to be available in {source_description}",
306 ))?;
307 opts.gctx.shell().note(format!(
308 "the registry may have a backlog that is delaying making the \
309 {crate} available. The {crate} should be available soon.",
310 crate = if to_confirm.len() == 1 {
311 "crate"
312 } else {
313 "crates"
314 }
315 ))?;
316 }
317 confirmed
318 } else {
319 BTreeSet::new()
320 }
321 };
322 if confirmed.is_empty() {
323 if plan.is_empty() {
326 break;
329 } else {
330 let failed_list = package_list(plan.iter(), "and");
331 bail!(
332 "unable to publish {failed_list} due to a timeout while waiting for published dependencies to be available."
333 );
334 }
335 }
336 for id in &confirmed {
337 to_confirm.remove(id);
338 }
339 plan.mark_confirmed(confirmed);
340 }
341
342 Ok(())
343}
344
345fn wait_for_any_publish_confirmation(
350 gctx: &GlobalContext,
351 registry_src: SourceId,
352 pkgs: &BTreeSet<PackageId>,
353 timeout: Duration,
354) -> CargoResult<BTreeSet<PackageId>> {
355 let mut source = SourceConfigMap::empty(gctx)?.load(registry_src, &HashSet::new())?;
356 source.set_quiet(true);
360
361 let now = std::time::Instant::now();
362 let sleep_time = Duration::from_secs(1);
363 let max = timeout.as_secs() as usize;
364 let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, gctx);
365 progress.tick_now(0, max, "")?;
366 let available = loop {
367 {
368 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
369 gctx.updated_sources().remove(&source.replaced_source_id());
375 source.invalidate_cache();
376 let mut available = BTreeSet::new();
377 for pkg in pkgs {
378 if poll_one_package(registry_src, pkg, &mut source)? {
379 available.insert(*pkg);
380 }
381 }
382
383 if !available.is_empty() {
386 break available;
387 }
388 }
389
390 let elapsed = now.elapsed();
391 if timeout < elapsed {
392 break BTreeSet::new();
393 }
394
395 progress.tick_now(elapsed.as_secs() as usize, max, "")?;
396 std::thread::sleep(sleep_time);
397 };
398
399 Ok(available)
400}
401
402fn poll_one_package(
403 registry_src: SourceId,
404 pkg_id: &PackageId,
405 source: &mut dyn Source,
406) -> CargoResult<bool> {
407 let version_req = format!("={}", pkg_id.version());
408 let query = Dependency::parse(pkg_id.name(), Some(&version_req), registry_src)?;
409 let summaries = loop {
410 match source.query_vec(&query, QueryKind::Exact) {
412 std::task::Poll::Ready(res) => {
413 break res?;
414 }
415 std::task::Poll::Pending => source.block_until_ready()?,
416 }
417 };
418 Ok(!summaries.is_empty())
419}
420
421fn verify_unpublished(
422 pkg: &Package,
423 source: &mut RegistrySource<'_>,
424 source_ids: &RegistrySourceIds,
425 dry_run: bool,
426 gctx: &GlobalContext,
427) -> CargoResult<()> {
428 let query = Dependency::parse(
429 pkg.name(),
430 Some(&pkg.version().to_exact_req().to_string()),
431 source_ids.replacement,
432 )?;
433 let duplicate_query = loop {
434 match source.query_vec(&query, QueryKind::Exact) {
435 std::task::Poll::Ready(res) => {
436 break res?;
437 }
438 std::task::Poll::Pending => source.block_until_ready()?,
439 }
440 };
441 if !duplicate_query.is_empty() {
442 if dry_run {
446 gctx.shell().warn(format!(
447 "crate {}@{} already exists on {}",
448 pkg.name(),
449 pkg.version(),
450 source.describe()
451 ))?;
452 } else {
453 bail!(
454 "crate {}@{} already exists on {}",
455 pkg.name(),
456 pkg.version(),
457 source.describe()
458 );
459 }
460 }
461
462 Ok(())
463}
464
465fn verify_dependencies(
466 pkg: &Package,
467 registry: &Registry,
468 registry_src: SourceId,
469) -> CargoResult<()> {
470 for dep in pkg.dependencies().iter() {
471 if check_dep_has_version(dep, true)? {
472 continue;
473 }
474 if dep.source_id() != registry_src {
477 if !dep.source_id().is_registry() {
478 panic!("unexpected source kind for dependency {:?}", dep);
482 }
483 if registry_src.is_crates_io() || registry.host_is_crates_io() {
488 bail!(
489 "crates cannot be published to crates.io with dependencies sourced from other\n\
490 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
491 (crate `{}` is pulled from {})",
492 dep.package_name(),
493 dep.package_name(),
494 dep.source_id()
495 );
496 }
497 }
498 }
499 Ok(())
500}
501
502pub(crate) fn prepare_transmit(
503 gctx: &GlobalContext,
504 ws: &Workspace<'_>,
505 local_pkg: &Package,
506 registry_id: SourceId,
507) -> CargoResult<NewCrate> {
508 let included = None; let publish_pkg = prepare_for_publish(local_pkg, ws, included)?;
510
511 let deps = publish_pkg
512 .dependencies()
513 .iter()
514 .map(|dep| {
515 let dep_registry_id = match dep.registry_id() {
518 Some(id) => id,
519 None => SourceId::crates_io(gctx)?,
520 };
521 let dep_registry = if dep_registry_id != registry_id {
524 Some(dep_registry_id.url().to_string())
525 } else {
526 None
527 };
528
529 Ok(NewCrateDependency {
530 optional: dep.is_optional(),
531 default_features: dep.uses_default_features(),
532 name: dep.package_name().to_string(),
533 features: dep.features().iter().map(|s| s.to_string()).collect(),
534 version_req: dep.version_req().to_string(),
535 target: dep.platform().map(|s| s.to_string()),
536 kind: match dep.kind() {
537 DepKind::Normal => "normal",
538 DepKind::Build => "build",
539 DepKind::Development => "dev",
540 }
541 .to_string(),
542 registry: dep_registry,
543 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
544 artifact: dep.artifact().map(|artifact| {
545 artifact
546 .kinds()
547 .iter()
548 .map(|x| x.as_str().into_owned())
549 .collect()
550 }),
551 bindep_target: dep.artifact().and_then(|artifact| {
552 artifact.target().map(|target| target.as_str().to_owned())
553 }),
554 lib: dep.artifact().map_or(false, |artifact| artifact.is_lib()),
555 })
556 })
557 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
558 let manifest = publish_pkg.manifest();
559 let ManifestMetadata {
560 ref authors,
561 ref description,
562 ref homepage,
563 ref documentation,
564 ref keywords,
565 ref readme,
566 ref repository,
567 ref license,
568 ref license_file,
569 ref categories,
570 ref badges,
571 ref links,
572 ref rust_version,
573 } = *manifest.metadata();
574 let rust_version = rust_version.as_ref().map(ToString::to_string);
575 let readme_content = local_pkg
576 .manifest()
577 .metadata()
578 .readme
579 .as_ref()
580 .map(|readme| {
581 paths::read(&local_pkg.root().join(readme)).with_context(|| {
582 format!("failed to read `readme` file for package `{}`", local_pkg)
583 })
584 })
585 .transpose()?;
586 if let Some(ref file) = local_pkg.manifest().metadata().license_file {
587 if !local_pkg.root().join(file).exists() {
588 bail!("the license file `{}` does not exist", file)
589 }
590 }
591
592 let string_features = match manifest.normalized_toml().features() {
593 Some(features) => features
594 .iter()
595 .map(|(feat, values)| {
596 (
597 feat.to_string(),
598 values.iter().map(|fv| fv.to_string()).collect(),
599 )
600 })
601 .collect::<BTreeMap<String, Vec<String>>>(),
602 None => BTreeMap::new(),
603 };
604
605 Ok(NewCrate {
606 name: publish_pkg.name().to_string(),
607 vers: publish_pkg.version().to_string(),
608 deps,
609 features: string_features,
610 authors: authors.clone(),
611 description: description.clone(),
612 homepage: homepage.clone(),
613 documentation: documentation.clone(),
614 keywords: keywords.clone(),
615 categories: categories.clone(),
616 readme: readme_content,
617 readme_file: readme.clone(),
618 repository: repository.clone(),
619 license: license.clone(),
620 license_file: license_file.clone(),
621 badges: badges.clone(),
622 links: links.clone(),
623 rust_version,
624 })
625}
626
627fn transmit(
628 gctx: &GlobalContext,
629 ws: &Workspace<'_>,
630 pkg: &Package,
631 tarball: &File,
632 registry: &mut Registry,
633 registry_id: SourceId,
634 dry_run: bool,
635) -> CargoResult<()> {
636 let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
637
638 if dry_run {
640 gctx.shell().warn("aborting upload due to dry run")?;
641 return Ok(());
642 }
643
644 let warnings = registry
645 .publish(&new_crate, tarball)
646 .with_context(|| format!("failed to publish to registry at {}", registry.host()))?;
647
648 if !warnings.invalid_categories.is_empty() {
649 let msg = format!(
650 "the following are not valid category slugs and were \
651 ignored: {}. Please see https://crates.io/category_slugs \
652 for the list of all category slugs. \
653 ",
654 warnings.invalid_categories.join(", ")
655 );
656 gctx.shell().warn(&msg)?;
657 }
658
659 if !warnings.invalid_badges.is_empty() {
660 let msg = format!(
661 "the following are not valid badges and were ignored: {}. \
662 Either the badge type specified is unknown or a required \
663 attribute is missing. Please see \
664 https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
665 for valid badge types and their required attributes.",
666 warnings.invalid_badges.join(", ")
667 );
668 gctx.shell().warn(&msg)?;
669 }
670
671 if !warnings.other.is_empty() {
672 for msg in warnings.other {
673 gctx.shell().warn(&msg)?;
674 }
675 }
676
677 Ok(())
678}
679
680struct PublishPlan {
682 dependents: Graph<PackageId, ()>,
684 dependencies_count: HashMap<PackageId, usize>,
686}
687
688impl PublishPlan {
689 fn new(graph: &Graph<PackageId, ()>) -> Self {
691 let dependents = graph.reversed();
692
693 let dependencies_count: HashMap<_, _> = dependents
694 .iter()
695 .map(|id| (*id, graph.edges(id).count()))
696 .collect();
697 Self {
698 dependents,
699 dependencies_count,
700 }
701 }
702
703 fn iter(&self) -> impl Iterator<Item = PackageId> + '_ {
704 self.dependencies_count.iter().map(|(id, _)| *id)
705 }
706
707 fn is_empty(&self) -> bool {
708 self.dependencies_count.is_empty()
709 }
710
711 fn len(&self) -> usize {
712 self.dependencies_count.len()
713 }
714
715 fn take_ready(&mut self) -> BTreeSet<PackageId> {
719 let ready: BTreeSet<_> = self
720 .dependencies_count
721 .iter()
722 .filter_map(|(id, weight)| (*weight == 0).then_some(*id))
723 .collect();
724 for pkg in &ready {
725 self.dependencies_count.remove(pkg);
726 }
727 ready
728 }
729
730 fn mark_confirmed(&mut self, published: impl IntoIterator<Item = PackageId>) {
733 for id in published {
734 for (dependent_id, _) in self.dependents.edges(&id) {
735 if let Some(weight) = self.dependencies_count.get_mut(dependent_id) {
736 *weight = weight.saturating_sub(1);
737 }
738 }
739 }
740 }
741}
742
743fn package_list(pkgs: impl IntoIterator<Item = PackageId>, final_sep: &str) -> String {
749 let mut names: Vec<_> = pkgs
750 .into_iter()
751 .map(|pkg| format!("{} v{}", pkg.name(), pkg.version()))
752 .collect();
753 names.sort();
754
755 match &names[..] {
756 [] => String::new(),
757 [a] => a.clone(),
758 [a, b] => format!("{a} {final_sep} {b}"),
759 [names @ .., last] => {
760 format!("{}, {final_sep} {last}", names.join(", "))
761 }
762 }
763}
764
765fn validate_registry(pkgs: &[&Package], reg_or_index: Option<&RegistryOrIndex>) -> CargoResult<()> {
766 let reg_name = match reg_or_index {
767 Some(RegistryOrIndex::Registry(r)) => Some(r.as_str()),
768 None => Some(CRATES_IO_REGISTRY),
769 Some(RegistryOrIndex::Index(_)) => None,
770 };
771 if let Some(reg_name) = reg_name {
772 for pkg in pkgs {
773 if let Some(allowed) = pkg.publish().as_ref() {
774 if !allowed.iter().any(|a| a == reg_name) {
775 bail!(
776 "`{}` cannot be published.\n\
777 The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
778 pkg.name(),
779 reg_name
780 );
781 }
782 }
783 }
784 }
785
786 Ok(())
787}
788
789#[cfg(test)]
790mod tests {
791 use crate::{
792 core::{PackageId, SourceId},
793 sources::CRATES_IO_INDEX,
794 util::{Graph, IntoUrl},
795 };
796
797 use super::PublishPlan;
798
799 fn pkg_id(name: &str) -> PackageId {
800 let loc = CRATES_IO_INDEX.into_url().unwrap();
801 PackageId::try_new(name, "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap()
802 }
803
804 #[test]
805 fn parallel_schedule() {
806 let mut graph: Graph<PackageId, ()> = Graph::new();
807 let a = pkg_id("a");
808 let b = pkg_id("b");
809 let c = pkg_id("c");
810 let d = pkg_id("d");
811 let e = pkg_id("e");
812
813 graph.add(a);
814 graph.add(b);
815 graph.add(c);
816 graph.add(d);
817 graph.add(e);
818 graph.link(a, c);
819 graph.link(b, c);
820 graph.link(c, d);
821 graph.link(c, e);
822
823 let mut order = PublishPlan::new(&graph);
824 let ready: Vec<_> = order.take_ready().into_iter().collect();
825 assert_eq!(ready, vec![d, e]);
826
827 order.mark_confirmed(vec![d]);
828 let ready: Vec<_> = order.take_ready().into_iter().collect();
829 assert!(ready.is_empty());
830
831 order.mark_confirmed(vec![e]);
832 let ready: Vec<_> = order.take_ready().into_iter().collect();
833 assert_eq!(ready, vec![c]);
834
835 order.mark_confirmed(vec![c]);
836 let ready: Vec<_> = order.take_ready().into_iter().collect();
837 assert_eq!(ready, vec![a, b]);
838
839 order.mark_confirmed(vec![a, b]);
840 let ready: Vec<_> = order.take_ready().into_iter().collect();
841 assert!(ready.is_empty());
842 }
843}