From c2b02b392605507822af9909d88ef373f1e81cf1 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sun, 20 Jun 2021 09:32:43 -0700 Subject: [PATCH] Updates to future-incompatible reporting. --- Cargo.toml | 1 - src/bin/cargo/commands/report.rs | 42 ++--- src/cargo/core/compiler/future_incompat.rs | 180 ++++++++++++++++++++- src/cargo/core/compiler/job_queue.rs | 148 ++++++----------- tests/testsuite/future_incompat_report.rs | 174 +++++++++++++++----- 5 files changed, 372 insertions(+), 173 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89fb46c76..783c43126 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,6 @@ itertools = "0.10.0" # See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust` # for more information. rustc-workspace-hack = "1.0.0" -rand = "0.8.3" [target.'cfg(windows)'.dependencies] fwdansi = "1.1.0" diff --git a/src/bin/cargo/commands/report.rs b/src/bin/cargo/commands/report.rs index ea24f4c30..a5ff63d13 100644 --- a/src/bin/cargo/commands/report.rs +++ b/src/bin/cargo/commands/report.rs @@ -1,8 +1,7 @@ use crate::command_prelude::*; -use anyhow::{anyhow, Context as _}; -use cargo::core::compiler::future_incompat::{OnDiskReport, FUTURE_INCOMPAT_FILE}; -use cargo::drop_eprint; -use std::io::Read; +use anyhow::anyhow; +use cargo::core::compiler::future_incompat::OnDiskReports; +use cargo::drop_println; pub fn cli() -> App { subcommand("report") @@ -17,8 +16,7 @@ pub fn cli() -> App { "id", "identifier of the report generated by a Cargo command invocation", ) - .value_name("id") - .required(true), + .value_name("id"), ), ) } @@ -35,31 +33,11 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult { fn report_future_incompatibilies(config: &Config, args: &ArgMatches<'_>) -> CliResult { let ws = args.workspace(config)?; - let report_file = ws.target_dir().open_ro( - FUTURE_INCOMPAT_FILE, - ws.config(), - "Future incompatible report", - )?; - - let mut file_contents = String::new(); - report_file - .file() - .read_to_string(&mut file_contents) - .with_context(|| "failed to read report")?; - let on_disk_report: OnDiskReport = - serde_json::from_str(&file_contents).with_context(|| "failed to load report")?; - - let id = args.value_of("id").unwrap(); - if id != on_disk_report.id { - return Err(anyhow!( - "Expected an id of `{}`, but `{}` was provided on the command line. \ - Your report may have been overwritten by a different one.", - on_disk_report.id, - id - ) - .into()); - } - - drop_eprint!(config, "{}", on_disk_report.report); + let reports = OnDiskReports::load(&ws)?; + let id = args + .value_of_u32("id")? + .unwrap_or_else(|| reports.last_id()); + let report = reports.get_report(id, config)?; + drop_println!(config, "{}", report); Ok(()) } diff --git a/src/cargo/core/compiler/future_incompat.rs b/src/cargo/core/compiler/future_incompat.rs index 0a74aa5f1..8a4b0e9a3 100644 --- a/src/cargo/core/compiler/future_incompat.rs +++ b/src/cargo/core/compiler/future_incompat.rs @@ -1,4 +1,10 @@ +//! Support for future-incompatible warning reporting. + +use crate::core::{PackageId, Workspace}; +use crate::util::{iter_join, CargoResult, Config}; +use anyhow::{bail, format_err, Context}; use serde::{Deserialize, Serialize}; +use std::io::{Read, Write}; /// The future incompatibility report, emitted by the compiler as a JSON message. #[derive(serde::Deserialize)] @@ -6,6 +12,13 @@ pub struct FutureIncompatReport { pub future_incompat_report: Vec, } +/// Structure used for collecting reports in-memory. +pub struct FutureIncompatReportPackage { + pub package_id: PackageId, + pub items: Vec, +} + +/// A single future-incompatible warning emitted by rustc. #[derive(Serialize, Deserialize)] pub struct FutureBreakageItem { /// The date at which this lint will become an error. @@ -24,13 +37,166 @@ pub struct Diagnostic { /// The filename in the top-level `target` directory where we store /// the report -pub const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json"; +const FUTURE_INCOMPAT_FILE: &str = ".future-incompat-report.json"; +/// Max number of reports to save on disk. +const MAX_REPORTS: usize = 5; +/// The structure saved to disk containing the reports. #[derive(Serialize, Deserialize)] -pub struct OnDiskReport { - // A Cargo-generated id used to detect when a report has been overwritten - pub id: String, - // Cannot be a &str, since Serde needs - // to be able to un-escape the JSON - pub report: String, +pub struct OnDiskReports { + /// A schema version number, to handle older cargo's from trying to read + /// something that they don't understand. + version: u32, + /// The report ID to use for the next report to save. + next_id: u32, + /// Available reports. + reports: Vec, +} + +/// A single report for a given compilation session. +#[derive(Serialize, Deserialize)] +struct OnDiskReport { + /// Unique reference to the report for the `--id` CLI flag. + id: u32, + /// Report, suitable for printing to the console. + report: String, +} + +impl Default for OnDiskReports { + fn default() -> OnDiskReports { + OnDiskReports { + version: 0, + next_id: 1, + reports: Vec::new(), + } + } +} + +impl OnDiskReports { + /// Saves a new report. + pub fn save_report( + ws: &Workspace<'_>, + per_package_reports: &[FutureIncompatReportPackage], + ) -> OnDiskReports { + let mut current_reports = match Self::load(ws) { + Ok(r) => r, + Err(e) => { + log::debug!( + "saving future-incompatible reports failed to load current reports: {:?}", + e + ); + OnDiskReports::default() + } + }; + let report = OnDiskReport { + id: current_reports.next_id, + report: render_report(per_package_reports), + }; + current_reports.next_id += 1; + current_reports.reports.push(report); + if current_reports.reports.len() > MAX_REPORTS { + current_reports.reports.remove(0); + } + let on_disk = serde_json::to_vec(¤t_reports).unwrap(); + if let Err(e) = ws + .target_dir() + .open_rw( + FUTURE_INCOMPAT_FILE, + ws.config(), + "Future incompatibility report", + ) + .and_then(|file| { + let mut file = file.file(); + file.set_len(0)?; + file.write_all(&on_disk)?; + Ok(()) + }) + { + crate::display_warning_with_error( + "failed to write on-disk future incompatible report", + &e, + &mut ws.config().shell(), + ); + } + current_reports + } + + /// Loads the on-disk reports. + pub fn load(ws: &Workspace<'_>) -> CargoResult { + let report_file = match ws.target_dir().open_ro( + FUTURE_INCOMPAT_FILE, + ws.config(), + "Future incompatible report", + ) { + Ok(r) => r, + Err(e) => { + if let Some(io_err) = e.downcast_ref::() { + if io_err.kind() == std::io::ErrorKind::NotFound { + bail!("no reports are currently available"); + } + } + return Err(e); + } + }; + + let mut file_contents = String::new(); + report_file + .file() + .read_to_string(&mut file_contents) + .with_context(|| "failed to read report")?; + let on_disk_reports: OnDiskReports = + serde_json::from_str(&file_contents).with_context(|| "failed to load report")?; + if on_disk_reports.version != 0 { + bail!("unable to read reports; reports were saved from a future version of Cargo"); + } + Ok(on_disk_reports) + } + + /// Returns the most recent report ID. + pub fn last_id(&self) -> u32 { + self.reports.last().map(|r| r.id).unwrap() + } + + pub fn get_report(&self, id: u32, config: &Config) -> CargoResult { + let report = self.reports.iter().find(|r| r.id == id).ok_or_else(|| { + let available = iter_join(self.reports.iter().map(|r| r.id.to_string()), ", "); + format_err!( + "could not find report with ID {}\n\ + Available IDs are: {}", + id, + available + ) + })?; + let report = if config.shell().err_supports_color() { + report.report.clone() + } else { + strip_ansi_escapes::strip(&report.report) + .map(|v| String::from_utf8(v).expect("utf8")) + .expect("strip should never fail") + }; + Ok(report) + } +} + +fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> String { + let mut per_package_reports: Vec<_> = per_package_reports.iter().collect(); + per_package_reports.sort_by_key(|r| r.package_id); + let mut rendered = String::new(); + for per_package in per_package_reports { + rendered.push_str(&format!( + "The package `{}` currently triggers the following future \ + incompatibility lints:\n", + per_package.package_id + )); + for item in &per_package.items { + rendered.extend( + item.diagnostic + .rendered + .lines() + .map(|l| format!("> {}\n", l)), + ); + } + rendered.push('\n'); + } + rendered } diff --git a/src/cargo/core/compiler/job_queue.rs b/src/cargo/core/compiler/job_queue.rs index 632720a87..1679e322c 100644 --- a/src/cargo/core/compiler/job_queue.rs +++ b/src/cargo/core/compiler/job_queue.rs @@ -50,8 +50,7 @@ //! improved. use std::cell::Cell; -use std::collections::BTreeSet; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::io; use std::marker; use std::sync::Arc; @@ -62,8 +61,6 @@ use cargo_util::ProcessBuilder; use crossbeam_utils::thread::Scope; use jobserver::{Acquired, Client, HelperThread}; use log::{debug, info, trace}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; use super::context::OutputFile; use super::job::{ @@ -73,7 +70,7 @@ use super::job::{ use super::timings::Timings; use super::{BuildContext, BuildPlan, CompileMode, Context, Unit}; use crate::core::compiler::future_incompat::{ - FutureBreakageItem, OnDiskReport, FUTURE_INCOMPAT_FILE, + FutureBreakageItem, FutureIncompatReportPackage, OnDiskReports, }; use crate::core::resolver::ResolveBehavior; use crate::core::{FeatureValue, PackageId, Shell, TargetKind}; @@ -161,7 +158,7 @@ struct DrainState<'cfg> { /// How many jobs we've finished finished: usize, - per_crate_future_incompat_reports: Vec, + per_package_future_incompat_reports: Vec, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -173,11 +170,6 @@ impl std::fmt::Display for JobId { } } -struct FutureIncompatReportCrate { - package_id: PackageId, - report: Vec, -} - /// A `JobState` is constructed by `JobQueue::run` and passed to `Job::run`. It includes everything /// necessary to communicate between the main thread and the execution of the job. /// @@ -432,7 +424,7 @@ impl<'cfg> JobQueue<'cfg> { pending_queue: Vec::new(), print: DiagnosticPrinter::new(cx.bcx.config), finished: 0, - per_crate_future_incompat_reports: Vec::new(), + per_package_future_incompat_reports: Vec::new(), }; // Create a helper thread for acquiring jobserver tokens @@ -615,10 +607,10 @@ impl<'cfg> DrainState<'cfg> { } } } - Message::FutureIncompatReport(id, report) => { + Message::FutureIncompatReport(id, items) => { let package_id = self.active[&id].pkg.package_id(); - self.per_crate_future_incompat_reports - .push(FutureIncompatReportCrate { package_id, report }); + self.per_package_future_incompat_reports + .push(FutureIncompatReportPackage { package_id, items }); } Message::Token(acquired_token) => { let token = acquired_token.with_context(|| "failed to acquire jobserver token")?; @@ -801,7 +793,7 @@ impl<'cfg> DrainState<'cfg> { if !cx.bcx.build_config.build_plan { // It doesn't really matter if this fails. drop(cx.bcx.config.shell().status("Finished", message)); - self.emit_future_incompat(cx); + self.emit_future_incompat(cx.bcx); } None @@ -811,93 +803,57 @@ impl<'cfg> DrainState<'cfg> { } } - fn emit_future_incompat(&mut self, cx: &mut Context<'_, '_>) { - if cx.bcx.config.cli_unstable().future_incompat_report { - if self.per_crate_future_incompat_reports.is_empty() { + fn emit_future_incompat(&mut self, bcx: &BuildContext<'_, '_>) { + if !bcx.config.cli_unstable().future_incompat_report { + return; + } + if self.per_package_future_incompat_reports.is_empty() { + if bcx.build_config.future_incompat_report { drop( - cx.bcx - .config + bcx.config .shell() - .note("0 dependencies had future-incompat warnings"), + .note("0 dependencies had future-incompatible warnings"), ); - return; } - self.per_crate_future_incompat_reports - .sort_by_key(|r| r.package_id); + return; + } - let crates_and_versions = self - .per_crate_future_incompat_reports - .iter() - .map(|r| r.package_id.to_string()) - .collect::>() - .join(", "); + // Get a list of unique and sorted package name/versions. + let package_vers: BTreeSet<_> = self + .per_package_future_incompat_reports + .iter() + .map(|r| r.package_id) + .collect(); + let package_vers: Vec<_> = package_vers + .into_iter() + .map(|pid| pid.to_string()) + .collect(); - drop(cx.bcx.config.shell().warn(&format!( - "the following crates contain code that will be rejected by a future version of Rust: {}", - crates_and_versions + drop(bcx.config.shell().warn(&format!( + "the following packages contain code that will be rejected by a future \ + version of Rust: {}", + package_vers.join(", ") + ))); + + let on_disk_reports = + OnDiskReports::save_report(bcx.ws, &self.per_package_future_incompat_reports); + let report_id = on_disk_reports.last_id(); + + if bcx.build_config.future_incompat_report { + let rendered = on_disk_reports.get_report(report_id, bcx.config).unwrap(); + drop_eprint!(bcx.config, "{}", rendered); + drop(bcx.config.shell().note(&format!( + "this report can be shown with `cargo report \ + future-incompatibilities -Z future-incompat-report --id {}`", + report_id + ))); + } else { + drop(bcx.config.shell().note(&format!( + "to see what the problems were, use the option \ + `--future-incompat-report`, or run `cargo report \ + future-incompatibilities --id {}`", + report_id ))); - - let mut full_report = String::new(); - let mut rng = thread_rng(); - - // Generate a short ID to allow detecting if a report gets overwritten - let id: String = std::iter::repeat(()) - .map(|()| char::from(rng.sample(Alphanumeric))) - .take(4) - .collect(); - - for report in std::mem::take(&mut self.per_crate_future_incompat_reports) { - full_report.push_str(&format!( - "The crate `{}` currently triggers the following future incompatibility lints:\n", - report.package_id - )); - for item in report.report { - let rendered = if cx.bcx.config.shell().err_supports_color() { - item.diagnostic.rendered - } else { - strip_ansi_escapes::strip(&item.diagnostic.rendered) - .map(|v| String::from_utf8(v).expect("utf8")) - .expect("strip should never fail") - }; - - for line in rendered.lines() { - full_report.push_str(&format!("> {}\n", line)); - } - } - } - - let report_file = cx.bcx.ws.target_dir().open_rw( - FUTURE_INCOMPAT_FILE, - cx.bcx.config, - "Future incompatibility report", - ); - let err = report_file - .and_then(|report_file| { - let on_disk_report = OnDiskReport { - id: id.clone(), - report: full_report.clone(), - }; - serde_json::to_writer(report_file, &on_disk_report).map_err(|e| e.into()) - }) - .err(); - if let Some(e) = err { - crate::display_warning_with_error( - "failed to write on-disk future incompat report", - &e, - &mut cx.bcx.config.shell(), - ); - } - - if cx.bcx.build_config.future_incompat_report { - drop_eprint!(cx.bcx.config, "{}", full_report); - drop(cx.bcx.config.shell().note( - &format!("this report can be shown with `cargo report future-incompatibilities -Z future-incompat-report --id {}`", id) - )); - } else { - drop(cx.bcx.config.shell().note( - &format!("to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id {}`", id) - )); - } } } diff --git a/tests/testsuite/future_incompat_report.rs b/tests/testsuite/future_incompat_report.rs index 11397049a..c0f055e3d 100644 --- a/tests/testsuite/future_incompat_report.rs +++ b/tests/testsuite/future_incompat_report.rs @@ -1,27 +1,33 @@ //! Tests for future-incompat-report messages use cargo_test_support::registry::Package; -use cargo_test_support::{basic_manifest, is_nightly, project}; +use cargo_test_support::{basic_manifest, is_nightly, project, Project}; + +// An arbitrary lint (array_into_iter) that triggers a report. +const FUTURE_EXAMPLE: &'static str = "fn main() { [true].into_iter(); }"; +// Some text that will be displayed when the lint fires. +const FUTURE_OUTPUT: &'static str = "[..]array_into_iter[..]"; + +fn simple_project() -> Project { + project() + .file("Cargo.toml", &basic_manifest("foo", "0.0.0")) + .file("src/main.rs", FUTURE_EXAMPLE) + .build() +} #[cargo_test] fn no_output_on_stable() { - let p = project() - .file("Cargo.toml", &basic_manifest("foo", "0.0.0")) - .file("src/main.rs", "fn main() { [true].into_iter(); }") - .build(); + let p = simple_project(); p.cargo("build") - .with_stderr_contains(" = note: `#[warn(array_into_iter)]` on by default") - .with_stderr_does_not_contain("[..]crates[..]") + .with_stderr_contains(FUTURE_OUTPUT) + .with_stderr_does_not_contain("[..]cargo report[..]") .run(); } #[cargo_test] fn gate_future_incompat_report() { - let p = project() - .file("Cargo.toml", &basic_manifest("foo", "0.0.0")) - .file("src/main.rs", "fn main() { [true].into_iter(); }") - .build(); + let p = simple_project(); p.cargo("build --future-incompat-report") .with_stderr_contains("error: the `--future-incompat-report` flag is unstable[..]") @@ -60,9 +66,25 @@ fn test_zero_future_incompat() { .file("src/main.rs", "fn main() {}") .build(); + // No note if --future-incompat-report is not specified. + p.cargo("build -Z future-incompat-report") + .masquerade_as_nightly_cargo() + .with_stderr( + "\ +[COMPILING] foo v0.0.0 [..] +[FINISHED] [..] +", + ) + .run(); + p.cargo("build --future-incompat-report -Z unstable-options -Z future-incompat-report") .masquerade_as_nightly_cargo() - .with_stderr_contains("note: 0 dependencies had future-incompat warnings") + .with_stderr( + "\ +[FINISHED] [..] +note: 0 dependencies had future-incompatible warnings +", + ) .run(); } @@ -72,24 +94,21 @@ fn test_single_crate() { return; } - let p = project() - .file("Cargo.toml", &basic_manifest("foo", "0.0.0")) - .file("src/main.rs", "fn main() { [true].into_iter(); }") - .build(); + let p = simple_project(); for command in &["build", "check", "rustc", "test"] { p.cargo(command).arg("-Zfuture-incompat-report") .masquerade_as_nightly_cargo() - .with_stderr_contains(" = note: `#[warn(array_into_iter)]` on by default") - .with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]") + .with_stderr_contains(FUTURE_OUTPUT) + .with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]") .with_stderr_does_not_contain("[..]incompatibility[..]") .run(); p.cargo(command).arg("-Zfuture-incompat-report").arg("-Zunstable-options").arg("--future-incompat-report") .masquerade_as_nightly_cargo() - .with_stderr_contains(" = note: `#[warn(array_into_iter)]` on by default") - .with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]") - .with_stderr_contains("The crate `foo v0.0.0 ([..])` currently triggers the following future incompatibility lints:") + .with_stderr_contains(FUTURE_OUTPUT) + .with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: foo v0.0.0 [..]") + .with_stderr_contains("The package `foo v0.0.0 ([..])` currently triggers the following future incompatibility lints:") .run(); } } @@ -101,10 +120,10 @@ fn test_multi_crate() { } Package::new("first-dep", "0.0.1") - .file("src/lib.rs", "fn foo() { [25].into_iter(); }") + .file("src/lib.rs", FUTURE_EXAMPLE) .publish(); Package::new("second-dep", "0.0.2") - .file("src/lib.rs", "fn foo() { ['a'].into_iter(); }") + .file("src/lib.rs", FUTURE_EXAMPLE) .publish(); let p = project() @@ -126,24 +145,17 @@ fn test_multi_crate() { for command in &["build", "check", "rustc", "test"] { p.cargo(command).arg("-Zfuture-incompat-report") .masquerade_as_nightly_cargo() - .with_stderr_does_not_contain("[..]array_into_iter[..]") - .with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2") + .with_stderr_does_not_contain(FUTURE_OUTPUT) + .with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2") // Check that we don't have the 'triggers' message shown at the bottom of this loop .with_stderr_does_not_contain("[..]triggers[..]") .run(); - p.cargo("report future-incompatibilities -Z future-incompat-report --id bad-id") - .masquerade_as_nightly_cargo() - .with_stderr_contains("error: Expected an id of [..]") - .with_stderr_does_not_contain("[..]triggers[..]") - .with_status(101) - .run(); - p.cargo(command).arg("-Zunstable-options").arg("-Zfuture-incompat-report").arg("--future-incompat-report") .masquerade_as_nightly_cargo() - .with_stderr_contains("warning: the following crates contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2") - .with_stderr_contains("The crate `first-dep v0.0.1` currently triggers the following future incompatibility lints:") - .with_stderr_contains("The crate `second-dep v0.0.2` currently triggers the following future incompatibility lints:") + .with_stderr_contains("warning: the following packages contain code that will be rejected by a future version of Rust: first-dep v0.0.1, second-dep v0.0.2") + .with_stderr_contains("The package `first-dep v0.0.1` currently triggers the following future incompatibility lints:") + .with_stderr_contains("The package `second-dep v0.0.2` currently triggers the following future incompatibility lints:") .run(); } @@ -172,7 +184,95 @@ fn test_multi_crate() { p.cargo(&format!("report future-incompatibilities -Z future-incompat-report --id {}", id)) .masquerade_as_nightly_cargo() - .with_stderr_contains("The crate `first-dep v0.0.1` currently triggers the following future incompatibility lints:") - .with_stderr_contains("The crate `second-dep v0.0.2` currently triggers the following future incompatibility lints:") + .with_stdout_contains("The package `first-dep v0.0.1` currently triggers the following future incompatibility lints:") + .with_stdout_contains("The package `second-dep v0.0.2` currently triggers the following future incompatibility lints:") + .run(); + + // Test without --id, and also the full output of the report. + let output = p + .cargo("report future-incompatibilities -Z future-incompat-report") + .masquerade_as_nightly_cargo() + .exec_with_output() + .unwrap(); + let output = std::str::from_utf8(&output.stdout).unwrap(); + let mut lines = output.lines(); + for expected in &["first-dep v0.0.1", "second-dep v0.0.2"] { + assert_eq!( + &format!( + "The package `{}` currently triggers the following future incompatibility lints:", + expected + ), + lines.next().unwrap() + ); + let mut count = 0; + while let Some(line) = lines.next() { + if line.is_empty() { + break; + } + count += 1; + } + assert!(count > 0); + } + assert_eq!(lines.next(), Some("")); + assert_eq!(lines.next(), None); +} + +#[cargo_test] +fn color() { + if !is_nightly() { + return; + } + + let p = simple_project(); + + p.cargo("check -Zfuture-incompat-report") + .masquerade_as_nightly_cargo() + .run(); + + p.cargo("report future-incompatibilities -Z future-incompat-report") + .masquerade_as_nightly_cargo() + .with_stdout_does_not_contain("[..]\x1b[[..]") + .run(); + + p.cargo("report future-incompatibilities -Z future-incompat-report") + .masquerade_as_nightly_cargo() + .env("CARGO_TERM_COLOR", "always") + .with_stdout_contains("[..]\x1b[[..]") + .run(); +} + +#[cargo_test] +fn bad_ids() { + if !is_nightly() { + return; + } + + let p = simple_project(); + + p.cargo("report future-incompatibilities -Z future-incompat-report --id 1") + .masquerade_as_nightly_cargo() + .with_status(101) + .with_stderr("error: no reports are currently available") + .run(); + + p.cargo("check -Zfuture-incompat-report") + .masquerade_as_nightly_cargo() + .run(); + + p.cargo("report future-incompatibilities -Z future-incompat-report --id foo") + .masquerade_as_nightly_cargo() + .with_status(1) + .with_stderr("error: Invalid value: could not parse `foo` as a number") + .run(); + + p.cargo("report future-incompatibilities -Z future-incompat-report --id 7") + .masquerade_as_nightly_cargo() + .with_status(101) + .with_stderr( + "\ +error: could not find report with ID 7 +Available IDs are: 1 +", + ) .run(); }