diff --git a/Cargo.toml b/Cargo.toml index 89fb46c76a2..783c4312663 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 ea24f4c3083..6906f62282c 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, REPORT_PREAMBLE}; +use cargo::drop_println; pub fn cli() -> App { subcommand("report") @@ -11,14 +10,14 @@ pub fn cli() -> App { .setting(clap::AppSettings::SubcommandRequiredElseHelp) .subcommand( subcommand("future-incompatibilities") + .alias("future-incompat") .about("Reports any crates which will eventually stop compiling") .arg( opt( "id", "identifier of the report generated by a Cargo command invocation", ) - .value_name("id") - .required(true), + .value_name("id"), ), ) } @@ -35,31 +34,12 @@ 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_PREAMBLE); + drop(config.shell().print_ansi_stdout(report.as_bytes())); Ok(()) } diff --git a/src/cargo/core/compiler/future_incompat.rs b/src/cargo/core/compiler/future_incompat.rs index 0a74aa5f110..b940bb2b7d0 100644 --- a/src/cargo/core/compiler/future_incompat.rs +++ b/src/cargo/core/compiler/future_incompat.rs @@ -1,4 +1,28 @@ +//! Support for future-incompatible warning reporting. + +use crate::core::{Dependency, PackageId, Workspace}; +use crate::sources::SourceConfigMap; +use crate::util::{iter_join, CargoResult, Config}; +use anyhow::{bail, format_err, Context}; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fmt::Write as _; +use std::io::{Read, Write}; + +pub const REPORT_PREAMBLE: &str = "\ +The following warnings were discovered during the build. These warnings are an +indication that the packages contain code that will become an error in a +future release of Rust. These warnings typically cover changes to close +soundness problems, unintended or undocumented behavior, or critical problems +that cannot be fixed in a backwards-compatible fashion, and are not expected +to be in wide use. + +Each warning should contain a link for more information on what the warning +means and how to resolve it. +"; + +/// Current version of the on-disk format. +const ON_DISK_VERSION: u32 = 0; /// The future incompatibility report, emitted by the compiler as a JSON message. #[derive(serde::Deserialize)] @@ -6,6 +30,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 +55,234 @@ 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: ON_DISK_VERSION, + 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(ws, 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 != ON_DISK_VERSION { + 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( + ws: &Workspace<'_>, + 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'); + } + if let Some(s) = render_suggestions(ws, &per_package_reports) { + rendered.push_str(&s); + } + rendered +} + +fn render_suggestions( + ws: &Workspace<'_>, + per_package_reports: &[&FutureIncompatReportPackage], +) -> Option { + // This in general ignores all errors since this is opportunistic. + let _lock = ws.config().acquire_package_cache_lock().ok()?; + // Create a set of updated registry sources. + let map = SourceConfigMap::new(ws.config()).ok()?; + let package_ids: BTreeSet<_> = per_package_reports + .iter() + .map(|r| r.package_id) + .filter(|pkg_id| pkg_id.source_id().is_registry()) + .collect(); + let source_ids: HashSet<_> = package_ids + .iter() + .map(|pkg_id| pkg_id.source_id()) + .collect(); + let mut sources: HashMap<_, _> = source_ids + .into_iter() + .filter_map(|sid| { + let source = map.load(sid, &HashSet::new()).ok()?; + Some((sid, source)) + }) + .collect(); + // Query the sources for new versions. + let mut suggestions = String::new(); + for pkg_id in package_ids { + let source = match sources.get_mut(&pkg_id.source_id()) { + Some(s) => s, + None => continue, + }; + let dep = Dependency::parse(pkg_id.name(), None, pkg_id.source_id()).ok()?; + let summaries = source.query_vec(&dep).ok()?; + let versions = itertools::sorted( + summaries + .iter() + .map(|summary| summary.version()) + .filter(|version| *version > pkg_id.version()), + ); + let versions = versions.map(|version| version.to_string()); + let versions = iter_join(versions, ", "); + if !versions.is_empty() { + writeln!( + suggestions, + "{} has the following newer versions available: {}", + pkg_id, versions + ) + .unwrap(); + } + } + if suggestions.is_empty() { + None + } else { + Some(format!( + "The following packages appear to have newer versions available.\n\ + You may want to consider updating them to a newer version to see if the \ + issue has been fixed.\n\n{}", + suggestions + )) + } } diff --git a/src/cargo/core/compiler/job_queue.rs b/src/cargo/core/compiler/job_queue.rs index 632720a8727..79d8e3a26c0 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,11 +70,10 @@ 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}; -use crate::drop_eprint; use crate::util::diagnostic_server::{self, DiagnosticPrinter}; use crate::util::interning::InternedString; use crate::util::machine_message::{self, Message as _}; @@ -161,7 +157,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 +169,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. /// @@ -280,7 +271,7 @@ impl<'a> JobState<'a> { pub fn stderr(&self, stderr: String) -> CargoResult<()> { if let Some(config) = self.output { let mut shell = config.shell(); - shell.print_ansi(stderr.as_bytes())?; + shell.print_ansi_stderr(stderr.as_bytes())?; shell.err().write_all(b"\n")?; } else { self.messages.push_bounded(Message::Stderr(stderr)); @@ -432,7 +423,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 @@ -569,7 +560,7 @@ impl<'cfg> DrainState<'cfg> { } Message::Stderr(err) => { let mut shell = cx.bcx.config.shell(); - shell.print_ansi(err.as_bytes())?; + shell.print_ansi_stderr(err.as_bytes())?; shell.err().write_all(b"\n")?; } Message::FixDiagnostic(msg) => { @@ -615,10 +606,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 +792,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 +802,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); - - let crates_and_versions = self - .per_crate_future_incompat_reports - .iter() - .map(|r| r.package_id.to_string()) - .collect::>() - .join(", "); + return; + } - 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 + // 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(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(bcx.config.shell().print_ansi_stderr(rendered.as_bytes())); + 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/src/cargo/core/shell.rs b/src/cargo/core/shell.rs index 20b779694ac..7a9b6824fd6 100644 --- a/src/cargo/core/shell.rs +++ b/src/cargo/core/shell.rs @@ -323,8 +323,8 @@ impl Shell { } } - /// Prints a message and translates ANSI escape code into console colors. - pub fn print_ansi(&mut self, message: &[u8]) -> CargoResult<()> { + /// Prints a message to stderr and translates ANSI escape code into console colors. + pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> { if self.needs_clear { self.err_erase_line(); } @@ -339,6 +339,22 @@ impl Shell { Ok(()) } + /// Prints a message to stdout and translates ANSI escape code into console colors. + pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> { + if self.needs_clear { + self.err_erase_line(); + } + #[cfg(windows)] + { + if let ShellOut::Stream { stdout, .. } = &mut self.output { + ::fwdansi::write_ansi(stdout, message)?; + return Ok(()); + } + } + self.out().write_all(message)?; + Ok(()) + } + pub fn print_json(&mut self, obj: &T) -> CargoResult<()> { // Path may fail to serialize to JSON ... let encoded = serde_json::to_string(&obj)?; diff --git a/tests/testsuite/future_incompat_report.rs b/tests/testsuite/future_incompat_report.rs index 11397049a44..2160125ff2e 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,171 @@ 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-incompat -Z future-incompat-report") + .masquerade_as_nightly_cargo() + .exec_with_output() + .unwrap(); + let output = std::str::from_utf8(&output.stdout).unwrap(); + assert!(output.starts_with("The following warnings were discovered")); + let mut lines = output + .lines() + // Skip the beginning of the per-package information. + .skip_while(|line| !line.starts_with("The package")); + 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(), 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(); +} + +#[cargo_test] +fn suggestions_for_updates() { + if !is_nightly() { + return; + } + + Package::new("with_updates", "1.0.0") + .file("src/lib.rs", FUTURE_EXAMPLE) + .publish(); + Package::new("big_update", "1.0.0") + .file("src/lib.rs", FUTURE_EXAMPLE) + .publish(); + Package::new("without_updates", "1.0.0") + .file("src/lib.rs", FUTURE_EXAMPLE) + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + + [dependencies] + with_updates = "1" + big_update = "1" + without_updates = "1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("with_updates", "1.0.1") + .file("src/lib.rs", "") + .publish(); + Package::new("with_updates", "1.0.2") + .file("src/lib.rs", "") + .publish(); + Package::new("big_update", "2.0.0") + .file("src/lib.rs", "") + .publish(); + + // This is a hack to force cargo to update the index. Cargo can't do this + // automatically because doing a network update on every build would be a + // bad idea. Under normal circumstances, we'll hope the user has done + // something else along the way to trigger an update (building some other + // project or something). This could use some more consideration of how to + // handle this better (maybe only trigger an update if it hasn't updated + // in a long while?). + p.cargo("update -p without_updates").run(); + + p.cargo("check -Zfuture-incompat-report") + .masquerade_as_nightly_cargo() + .with_stderr_contains("[..]cargo report future-incompatibilities --id 1[..]") + .run(); + + p.cargo("report future-incompatibilities") + .masquerade_as_nightly_cargo() + .with_stdout_contains( + "\ +The following packages appear to have newer versions available. +You may want to consider updating them to a newer version to see if the issue has been fixed. + +big_update v1.0.0 has the following newer versions available: 2.0.0 +with_updates v1.0.0 has the following newer versions available: 1.0.1, 1.0.2 +", + ) .run(); }