diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..7e34a3c --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[alias] +dep-tests = ["run", "--manifest-path", "./dep-tests/Cargo.toml", "--"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a117259..d881bde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,48 @@ jobs: command: fmt args: --all -- --check + dep-tests: + strategy: + fail-fast: false + + name: dep-tests + runs-on: ubuntu-18.04 + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: rust-toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: 1.38.0-x86_64-unknown-linux-gnu + override: true + profile: default + + - name: '`cargo fmt --all --manifest-path ./dep-tests/Cargo.toml -- --check`' + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all --manifest-path ./dep-tests/Cargo.toml -- --check + + - name: '`cargo clippy --manifest-path ./dep-tests/Cargo.toml -- -D warnings`' + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --manifest-path ./dep-tests/Cargo.toml -- -D warnings + + - name: '`cargo test --no-fail-fast --manifest-path ./dep-tests/Cargo.toml`' + uses: actions-rs/cargo@v1 + with: + command: test + args: --no-fail-fast --manifest-path ./dep-tests/Cargo.toml + + - name: '`cargo dep-tests --all-features -d 1`' + uses: actions-rs/cargo@v1 + with: + command: dep-tests + args: --all-features -d 1 + build: strategy: fail-fast: false diff --git a/.gitignore b/.gitignore index c7f4426..74250da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target/ +/dep-tests/Cargo.lock +/dep-tests/target/ **/*.rs.bk **/*~ diff --git a/Cargo.toml b/Cargo.toml index 10d4299..81a4bc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ name = "atcoder-rust-base" version = "0.1.0" edition = "2018" +[workspace] +exclude = ["./dep-tests"] + [[bin]] name = "main" path = "src/main.rs" diff --git a/cargo-generate.toml b/cargo-generate.toml new file mode 100644 index 0000000..1034944 --- /dev/null +++ b/cargo-generate.toml @@ -0,0 +1,2 @@ +[template] +exclude = [".github", ".cargo", "dep-tests"] diff --git a/dep-tests.toml b/dep-tests.toml new file mode 100644 index 0000000..1b86cdb --- /dev/null +++ b/dep-tests.toml @@ -0,0 +1,17 @@ +exclude = [ + "c2-chacha:0.2.3", # よくわからない理由でビルドに失敗する + "derive_more:0.99.2", # 必要なファイルがexcludeされている + "jemallocator:0.3.2", # よくわからない理由でビルドに失敗する + "libm:0.1.4", # `#![deny(warnings)]` + "mac:0.1.1", # `#![deny(warnings)]` + "nom:5.0.1", # 必要なファイルがexcludeされている + "num-rational:0.2.2", # よくわからない理由でビルドに失敗する + "petgraph:0.4.13", # よくわからない理由で実行時に失敗する + "primal:0.2.3", # 最終リリース日が古すぎて"normalizing"が行なわれておらず、workspace membersが相対パスのまま + "primal-estimate:0.2.1", # 最終リリース日が古すぎて"normalizing"が行なわれておらず、workspace membersが相対パスのまま + "proc-macro2:1.0.6", # よくわからない理由でビルドに失敗する + "rand_core:0.3.1", # よくわからない理由でビルドに失敗する + # "smallvec:1.0.0", # 成功はするが謎のエラーが表示される + "syn:0.15.44", # よくわからない理由でビルドに失敗する + "syn:1.0.8", # よくわからない理由でビルドに失敗する +] diff --git a/dep-tests/Cargo.toml b/dep-tests/Cargo.toml new file mode 100644 index 0000000..2ddde99 --- /dev/null +++ b/dep-tests/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "dep-tests" +version = "0.0.0" +authors = [] +edition = "2018" +description = "Run all of the tests in the dependency graph." +publish = false + +[dependencies] +cargo = "0.40.0" +failure = "0.1.6" +fs_extra = "1.1.0" +itertools = "0.8.2" +maplit = "1.0.2" +once_cell = "1.2.0" +serde = { version = "1.0.103", features = ["derive"] } +structopt = "0.3.5" +toml = "0.5.5" diff --git a/dep-tests/src/main.rs b/dep-tests/src/main.rs new file mode 100644 index 0000000..8d1c591 --- /dev/null +++ b/dep-tests/src/main.rs @@ -0,0 +1,337 @@ +use cargo::core::compiler::{self, CompileMode, TargetInfo}; +use cargo::core::dependency; +use cargo::core::package::{Package, PackageSet}; +use cargo::core::resolver::ResolveOpts; +use cargo::core::shell::{Shell, Verbosity}; +use cargo::core::{PackageId, PackageIdSpec, Resolve, Workspace}; +use cargo::ops::{Packages, TestOptions}; +use cargo::util::command_prelude::{App, AppExt as _, AppSettings, ArgMatchesExt as _}; +use cargo::{CliError, CliResult}; +use failure::{format_err, Fail as _, Fallible}; +use itertools::Itertools as _; +use maplit::btreeset; +use once_cell::sync::Lazy; +use serde::Deserialize; +use structopt::StructOpt; + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::env; +use std::fmt::{Display, Write as _}; +use std::num::NonZeroUsize; +use std::path::{Path, PathBuf}; +use std::process::ExitStatus; + +static MANIFEST_PATH: &str = "./Cargo.toml"; +static CONFIG_PATH: &str = "./dep-tests.toml"; + +fn main() { + debug_assert_eq!( + env::current_dir() + .ok() + .and_then(|d| d.file_name()?.to_str().map(ToOwned::to_owned)), + Some("atcoder-rust-base".to_owned()), + "The cwd should be \"atcoder-rust-base\"", + ); + + let matches = Opt::clap() + .get_matches_safe() + .unwrap_or_else(|e| cargo::exit_with_error(e.into(), &mut Shell::new())); + let opt = Opt::from_clap(&matches); + + let mut config = cargo::Config::default() + .unwrap_or_else(|e| cargo::exit_with_error(e.into(), &mut Shell::new())); + + if let Err(err) = opt.run(&mut config) { + cargo::exit_with_error(err, &mut config.shell()); + } +} + +#[derive(StructOpt, Debug)] +#[structopt(about, setting(AppSettings::DeriveDisplayOrder))] +struct Opt { + #[structopt(long, help("Activate all available features"))] + all_features: bool, + #[structopt(long, help("Do not activate the `default` feature"))] + no_default_features: bool, + #[structopt(long, help("Require Cargo.lock and cache are up to date"))] + frozen: bool, + #[structopt(long, help("Require Cargo.lock is up to date"))] + locked: bool, + #[structopt(long, help("Run without accessing the network"))] + offline: bool, + #[structopt( + short, + long, + value_name("SPEC"), + number_of_values(1), + parse(try_from_str = PackageIdSpec::parse), + help("**Dependency** to run test for") + )] + package: Vec, + #[structopt( + long, + value_name("FEATURES"), + min_values(1), + help("Space-separated list of features to activate") + )] + features: Vec, + #[structopt(long, value_name("WHEN"), help("Coloring: auto, always, never"))] + color: Option, + #[structopt( + short, + long, + value_name("N"), + help("How deep in the dependency chain to search") + )] + depth: Option, + #[structopt(long, value_name("N"), help("Skips the first N packages"))] + skip: Option, + #[structopt( + default_value_os({ + static DEFAULT: Lazy = + Lazy::new(|| env::temp_dir().join("atcoder-rust-base-dep-tests")); + DEFAULT.as_ref() + }), + help("Directory to run tests") + )] + dir: PathBuf, +} + +impl Opt { + fn run(&self, config: &mut cargo::Config) -> CliResult { + config.configure( + 0, + None, + &self.color, + self.frozen, + self.locked, + self.offline, + &None, + &[], + )?; + + let DepTestsConfig { exclude } = DepTestsConfig::load(config)?; + + let ws = Workspace::new(&config.cwd().join(MANIFEST_PATH), config)?; + + let (packages, resolve) = cargo::ops::resolve_ws_with_opts( + &ws, + ResolveOpts::new( + false, + &self.features, + self.all_features, + self.no_default_features, + ), + &Packages::Default.to_package_id_specs(&ws)?, + )?; + + let (normal_deps, packages) = ( + find_normal_deps(&ws, &resolve, self.depth)?, + filter_packages(&packages, &self.package, &exclude)?, + ); + + let wss = setup_workspaces(config, &self.dir, &normal_deps, &packages)?; + for (i, (id, ws)) in wss.iter().enumerate().skip(self.skip()) { + let mut msg = format!("Testing `{}` ({}/{})", id, i + 1, wss.len()); + if let Some(skip) = self.skip { + write!(msg, " (skipping the first {} package(s))", skip).unwrap(); + } + config.shell().info(msg)?; + run_tests(&resolve, *id, ws)?; + } + + config.shell().info("Successful!").map_err(Into::into) + } + + fn skip(&self) -> usize { + self.skip.map(NonZeroUsize::get).unwrap_or_default() + } +} + +fn find_normal_deps( + ws: &Workspace, + resolve: &Resolve, + depth: Option, +) -> Fallible> { + let rustc = ws.config().load_global_rustc(Some(&ws))?; + let host_triple = &rustc.host; + let target_info = TargetInfo::new( + ws.config(), + &Some(host_triple.clone()), + &rustc, + compiler::Kind::Host, + )?; + + let member = ws.current()?.package_id(); + let mut normal_deps = btreeset!(member); + let mut cur = btreeset!(member); + let mut depth = depth.map(NonZeroUsize::get); + while !cur.is_empty() && depth.map_or(true, |d| d > 0) { + let mut next = btreeset!(); + for from in cur { + for (to, deps) in resolve.deps(from) { + for dep in deps { + if dep.kind() == dependency::Kind::Normal // `dep` may be a build-dependency. + && dep + .platform() + .as_ref() + .map_or(true, |p| p.matches(host_triple, target_info.cfg())) + && normal_deps.insert(to) + { + next.insert(to); + } + } + } + } + cur = next; + depth = depth.map(|d| d - 1); + } + for member in ws.members() { + normal_deps.remove(&member.package_id()); + } + Ok(normal_deps) +} + +fn filter_packages<'a>( + packages: &'a PackageSet, + include: &[PackageIdSpec], + exclude: &HashSet, +) -> Fallible> { + let packages = packages.get_many(packages.package_ids())?; + Ok(packages + .into_iter() + .map(|p| (p.package_id(), p)) + .filter(|&(id, _)| { + (include.is_empty() || include.iter().any(|s| s.matches(id))) + && !exclude.iter().any(|s| s.matches(id)) + }) + .collect::>()) +} + +fn setup_workspaces<'cfg>( + config: &'cfg cargo::Config, + root: &Path, + normal_deps: &BTreeSet, + packages: &HashMap, +) -> Fallible>> { + let wss = normal_deps + .iter() + .flat_map(|d| packages.get(d)) + .map(|dep| { + let src = dep.root(); + let dst = root.join(src.file_name().unwrap_or_default()); + let dst = cargo::util::paths::normalize_path(&if dst.is_relative() { + config.cwd().join(dst) + } else { + dst + }); + + config + .shell() + .info(&format!("Copying {} to {}", src.display(), dst.display()))?; + + fs_extra::dir::copy( + src, + &dst, + &fs_extra::dir::CopyOptions { + skip_exist: true, + copy_inside: true, + ..fs_extra::dir::CopyOptions::new() + }, + )?; + + let ws = Workspace::new(&dst.join("Cargo.toml"), config)?; + Ok((dep.package_id(), ws)) + }) + .collect::>>()?; + + for ws in wss.values() { + let src = cargo::util::paths::normalize_path(&config.cwd().join("Cargo.lock")); + let dst = ws.root().join("Cargo.lock"); + + config + .shell() + .info(&format!("Copying {} to {}", src.display(), dst.display()))?; + + fs_extra::file::copy( + src, + dst, + &fs_extra::file::CopyOptions { + overwrite: true, + ..fs_extra::file::CopyOptions::new() + }, + )?; + } + Ok(wss) +} + +fn run_tests(resolve: &Resolve, id: PackageId, ws: &Workspace) -> CliResult { + // `ws.current()?.package_id().source_id()` differs to `id.source_id()`. + + let compile_opts = { + let features = resolve.features(id); + let mut args = vec!["".to_owned(), "--no-default-features".to_owned()]; + if !features.is_empty() { + args.push("--features".to_owned()); + args.push(features.iter().join(" ")); + } + App::new("") + .arg_features() + .get_matches_from_safe(args)? + .compile_options(ws.config(), CompileMode::Test, Some(ws))? + }; + + let test_opts = TestOptions { + compile_opts, + no_run: false, + no_fail_fast: false, + }; + + match cargo::ops::run_tests(&ws, &test_opts, &[])? { + None => Ok(()), + Some(err) => Err(match err.exit.as_ref().and_then(ExitStatus::code) { + Some(code) => { + let hint = format_err!("{}", err.hint(&ws, &test_opts.compile_opts)); + CliError::new(err.context(hint).into(), code) + } + None => CliError::new(err.into(), 101), + }), + } +} + +trait ShellExt { + fn info(&mut self, message: impl Display) -> Fallible<()>; +} + +impl ShellExt for Shell { + fn info(&mut self, message: impl Display) -> Fallible<()> { + if self.verbosity() == Verbosity::Quiet { + return Ok(()); + } + let message = format!( + "{} {}\n", + if self.supports_color() { + "\x1B[1m\x1B[36minfo:\x1B[0m" + } else { + "info:" + }, + message, + ); + self.print_ansi(message.as_ref()) + } +} + +#[derive(Deserialize, Debug)] +struct DepTestsConfig { + exclude: HashSet, +} + +impl DepTestsConfig { + fn load(config: &cargo::Config) -> Fallible { + let path = cargo::util::paths::normalize_path(&config.cwd().join(CONFIG_PATH)); + let toml = cargo::util::paths::read(&path)?; + let this = toml::from_str(&toml)?; + config.shell().info(format!("Loaded {}", path.display()))?; + Ok(this) + } +}