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