#![warn(clippy::pedantic)]
#![warn(clippy::cargo)]
#![allow(clippy::semicolon_if_nothing_returned)]
#![allow(clippy::let_underscore_drop)]
#![allow(clippy::single_match_else)]
use std::env;
use std::ffi::OsString;
use std::fmt;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::process;
use std::str::FromStr;
use anyhow::{bail, Context};
use chrono::{Date, Duration, NaiveDate, Utc};
use clap::{ArgAction, Parser, ValueEnum};
use colored::Colorize;
use log::debug;
use reqwest::blocking::Client;
mod git;
mod github;
mod least_satisfying;
mod repo_access;
mod toolchains;
use crate::least_satisfying::{least_satisfying, Satisfies};
use crate::repo_access::{AccessViaGithub, AccessViaLocalGit, RustRepositoryAccessor};
use crate::toolchains::{
download_progress, parse_to_utc_date, DownloadParams, InstallError, TestOutcome, Toolchain,
ToolchainSpec, NIGHTLY_SERVER, YYYY_MM_DD,
};
#[derive(Debug, Clone, PartialEq)]
pub struct Commit {
pub sha: String,
pub date: GitDate,
pub summary: String,
}
const EPOCH_COMMIT: &str = "927c55d86b0be44337f37cf5b0a76fb8ba86e06c";
const REPORT_HEADER: &str = "\
==================================================================================
= Please file this regression report on the rust-lang/rust GitHub repository =
= New issue: https://github.com/rust-lang/rust/issues/new =
= Known issues: https://github.com/rust-lang/rust/issues =
= Copy and paste the text below into the issue report thread. Thanks! =
==================================================================================";
#[derive(Debug, Parser)]
#[command(bin_name = "cargo", subcommand_required = true)]
enum Cargo {
BisectRustc(Opts),
}
#[derive(Debug, Parser)]
#[command(
bin_name = "cargo bisect-rustc",
version,
about,
next_display_order = None,
after_help = "Examples:
Run a fully automatic nightly bisect doing `cargo check`:
```
cargo bisect-rustc --start 2018-07-07 --end 2018-07-30 --test-dir ../my_project/ -- check
```
Run a PR-based bisect with manual prompts after each run doing `cargo build`:
```
cargo bisect-rustc --start 6a1c0637ce44aeea6c60527f4c0e7fb33f2bcd0d \\
--end 866a713258915e6cbb212d135f751a6a8c9e1c0a --test-dir ../my_project/ --prompt -- build
```"
)]
#[allow(clippy::struct_excessive_bools)]
struct Opts {
#[arg(
long,
help = "Custom regression definition",
value_enum,
default_value_t = RegressOn::Error,
)]
regress: RegressOn,
#[arg(short, long, help = "Download the alt build instead of normal build")]
alt: bool,
#[arg(
long,
help = "Host triple for the compiler",
default_value = env!("HOST"),
)]
host: String,
#[arg(long, help = "Cross-compilation target platform")]
target: Option<String>,
#[arg(long, help = "Preserve the downloaded artifacts")]
preserve: bool,
#[arg(long, help = "Preserve the target directory used for builds")]
preserve_target: bool,
#[arg(long, help = "Download rust-src [default: no download]")]
with_src: bool,
#[arg(long, help = "Download rustc-dev [default: no download]")]
with_dev: bool,
#[arg(short, long = "component", help = "additional components to install")]
components: Vec<String>,
#[arg(
long,
help = "Root directory for tests",
default_value = ".",
value_parser = validate_dir
)]
test_dir: PathBuf,
#[arg(long, help = "Manually evaluate for regression with prompts")]
prompt: bool,
#[arg(
long,
short,
help = "Assume failure after specified number of seconds (for bisecting hangs)"
)]
timeout: Option<usize>,
#[arg(short, long = "verbose", action = ArgAction::Count)]
verbosity: u8,
#[arg(
help = "Arguments to pass to cargo or the file specified by --script during tests",
num_args = 1..,
last = true
)]
command_args: Vec<OsString>,
#[arg(
long,
help = "Left bound for search (*without* regression). You can use \
a date (YYYY-MM-DD), git tag name (e.g. 1.58.0) or git commit SHA."
)]
start: Option<Bound>,
#[arg(
long,
help = "Right bound for search (*with* regression). You can use \
a date (YYYY-MM-DD), git tag name (e.g. 1.58.0) or git commit SHA."
)]
end: Option<Bound>,
#[arg(long, help = "Bisect via commit artifacts")]
by_commit: bool,
#[arg(long, value_enum, help = "How to access Rust git repository", default_value_t = Access::Checkout)]
access: Access,
#[arg(long, help = "Install the given artifact")]
install: Option<Bound>,
#[arg(long, help = "Force installation over existing artifacts")]
force_install: bool,
#[arg(long, help = "Script replacement for `cargo build` command")]
script: Option<PathBuf>,
#[arg(long, help = "Do not install cargo [default: install cargo]")]
without_cargo: bool,
}
pub type GitDate = Date<Utc>;
fn validate_dir(s: &str) -> anyhow::Result<PathBuf> {
let path: PathBuf = s.parse()?;
if path.is_dir() {
Ok(path)
} else {
bail!(
"{} is not an existing directory",
path.canonicalize()?.display()
)
}
}
#[derive(Clone, Debug)]
enum Bound {
Commit(String),
Date(GitDate),
}
impl FromStr for Bound {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_to_utc_date(s)
.map(Self::Date)
.or_else(|_| Ok(Self::Commit(s.to_string())))
}
}
impl Bound {
fn sha(&self) -> anyhow::Result<String> {
match self {
Bound::Commit(commit) => Ok(commit.clone()),
Bound::Date(date) => {
let date_str = date.format(YYYY_MM_DD);
let url =
format!("{NIGHTLY_SERVER}/{date_str}/channel-rust-nightly-git-commit-hash.txt");
eprintln!("fetching {url}");
let client = Client::new();
let name = format!("nightly manifest {date_str}");
let mut response = download_progress(&client, &name, &url)?;
let mut commit = String::new();
response.read_to_string(&mut commit)?;
eprintln!("converted {date_str} to {commit}");
Ok(commit)
}
}
}
fn as_commit(&self) -> anyhow::Result<Self> {
self.sha().map(Bound::Commit)
}
}
impl Opts {
fn emit_cargo_output(&self) -> bool {
self.verbosity >= 2
}
}
#[derive(Debug, thiserror::Error)]
struct ExitError(i32);
impl fmt::Display for ExitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "exiting with {}", self.0)
}
}
impl Config {
fn default_outcome_of_output(&self, output: &process::Output) -> TestOutcome {
let status = output.status;
let stdout_utf8 = String::from_utf8_lossy(&output.stdout).to_string();
let stderr_utf8 = String::from_utf8_lossy(&output.stderr).to_string();
debug!(
"status: {:?} stdout: {:?} stderr: {:?}",
status, stdout_utf8, stderr_utf8
);
let saw_ice = stderr_utf8.contains("error: internal compiler error")
|| stderr_utf8.contains("' has overflowed its stack");
let input = (self.args.regress, status.success());
let result = match input {
(RegressOn::Error, true) | (RegressOn::Success, false) => TestOutcome::Baseline,
(RegressOn::Error, false) | (RegressOn::Success | RegressOn::NonError, true) => {
TestOutcome::Regressed
}
(RegressOn::Ice, _) | (RegressOn::NonError, false) => {
if saw_ice {
TestOutcome::Regressed
} else {
TestOutcome::Baseline
}
}
(RegressOn::NonIce, _) => {
if saw_ice {
TestOutcome::Baseline
} else {
TestOutcome::Regressed
}
}
};
debug!(
"default_outcome_of_output: input: {:?} result: {:?}",
input, result
);
result
}
}
#[derive(Clone, Debug, ValueEnum)]
enum Access {
Checkout,
Github,
}
impl Access {
fn repo(&self) -> Box<dyn RustRepositoryAccessor> {
match self {
Self::Checkout => Box::new(AccessViaLocalGit),
Self::Github => Box::new(AccessViaGithub),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
enum RegressOn {
Error,
Success,
Ice,
NonIce,
NonError,
}
impl RegressOn {
fn must_process_stderr(self) -> bool {
match self {
RegressOn::Error | RegressOn::Success => false,
RegressOn::NonError | RegressOn::Ice | RegressOn::NonIce => true,
}
}
}
struct Config {
args: Opts,
rustup_tmp_path: PathBuf,
toolchains_path: PathBuf,
target: String,
is_commit: bool,
client: Client,
}
impl Config {
fn from_args(mut args: Opts) -> anyhow::Result<Config> {
let target = args.target.clone().unwrap_or_else(|| args.host.clone());
let mut toolchains_path = home::rustup_home()?;
let rustup_tmp_path = toolchains_path.join("tmp");
if !rustup_tmp_path.exists() {
fs::create_dir(&rustup_tmp_path)?;
}
toolchains_path.push("toolchains");
if !toolchains_path.is_dir() {
bail!(
"`{}` is not a directory. Please install rustup.",
toolchains_path.display()
);
}
let is_commit = match (args.start.clone(), args.end.clone()) {
(Some(Bound::Commit(_)) | None, Some(Bound::Commit(_)))
| (Some(Bound::Commit(_)), None) => Some(true),
(Some(Bound::Date(_)) | None, Some(Bound::Date(_))) | (Some(Bound::Date(_)), None) => {
Some(false)
}
(None, None) => None,
(start, end) => bail!(
"cannot take different types of bounds for start/end, got start: {:?} and end {:?}",
start,
end
),
};
if is_commit == Some(false) && args.by_commit {
eprintln!("finding commit range that corresponds to dates specified");
match (args.start, args.end) {
(Some(b1), Some(b2)) => {
args.start = Some(b1.as_commit()?);
args.end = Some(b2.as_commit()?);
}
_ => unreachable!(),
}
}
Ok(Config {
is_commit: args.by_commit || is_commit == Some(true),
args,
target,
toolchains_path,
rustup_tmp_path,
client: Client::new(),
})
}
}
fn fixup_bounds(
access: &Access,
start: &mut Option<Bound>,
end: &mut Option<Bound>,
) -> anyhow::Result<()> {
let is_tag = |bound: &Option<Bound>| -> bool {
match bound {
Some(Bound::Commit(commit)) => commit.contains('.'),
None | Some(Bound::Date(_)) => false,
}
};
let is_datelike = |bound: &Option<Bound>| -> bool {
matches!(bound, None | Some(Bound::Date(_))) || is_tag(bound)
};
if !(is_datelike(start) && is_datelike(end)) {
return Ok(());
}
let fixup = |which: &str, bound: &mut Option<Bound>| -> anyhow::Result<()> {
if is_tag(bound) {
if let Some(Bound::Commit(tag)) = bound {
let date = access.repo().bound_to_date(Bound::Commit(tag.clone()))?;
eprintln!(
"translating --{which}={tag} to {date}",
date = date.format(YYYY_MM_DD)
);
*bound = Some(Bound::Date(date));
}
}
Ok(())
};
fixup("start", start)?;
fixup("end", end)?;
Ok(())
}
fn check_bounds(start: &Option<Bound>, end: &Option<Bound>) -> anyhow::Result<()> {
let current = Utc::today();
match start.as_ref().zip(end.as_ref()) {
Some((Bound::Date(start), Bound::Date(end))) if end < start => {
bail!(
"end should be after start, got start: {} and end {}",
start,
end
);
}
Some((Bound::Date(start), _)) if start > ¤t => {
bail!(
"start date should be on or before current date, got start date request: {} and current date is {}",
start,
current
);
}
Some((_, Bound::Date(end))) if end > ¤t => {
bail!(
"end date should be on or before current date, got start date request: {} and current date is {}",
end,
current
);
}
_ => Ok(()),
}
}
fn run() -> anyhow::Result<()> {
env_logger::try_init()?;
let mut args = match Cargo::try_parse() {
Ok(Cargo::BisectRustc(args)) => args,
Err(e) => match e.context().next() {
None => {
Cargo::parse();
unreachable!()
}
_ => Opts::parse(),
},
};
fixup_bounds(&args.access, &mut args.start, &mut args.end)?;
check_bounds(&args.start, &args.end)?;
let cfg = Config::from_args(args)?;
if let Some(ref bound) = cfg.args.install {
cfg.install(bound)
} else {
cfg.bisect()
}
}
impl Config {
fn install(&self, bound: &Bound) -> anyhow::Result<()> {
match *bound {
Bound::Commit(ref sha) => {
let sha = self.args.access.repo().commit(sha)?.sha;
let mut t = Toolchain {
spec: ToolchainSpec::Ci {
commit: sha,
alt: self.args.alt,
},
host: self.args.host.clone(),
std_targets: vec![self.args.host.clone(), self.target.clone()],
};
t.std_targets.sort();
t.std_targets.dedup();
let dl_params = DownloadParams::for_ci(self);
t.install(&self.client, &dl_params)?;
}
Bound::Date(date) => {
let mut t = Toolchain {
spec: ToolchainSpec::Nightly { date },
host: self.args.host.clone(),
std_targets: vec![self.args.host.clone(), self.target.clone()],
};
t.std_targets.sort();
t.std_targets.dedup();
let dl_params = DownloadParams::for_nightly(self);
t.install(&self.client, &dl_params)?;
}
}
Ok(())
}
fn bisect(&self) -> anyhow::Result<()> {
if self.is_commit {
let bisection_result = self.bisect_ci()?;
self.print_results(&bisection_result);
} else {
let nightly_bisection_result = self.bisect_nightlies()?;
self.print_results(&nightly_bisection_result);
let nightly_regression =
&nightly_bisection_result.searched[nightly_bisection_result.found];
if let ToolchainSpec::Nightly { date } = nightly_regression.spec {
let previous_date = date.pred();
let working_commit = Bound::Date(previous_date).sha()?;
let bad_commit = Bound::Date(date).sha()?;
eprintln!(
"looking for regression commit between {} and {}",
previous_date.format(YYYY_MM_DD),
date.format(YYYY_MM_DD),
);
let ci_bisection_result = self.bisect_ci_via(&working_commit, &bad_commit)?;
self.print_results(&ci_bisection_result);
print_final_report(self, &nightly_bisection_result, &ci_bisection_result);
}
}
Ok(())
}
}
fn searched_range(
cfg: &Config,
searched_toolchains: &[Toolchain],
) -> (ToolchainSpec, ToolchainSpec) {
let first_toolchain = searched_toolchains.first().unwrap().spec.clone();
let last_toolchain = searched_toolchains.last().unwrap().spec.clone();
match (&first_toolchain, &last_toolchain) {
(ToolchainSpec::Ci { .. }, ToolchainSpec::Ci { .. }) => (first_toolchain, last_toolchain),
_ => {
let start_toolchain = if let Some(Bound::Date(date)) = cfg.args.start {
ToolchainSpec::Nightly { date }
} else {
first_toolchain
};
(
start_toolchain,
ToolchainSpec::Nightly {
date: get_end_date(cfg),
},
)
}
}
}
impl Config {
fn print_results(&self, bisection_result: &BisectionResult) {
let BisectionResult {
searched: toolchains,
dl_spec,
found,
} = bisection_result;
let (start, end) = searched_range(self, toolchains);
eprintln!("searched toolchains {} through {}", start, end);
if toolchains[*found] == *toolchains.last().unwrap() {
eprintln!("checking last toolchain to determine final result");
let t = &toolchains[*found];
let r = match t.install(&self.client, dl_spec) {
Ok(()) => {
let outcome = t.test(self);
remove_toolchain(self, t, dl_spec);
match outcome {
TestOutcome::Baseline => Satisfies::No,
TestOutcome::Regressed => Satisfies::Yes,
}
}
Err(_) => {
let _ = t.remove(dl_spec);
Satisfies::Unknown
}
};
match r {
Satisfies::Yes => {}
Satisfies::No | Satisfies::Unknown => {
eprintln!(
"error: The regression was not found. Expanding the bounds may help."
);
return;
}
}
}
let tc_found = format!("Regression in {}", toolchains[*found]);
eprintln!();
eprintln!();
eprintln!("{}", "*".repeat(80).dimmed().bold());
eprintln!("{}", tc_found.red());
eprintln!("{}", "*".repeat(80).dimmed().bold());
eprintln!();
}
}
fn remove_toolchain(cfg: &Config, toolchain: &Toolchain, dl_params: &DownloadParams) {
if cfg.args.preserve {
let toolchain_dir = cfg.toolchains_path.join(toolchain.rustup_name());
match fs::symlink_metadata(&toolchain_dir) {
Ok(meta) => {
#[cfg(windows)]
let is_junction = {
use std::os::windows::fs::MetadataExt;
(meta.file_attributes() & 1024) != 0
};
#[cfg(not(windows))]
let is_junction = false;
if !meta.file_type().is_symlink() && !is_junction {
return;
}
debug!("removing linked toolchain {}", toolchain);
}
Err(e) => {
debug!(
"remove_toolchain: cannot stat toolchain {}: {}",
toolchain, e
);
return;
}
}
}
if let Err(e) = toolchain.remove(dl_params) {
debug!(
"failed to remove toolchain {} in {}: {}",
toolchain,
cfg.toolchains_path.display(),
e
);
}
}
fn print_final_report(
cfg: &Config,
nightly_bisection_result: &BisectionResult,
ci_bisection_result: &BisectionResult,
) {
let BisectionResult {
searched: nightly_toolchains,
found: nightly_found,
..
} = nightly_bisection_result;
let BisectionResult {
searched: ci_toolchains,
found: ci_found,
..
} = ci_bisection_result;
eprintln!("{}", REPORT_HEADER.dimmed());
eprintln!();
let (start, end) = searched_range(cfg, nightly_toolchains);
eprintln!("searched nightlies: from {} to {}", start, end);
eprintln!("regressed nightly: {}", nightly_toolchains[*nightly_found],);
eprintln!(
"searched commit range: https://github.com/rust-lang/rust/compare/{0}...{1}",
ci_toolchains.first().unwrap(),
ci_toolchains.last().unwrap(),
);
eprintln!(
"regressed commit: https://github.com/rust-lang/rust/commit/{}",
ci_toolchains[*ci_found],
);
eprintln!();
eprintln!("<details>");
eprintln!(
"<summary>bisected with <a href='{}'>cargo-bisect-rustc</a> v{}</summary>",
env!("CARGO_PKG_REPOSITORY"),
env!("CARGO_PKG_VERSION"),
);
eprintln!();
eprintln!();
if let Some(host) = option_env!("HOST") {
eprintln!("Host triple: {}", host);
}
eprintln!("Reproduce with:");
eprintln!("```bash");
eprint!("cargo bisect-rustc ");
for (index, arg) in env::args_os().enumerate() {
if index > 1 {
eprint!("{} ", arg.to_string_lossy());
}
}
eprintln!();
eprintln!("```");
eprintln!("</details>");
}
struct NightlyFinderIter {
start_date: GitDate,
current_date: GitDate,
}
impl NightlyFinderIter {
fn new(start_date: GitDate) -> Self {
Self {
start_date,
current_date: start_date,
}
}
}
impl Iterator for NightlyFinderIter {
type Item = GitDate;
fn next(&mut self) -> Option<GitDate> {
let current_distance = self.start_date - self.current_date;
let jump_length = if current_distance.num_days() < 7 {
2
} else if current_distance.num_days() < 49 {
7
} else {
14
};
self.current_date = self.current_date - Duration::days(jump_length);
Some(self.current_date)
}
}
impl Config {
fn install_and_test(
&self,
t: &Toolchain,
dl_spec: &DownloadParams,
) -> Result<Satisfies, InstallError> {
match t.install(&self.client, dl_spec) {
Ok(()) => {
let outcome = t.test(self);
let r = match outcome {
TestOutcome::Baseline => Satisfies::No,
TestOutcome::Regressed => Satisfies::Yes,
};
eprintln!("RESULT: {}, ===> {}", t, r);
remove_toolchain(self, t, dl_spec);
eprintln!();
Ok(r)
}
Err(error) => {
remove_toolchain(self, t, dl_spec);
Err(error)
}
}
}
fn bisect_to_regression(&self, toolchains: &[Toolchain], dl_spec: &DownloadParams) -> usize {
least_satisfying(toolchains, |t, remaining, estimate| {
eprintln!(
"{remaining} versions remaining to test after this (roughly {estimate} steps)"
);
self.install_and_test(t, dl_spec)
.unwrap_or(Satisfies::Unknown)
})
}
}
fn get_start_date(cfg: &Config) -> Date<Utc> {
if let Some(Bound::Date(date)) = cfg.args.start {
date
} else {
get_end_date(cfg)
}
}
fn get_end_date(cfg: &Config) -> Date<Utc> {
if let Some(Bound::Date(date)) = cfg.args.end {
date
} else {
match (Toolchain::default_nightly(), &cfg.args.start) {
(Some(date), None) => date,
_ => Utc::today(),
}
}
}
fn date_is_future(test_date: Date<Utc>) -> bool {
test_date > Utc::today()
}
impl Config {
fn bisect_nightlies(&self) -> anyhow::Result<BisectionResult> {
if self.args.alt {
bail!("cannot bisect nightlies with --alt: not supported");
}
let dl_spec = DownloadParams::for_nightly(self);
let end_at = Date::from_utc(NaiveDate::from_ymd(2015, 10, 20), Utc);
let mut first_success = None;
let mut nightly_date = get_start_date(self);
let mut last_failure = get_end_date(self);
let has_start = self.args.start.is_some();
if has_start && date_is_future(nightly_date) {
bail!(
"start date must be on or before the current date. received start date request {}",
nightly_date
)
}
if date_is_future(last_failure) {
bail!(
"end date must be on or before the current date. received end date request {}",
nightly_date
)
}
let mut nightly_iter = NightlyFinderIter::new(nightly_date);
while nightly_date > end_at {
let mut t = Toolchain {
spec: ToolchainSpec::Nightly { date: nightly_date },
host: self.args.host.clone(),
std_targets: vec![self.args.host.clone(), self.target.clone()],
};
t.std_targets.sort();
t.std_targets.dedup();
if t.is_current_nightly() {
eprintln!(
"checking {} from the currently installed default nightly \
toolchain as the last failure",
t
);
}
eprintln!("checking the start range to find a passing nightly");
match self.install_and_test(&t, &dl_spec) {
Ok(r) => {
if r == Satisfies::No {
first_success = Some(nightly_date);
break;
} else if has_start {
bail!(
"the start of the range ({}) must not reproduce the regression",
t
);
}
last_failure = nightly_date;
nightly_date = nightly_iter.next().unwrap();
}
Err(InstallError::NotFound { .. }) => {
nightly_date = nightly_date.pred();
eprintln!(
"*** unable to install {}. roll back one day and try again...",
t
);
if has_start {
bail!("could not find {}", t);
}
}
Err(error) => return Err(error.into()),
}
}
let first_success = first_success.context("could not find a nightly that built")?;
let mut t_end = Toolchain {
spec: ToolchainSpec::Nightly { date: last_failure },
host: self.args.host.clone(),
std_targets: vec![self.args.host.clone(), self.target.clone()],
};
t_end.std_targets.sort();
t_end.std_targets.dedup();
eprintln!("checking the end range to verify it does not pass");
let result_nightly = self.install_and_test(&t_end, &dl_spec)?;
if result_nightly == Satisfies::No {
bail!(
"the end of the range ({}) does not reproduce the regression",
t_end
);
}
let toolchains = toolchains_between(
self,
ToolchainSpec::Nightly {
date: first_success,
},
ToolchainSpec::Nightly { date: last_failure },
);
let found = self.bisect_to_regression(&toolchains, &dl_spec);
Ok(BisectionResult {
dl_spec,
searched: toolchains,
found,
})
}
}
fn toolchains_between(cfg: &Config, a: ToolchainSpec, b: ToolchainSpec) -> Vec<Toolchain> {
match (a, b) {
(ToolchainSpec::Nightly { date: a }, ToolchainSpec::Nightly { date: b }) => {
let mut toolchains = Vec::new();
let mut date = a;
let mut std_targets = vec![cfg.args.host.clone(), cfg.target.clone()];
std_targets.sort();
std_targets.dedup();
while date <= b {
let t = Toolchain {
spec: ToolchainSpec::Nightly { date },
host: cfg.args.host.clone(),
std_targets: std_targets.clone(),
};
toolchains.push(t);
date = date.succ();
}
toolchains
}
_ => unimplemented!(),
}
}
impl Config {
fn bisect_ci(&self) -> anyhow::Result<BisectionResult> {
eprintln!("bisecting ci builds");
let start = if let Some(Bound::Commit(ref sha)) = self.args.start {
sha
} else {
EPOCH_COMMIT
};
let end = if let Some(Bound::Commit(ref sha)) = self.args.end {
sha
} else {
"origin/master"
};
eprintln!("starting at {}, ending at {}", start, end);
self.bisect_ci_via(start, end)
}
fn bisect_ci_via(&self, start_sha: &str, end_ref: &str) -> anyhow::Result<BisectionResult> {
let access = self.args.access.repo();
let end_sha = access.commit(end_ref)?.sha;
let commits = access.commits(start_sha, &end_sha)?;
assert_eq!(commits.last().expect("at least one commit").sha, end_sha);
commits.iter().zip(commits.iter().skip(1)).all(|(a, b)| {
let sorted_by_date = a.date <= b.date;
assert!(
sorted_by_date,
"commits must chronologically ordered,\
but {:?} comes after {:?}",
a, b
);
sorted_by_date
});
for (j, commit) in commits.iter().enumerate() {
eprintln!(
" commit[{}] {}: {}",
j,
commit.date,
commit.summary.split('\n').next().unwrap()
)
}
self.bisect_ci_in_commits(start_sha, &end_sha, commits)
}
fn bisect_ci_in_commits(
&self,
start: &str,
end: &str,
mut commits: Vec<Commit>,
) -> anyhow::Result<BisectionResult> {
let dl_spec = DownloadParams::for_ci(self);
commits.retain(|c| Utc::today() - c.date < Duration::days(167));
if commits.is_empty() {
bail!(
"no CI builds available between {} and {} within last 167 days",
start,
end
);
}
if let Some(c) = commits.last() {
if end != "origin/master" && !c.sha.starts_with(end) {
bail!("expected to end with {}, but ended with {}", end, c.sha);
}
}
eprintln!("validated commits found, specifying toolchains");
eprintln!();
let toolchains = commits
.into_iter()
.map(|commit| {
let mut t = Toolchain {
spec: ToolchainSpec::Ci {
commit: commit.sha,
alt: self.args.alt,
},
host: self.args.host.clone(),
std_targets: vec![self.args.host.clone(), self.target.clone()],
};
t.std_targets.sort();
t.std_targets.dedup();
t
})
.collect::<Vec<_>>();
if !toolchains.is_empty() {
eprintln!("checking the start range to verify it passes");
let start_range_result = self.install_and_test(&toolchains[0], &dl_spec)?;
if start_range_result == Satisfies::Yes {
bail!(
"the commit at the start of the range ({}) includes the regression",
&toolchains[0]
);
}
eprintln!("checking the end range to verify it does not pass");
let end_range_result =
self.install_and_test(&toolchains[toolchains.len() - 1], &dl_spec)?;
if end_range_result == Satisfies::No {
bail!(
"the commit at the end of the range ({}) does not reproduce the regression",
&toolchains[toolchains.len() - 1]
);
}
}
let found = self.bisect_to_regression(&toolchains, &dl_spec);
Ok(BisectionResult {
searched: toolchains,
found,
dl_spec,
})
}
}
#[derive(Clone)]
struct BisectionResult {
searched: Vec<Toolchain>,
found: usize,
dl_spec: DownloadParams,
}
fn main() {
if let Err(err) = run() {
match err.downcast::<ExitError>() {
Ok(ExitError(code)) => process::exit(code),
Err(err) => {
let error_str = "ERROR:".red().bold();
eprintln!("{} {:?}", error_str, err);
process::exit(1);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_bounds_valid_bounds() {
let date1 = chrono::Utc::today().pred();
let date2 = chrono::Utc::today().pred();
assert!(check_bounds(&Some(Bound::Date(date1)), &Some(Bound::Date(date2))).is_ok());
}
#[test]
fn test_check_bounds_invalid_start_after_end() {
let start = chrono::Utc::today();
let end = chrono::Utc::today().pred();
assert!(check_bounds(&Some(Bound::Date(start)), &Some(Bound::Date(end))).is_err());
}
#[test]
fn test_check_bounds_invalid_start_after_current() {
let start = chrono::Utc::today().succ();
let end = chrono::Utc::today();
assert!(check_bounds(&Some(Bound::Date(start)), &Some(Bound::Date(end))).is_err());
}
#[test]
fn test_check_bounds_invalid_end_after_current() {
let start = chrono::Utc::today();
let end = chrono::Utc::today().succ();
assert!(check_bounds(&Some(Bound::Date(start)), &Some(Bound::Date(end))).is_err());
}
#[test]
fn test_nightly_finder_iterator() {
let start_date = Date::from_utc(NaiveDate::from_ymd(2019, 01, 01), Utc);
let iter = NightlyFinderIter::new(start_date);
for (date, i) in iter.zip([2, 4, 6, 8, 15, 22, 29, 36, 43, 50, 64, 78]) {
assert_eq!(start_date - Duration::days(i), date)
}
}
#[test]
fn test_validate_dir() {
let current_dir = ".";
assert!(validate_dir(current_dir).is_ok());
let main = "src/main.rs";
assert!(
validate_dir(main).is_err(),
"{}",
validate_dir(main).unwrap_err()
)
}
}