diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 16ff1da7..bcad10ed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,4 +15,4 @@ regress:nightly: - rustup component add rustfmt-preview - cd ci/svd2rust-regress - rm -rf ./output - - cargo run --release -- --long-test --format --verbose + - cargo run --release -- --long-test --format --verbose --nightly diff --git a/ci/svd2rust-regress/src/errors.rs b/ci/svd2rust-regress/src/errors.rs index a9683ee7..30408a1b 100644 --- a/ci/svd2rust-regress/src/errors.rs +++ b/ci/svd2rust-regress/src/errors.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; error_chain!{ errors { - ProcessFailed(command: String, stderr: Option, stdout: Option) { + ProcessFailed(command: String, stderr: Option, stdout: Option, previous_processes_stderr: Vec) { description("Process Failed") display("Process Failed - {}", command) } diff --git a/ci/svd2rust-regress/src/main.rs b/ci/svd2rust-regress/src/main.rs index 4791934e..5959c126 100644 --- a/ci/svd2rust-regress/src/main.rs +++ b/ci/svd2rust-regress/src/main.rs @@ -32,18 +32,23 @@ struct Opt { /// (which must be already built) #[structopt(short = "p", long = "svd2rust-path", parse(from_os_str))] bin_path: Option, + + // TODO: Consider using the same strategy cargo uses for passing args to rustc via `--` + /// Run svd2rust with `--nightly` + #[structopt(long = "nightly")] + nightly: bool, /// Filter by chip name, case sensitive, may be combined with other filters - #[structopt(short = "c", long = "chip")] - chip: Option, + #[structopt(short = "c", long = "chip", raw(validator = "validate_chips"))] + chip: Vec, /// Filter by manufacturer, case sensitive, may be combined with other filters - #[structopt(short = "m", long = "manufacturer")] + #[structopt(short = "m", long = "manufacturer", raw(validator = "validate_manufacturer"))] mfgr: Option, /// Filter by architecture, case sensitive, may be combined with other filters /// Options are: "CortexM", "RiscV", and "Msp430" - #[structopt(short = "a", long = "architecture")] + #[structopt(short = "a", long = "architecture", raw(validator = "validate_architecture"))] arch: Option, /// Include tests expected to fail (will cause a non-zero return code) @@ -54,19 +59,50 @@ struct Opt { #[structopt(short = "f", long = "format")] format: bool, + /// Print all available test using the specified filters + #[structopt(long = "list")] + list: bool, + /// Path to an `rustfmt` binary, relative or absolute. /// Defaults to `$(rustup which rustfmt)` #[structopt(long = "rustfmt_bin_path", parse(from_os_str))] rustfmt_bin_path: Option, + /// Specify what rustup toolchain to use when compiling chip(s) + #[structopt(long = "toolchain", env = "RUSTUP_TOOLCHAIN")] + rustup_toolchain: Option, + /// Use verbose output #[structopt(long = "verbose", short = "v", parse(from_occurrences))] verbose: u8, // TODO: Specify smaller subset of tests? Maybe with tags? - // TODO: Early fail // TODO: Compile svd2rust? } +fn validate_chips(s: String) -> Result<(), String>{ + if tests::TESTS.iter().any(|t| t.chip == s) { + Ok(()) + } else { + Err(format!("Chip `{}` is not a valid value", s)) + } +} + +fn validate_architecture(s: String) -> Result<(), String>{ + if tests::TESTS.iter().any(|t| format!("{:?}", t.arch) == s) { + Ok(()) + } else { + Err(format!("Architecture `{}` is not a valid value", s)) + } +} + +fn validate_manufacturer(s: String) -> Result<(), String>{ + if tests::TESTS.iter().any(|t| format!("{:?}", t.mfgr) == s) { + Ok(()) + } else { + Err(format!("Manufacturer `{}` is not a valid value", s)) + } +} + /// Validate any assumptions made by this program fn validate_tests(tests: &[&tests::TestCase]) { use std::collections::HashSet; @@ -88,6 +124,18 @@ fn validate_tests(tests: &[&tests::TestCase]) { } } +fn read_file(path: &PathBuf, buf: &mut String) { + if buf.is_empty() { + buf.push_str(&format!("{}\n", path.display())); + } else { + buf.push_str(&format!("\n{}\n", path.display())); + } + File::open(path) + .expect("Couldn't open file") + .read_to_string(buf) + .expect("Couldn't read file to string"); +} + fn main() { let opt = Opt::from_args(); @@ -138,6 +186,12 @@ fn main() { default_rustfmt.as_ref() } }; + + // Set RUSTUP_TOOLCHAIN if needed + if let Some(toolchain) = &opt.rustup_toolchain { + ::std::env::set_var("RUSTUP_TOOLCHAIN", toolchain); + } + // collect enabled tests let tests = tests::TESTS .iter() @@ -161,8 +215,8 @@ fn main() { }) // Specify chip - note: may match multiple .filter(|t| { - if let Some(ref chip) = opt.chip { - chip == t.chip + if !opt.chip.is_empty() { + opt.chip.iter().any(|c| c == t.chip) } else { true } @@ -171,6 +225,15 @@ fn main() { .filter(|t| opt.bad_tests || t.should_pass) .collect::>(); + if opt.list { + // FIXME: Prettier output + eprintln!("{:?}", tests.iter().map(|t| t.name()).collect::>()); + exit(0); + } + if tests.is_empty() { + eprintln!("No tests run, you might want to use `--bad-tests` and/or `--long-test`"); + } + let any_fails = AtomicBool::new(false); // TODO: It would be more efficient to reuse directories, so we don't @@ -178,29 +241,39 @@ fn main() { tests.par_iter().for_each(|t| { let start = Instant::now(); - match svd_test::test(t, &bin_path, rustfmt_bin_path) { - Ok(()) => { - // TODO: If verbosity is > 1, print every logged stderr - eprintln!( - "Passed: {} - {} seconds", - t.name(), - start.elapsed().as_secs() - ); + match svd_test::test(t, &bin_path, rustfmt_bin_path, opt.nightly, opt.verbose) { + Ok(s) => { + if let Some(stderrs) = s { + let mut buf = String::new(); + for stderr in stderrs { + read_file(&stderr, &mut buf); + } + eprintln!( + "Passed: {} - {} seconds\n{}", + t.name(), + start.elapsed().as_secs(), + buf + ); + } else { + eprintln!( + "Passed: {} - {} seconds", + t.name(), + start.elapsed().as_secs() + ); + } } Err(e) => { any_fails.store(true, Ordering::Release); let additional_info = if opt.verbose > 0 { match e.kind() { - &errors::ErrorKind::ProcessFailed(ref command, _, Some(ref stderr)) - if command == "cargo check" => - { - let mut buf = String::new(); - // Unwrap is safe - File::open(stderr) - .expect("Couldn't open file") - .read_to_string(&mut buf) - .expect("Couldn't read file to string"); - buf.insert_str(0, &format!("\n---{:?}---\n", stderr.as_os_str())); + &errors::ErrorKind::ProcessFailed(_, _, Some(ref stderr), ref previous_processes_stderr) => { + let mut buf = String::new(); + if opt.verbose > 1 { + for stderr in previous_processes_stderr { + read_file(&stderr, &mut buf); + } + } + read_file(&stderr, &mut buf); buf } _ => "".into(), diff --git a/ci/svd2rust-regress/src/svd_test.rs b/ci/svd2rust-regress/src/svd_test.rs index 65d98d12..a90218c5 100644 --- a/ci/svd2rust-regress/src/svd_test.rs +++ b/ci/svd2rust-regress/src/svd_test.rs @@ -1,16 +1,19 @@ use errors::*; use reqwest; -use std::fs::{remove_dir_all, File, OpenOptions}; +use std::fs::{self, File, OpenOptions}; use std::io::prelude::*; use std::path::PathBuf; use std::process::{Command, Output}; use tests::TestCase; -static CRATES_ALL: &[&str] = &["bare-metal = \"0.1.0\"", "vcell = \"0.1.0\""]; +static CRATES_ALL: &[&str] = &["bare-metal = \"0.2.0\"", "vcell = \"0.1.0\""]; static CRATES_MSP430: &[&str] = &["msp430 = \"0.1.0\""]; -static CRATES_CORTEX_M: &[&str] = &["cortex-m = \"0.4.0\"", "cortex-m-rt = \"0.3.0\""]; +static CRATES_CORTEX_M: &[&str] = &["cortex-m = \"0.5.0\"", "cortex-m-rt = \"0.5.0\""]; static CRATES_RISCV: &[&str] = &["riscv = \"0.4.0\"", "riscv-rt = \"0.4.0\""]; static PROFILE_ALL: &[&str] = &["[profile.dev]", "incremental = false"]; +static FEATURES_ALL: &[&str] = &["[features]"]; +static FEATURES_CORTEX_M: &[&str] = &["const-fn = [\"bare-metal/const-fn\", \"cortex-m/const-fn\"]"]; +static FEATURES_EMPTY: &[&str] = &[]; fn path_helper(input: &[&str]) -> PathBuf { input.iter().collect() @@ -39,6 +42,7 @@ trait CommandHelper { name: &str, stdout: Option<&PathBuf>, stderr: Option<&PathBuf>, + previous_processes_stderr: &Vec, ) -> Result<()>; } @@ -49,6 +53,7 @@ impl CommandHelper for Output { name: &str, stdout: Option<&PathBuf>, stderr: Option<&PathBuf>, + previous_processes_stderr: &Vec, ) -> Result<()> { if let Some(out) = stdout { let out_payload = String::from_utf8_lossy(&self.stdout); @@ -62,7 +67,11 @@ impl CommandHelper for Output { if cant_fail && !self.status.success() { return Err( - ErrorKind::ProcessFailed(name.into(), stdout.cloned(), stderr.cloned()).into(), + ErrorKind::ProcessFailed(name.into(), + stdout.cloned(), + stderr.cloned(), + previous_processes_stderr.clone(), + ).into() ); } @@ -70,7 +79,7 @@ impl CommandHelper for Output { } } -pub fn test(t: &TestCase, bin_path: &PathBuf, rustfmt_bin_path: Option<&PathBuf>) -> Result<()> { +pub fn test(t: &TestCase, bin_path: &PathBuf, rustfmt_bin_path: Option<&PathBuf>, nightly: bool, verbosity: u8) -> Result>> { let user = match ::std::env::var("USER") { Ok(val) => val, Err(_) => "rusttester".into(), @@ -78,7 +87,15 @@ pub fn test(t: &TestCase, bin_path: &PathBuf, rustfmt_bin_path: Option<&PathBuf> // Remove the existing chip directory, if it exists let chip_dir = path_helper(&["output", &t.name()]); - let _ = remove_dir_all(&chip_dir); + if let Err(err) = fs::remove_dir_all(&chip_dir) { + match err.kind() { + ::std::io::ErrorKind::NotFound => (), + _ => Err(err).chain_err(|| "While removing chip directory")? + } + } + + // Used to build the output from stderr for -v and -vv* + let mut process_stderr_paths: Vec = vec![]; // Create a new cargo project. It is necesary to set the user, otherwise // cargo init will not work (when running in a container with no user set) @@ -92,7 +109,7 @@ pub fn test(t: &TestCase, bin_path: &PathBuf, rustfmt_bin_path: Option<&PathBuf> .arg(&chip_dir) .output() .chain_err(|| "Failed to cargo init")? - .capture_outputs(true, "cargo init", None, None)?; + .capture_outputs(true, "cargo init", None, None, &vec![])?; // Add some crates to the Cargo.toml of our new project let svd_toml = path_helper_base(&chip_dir, &["Cargo.toml"]); @@ -106,11 +123,16 @@ pub fn test(t: &TestCase, bin_path: &PathBuf, rustfmt_bin_path: Option<&PathBuf> let crates = CRATES_ALL .iter() .chain(match &t.arch { - &CortexM => CRATES_CORTEX_M.iter(), - &RiscV => CRATES_RISCV.iter(), - &Msp430 => CRATES_MSP430.iter(), + CortexM => CRATES_CORTEX_M.iter(), + RiscV => CRATES_RISCV.iter(), + Msp430 => CRATES_MSP430.iter(), }) - .chain(PROFILE_ALL.iter()); + .chain(PROFILE_ALL.iter()) + .chain(FEATURES_ALL.iter()) + .chain(match &t.arch { + CortexM => FEATURES_CORTEX_M.iter(), + _ => FEATURES_EMPTY.iter(), + }); for c in crates { writeln!(file, "{}", c).chain_err(|| "Failed to append to file!")?; @@ -129,6 +151,7 @@ pub fn test(t: &TestCase, bin_path: &PathBuf, rustfmt_bin_path: Option<&PathBuf> file_helper(&svd, &svd_file)?; // Generate the lib.rs from the SVD file using the specified `svd2rust` binary + // If the architecture is cortex-m we move the generated lib.rs file to src/ let lib_rs_file = path_helper_base(&chip_dir, &["src", "lib.rs"]); let svd2rust_err_file = path_helper_base(&chip_dir, &["svd2rust.err.log"]); let target = match &t.arch { @@ -136,37 +159,66 @@ pub fn test(t: &TestCase, bin_path: &PathBuf, rustfmt_bin_path: Option<&PathBuf> &Msp430 => "msp430", &RiscV => "riscv", }; + let mut svd2rust_bin = Command::new(bin_path); + if nightly { + svd2rust_bin.arg("--nightly"); + } - Command::new(bin_path) + let output = svd2rust_bin .args(&["-i", &chip_svd]) .args(&["--target", &target]) .current_dir(&chip_dir) .output() - .chain_err(|| "failed to execute process")? - .capture_outputs( - true, - "svd2rust", - Some(&lib_rs_file), - Some(&svd2rust_err_file), - )?; + .chain_err(|| "failed to execute process")?; + output.capture_outputs( + true, + "svd2rust", + if t.arch != CortexM { Some(&lib_rs_file) } else { None }, // use Option.filter + Some(&svd2rust_err_file), + &vec![], + )?; + process_stderr_paths.push(svd2rust_err_file); + + match &t.arch { + &CortexM => { + // TODO: Give error the path to stderr + fs::rename(path_helper_base(&chip_dir, &["lib.rs"]), &lib_rs_file).chain_err(|| "While moving lib.rs file")? + } + _ => (), + } + let rustfmt_err_file = path_helper_base(&chip_dir, &["rustfmt.err.log"]); if let Some(rustfmt_bin_path) = rustfmt_bin_path { // Run `cargo fmt`, capturing stderr to a log file - let fmt_err_file = path_helper_base(&chip_dir, &["rustfmt.err.log"]); - Command::new(rustfmt_bin_path) + + let output = Command::new(rustfmt_bin_path) .arg(lib_rs_file) .output() - .chain_err(|| "failed to format")? - .capture_outputs(false, "rustfmt", None, Some(&fmt_err_file))?; + .chain_err(|| "failed to format")?; + output.capture_outputs( + false, + "rustfmt", + None, + Some(&rustfmt_err_file), + &process_stderr_paths, + )?; + process_stderr_paths.push(rustfmt_err_file); + } // Run `cargo check`, capturing stderr to a log file let cargo_check_err_file = path_helper_base(&chip_dir, &["cargo-check.err.log"]); - Command::new("cargo") + let output = Command::new("cargo") .arg("check") .current_dir(&chip_dir) .output() - .chain_err(|| "failed to check")? - .capture_outputs(true, "cargo check", None, Some(&cargo_check_err_file))?; - - Ok(()) + .chain_err(|| "failed to check")?; + output.capture_outputs( + true, + "cargo check", + None, + Some(&cargo_check_err_file), + &process_stderr_paths, + )?; + process_stderr_paths.push(cargo_check_err_file); + Ok(if verbosity > 1 {Some(process_stderr_paths)} else {None}) } diff --git a/ci/svd2rust-regress/src/tests.rs b/ci/svd2rust-regress/src/tests.rs index f1e05d88..32d382cc 100644 --- a/ci/svd2rust-regress/src/tests.rs +++ b/ci/svd2rust-regress/src/tests.rs @@ -1,6 +1,6 @@ use inflections::Inflect; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Architecture { // TODO: Coming soon! // Avr, @@ -709,9 +709,9 @@ pub const TESTS: &'static [&'static TestCase] = &[ arch: CortexM, mfgr: Atmel, chip: "ATSAMD21G18A", - svd_url: None, - should_pass: false, - run_when: Never, + svd_url: Some("https://raw.githubusercontent.com/wez/atsamd21-rs/master/svd/ATSAMD21G18A.svd"), + should_pass: true, + run_when: NotShort, }, &TestCase { arch: CortexM,