/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{Display, Error, Formatter}; use std::fs::{create_dir, File}; use std::io::Write; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; use std::process::Command; use std::thread;
use anyhow::{anyhow, Result}; use log::{error, info};
pubstruct Test { /// A human readable identifier for the `from` build under test. Eg: a buildid /// or app version. pub id: String, /// The `from` installer to use as a starting point for the test pub from_installer: PathBuf, /// The locale of the `from` installer (needed to fully unpack it) pub locale: String, /// The MAR file to apply to the unpacked `from` installer pub mar: PathBuf, /// The package containing the updater to use when applying the MAR pub updater_package: PathBuf,
}
#[derive(Debug, PartialEq)] pubenum TestResult {
Pass, /// Updater did not run succesfully.
UpdateStatusErr, /// UpdateSettings.framework is missing in the updated build (mac only)
UpdateSettingsMissingErr, /// ChannelPrefs.framework is missing in the updated build (mac only)
ChannelPrefsMissingErr, /// Unacceptable differences found between the MAR and installer
DifferencesFoundErr, /// Error running `diff`
DiffErr, /// Novel error occurred - such things may benefit from distinct results /// or to be converted to SetupErr when encountered!
UnknownErr, /// Setup problems are not results in the same way that specific failure /// modes are; we group them all together and allow for additional /// information to be included in them for this reason.
SetupErr(String),
}
// Prepare updater packages // Find distinct updater packages (so we don't process repeated ones // more than once). letmut distinct_updater_packages: Vec<PathBuf> =
tests.iter().map(|t| t.updater_package.clone()).collect();
distinct_updater_packages.sort();
distinct_updater_packages.dedup();
// Decide on locations for them. let updater_locations: HashMap<PathBuf, PathBuf> = distinct_updater_packages
.iter()
.map(|updater_package| -> Result<(PathBuf, PathBuf)> { let unpack_dir = tempfile::Builder::new()
.prefix("updater_")
.tempdir_in(tmpdir)?
.keep(); return Ok((updater_package.clone(), unpack_dir));
})
.collect::<Result<HashMap<_, _>>>()?;
// Unpack into the desired locations. let updater_items: Vec<_> = updater_locations.iter().collect(); let chunk_size = updater_items.len().div_ceil(parallelism).max(1); let updater_results: Vec<_> = thread::scope(|s| -> Result<Vec<_>> { let handles: Vec<_> = updater_items // Return `chunk_size` updater packages at a time
.chunks(chunk_size) // For each chunk...
.map(|chunk| { // Spawn a thread...
s.spawn(move || {
chunk
.iter() // For each updater package in the chunk...
.map(|(updater_package, unpack_dir)| { // Unpack it... let result = prepare_updater(
updater_package,
appname,
cert_dir,
cert_overrides,
unpack_dir,
);
// And return the package along with the // Result (whose Ok value is the path to the // containing updater binary on disk). return (*updater_package, result);
})
.collect::<Vec<_>>()
})
})
.collect();
letmut results = Vec::with_capacity(updater_items.len()); for h in handles { let chunk_result = h
.join() // Handle errors that come up when joining the thread
.map_err(|_| anyhow::anyhow!("prepare updater thread panicked"))?;
results.extend(chunk_result);
}
Ok(results)
})?;
for (package, result) in updater_results { match result {
Ok(updater) => {
updater_binaries.insert(package.clone(), updater);
} // Handle errors that came up when preparing an updater
Err(e) => return Err(e),
}
}
// Creating a named reference to `updater_binaries` allows it to be moved // into the thread scope multiple times (because references can be copied). // Without this, the thread scope tries to move the owned `updater_binaries` // which fails because HashMaps are not copyable. let updater_binaries_ref = &updater_binaries; let chunk_size = tests.len().div_ceil(parallelism).max(1); let outcomes: Vec<TestOutcome> = thread::scope(|s| -> Result<Vec<TestOutcome>> { let handles: Vec<_> = tests
.chunks(chunk_size)
.map(|chunk| {
s.spawn(move || {
chunk
.iter()
.map(|test| { let updater = updater_binaries_ref[&test.updater_package].clone(); match run_test(
test,
&updater,
check_updates,
target_platform,
to_installer,
channel,
appname,
tmpdir,
artifact_dir,
runner,
) {
Ok(o) => o, // Any errors coming from `run_test` constitute // a test failure. Unlike other errors (eg: when // unpacking updaters above), these do not // constitute an overall error with the program // that we need to exit for -- they are simply // converted to a TestResult indicating the // error.
Err(e) => TestOutcome {
label: test.to_string(),
result: TestResult::SetupErr(e.to_string()),
output: String::new(),
},
}
})
.collect::<Vec<_>>()
})
})
.collect();
letmut outcomes = vec![]; for h in handles { let chunk_result = h
.join()
.map_err(|_| anyhow::anyhow!("test thread panicked"))?;
outcomes.extend(chunk_result);
}
Ok(outcomes)
})?;
letmut cmd = Command::new("/bin/bash");
cmd.arg(check_updates)
.arg(target_platform)
.arg(&test.from_installer)
.arg(to_installer)
.arg(&test.locale)
.arg(updater)
.arg(diff_file.to_str().unwrap())
.arg(channel) // check_updates.sh requires positional args that we don't use
.arg("")
.arg("") // mac-only option that tells `check_updates.sh` where to find // a usable `update-settings.ini` file that the updater requires // this file is always located in the same dir as the `updater` // binary. `check_updates.sh` ignores this option on other platforms.
.arg(
updater
.parent()
.ok_or_else(|| anyhow!("Couldn't determine update-settings.ini dir!"))?,
)
.arg(appname)
.current_dir(test_dir); let command_result = runner.run(cmd)?; let result = match command_result.exit_code { 0 => TestResult::Pass, 1 => TestResult::SetupErr("Failed to unpack from or to build".to_string()), 2 => TestResult::SetupErr("Failed to cd into from build application directory".to_string()), 3 => TestResult::SetupErr("from build application directory does not exist".to_string()), 4 => TestResult::UpdateStatusErr, 5 => TestResult::UpdateSettingsMissingErr, 6 => TestResult::ChannelPrefsMissingErr, 7 => TestResult::DifferencesFoundErr, 8 => TestResult::DiffErr,
_ => TestResult::UnknownErr,
};
// save the output to an artifact letmut output_file = artifact_dir.to_path_buf();
output_file.push(format!("{}.output.log", test.full_id())); letmut f = File::create(output_file)?;
f.write_all(command_result.output.as_bytes())?;
/// Setup the test directory in a way that is compatible with the expectations /// of `check_updates.sh`: create a fresh directory with an `update` directory /// inside of it, containing the MAR to be applied in `update.mar`. /// When we get rid of `check_updates.sh` we can consider changing or getting /// rid of this. fn setup_test_dir(mar: &Path, tmpdir: &Path) -> Result<PathBuf> { let test_dir = tempfile::Builder::new()
.prefix("work_")
.tempdir_in(tmpdir)?
.keep(); letmut update_dir = test_dir.clone();
update_dir.push("update");
create_dir(update_dir.as_path())?; letmut mar_path = update_dir.clone();
mar_path.push("update.mar");
symlink(mar, mar_path.as_path())?; return Ok(test_dir);
}
#[cfg(test)] mod tests { usecrate::runner::CommandResult;
#[test] fn run_tests_setup_err_on_bad_installer() { let tmpdir = TempDir::with_prefix("marannon_run_tests").unwrap(); let tmp = tmpdir.path(); let artifact_dir = TempDir::with_prefix("marannon_artifacts").unwrap(); let artifacts = artifact_dir.path();
let test = Test {
id: "from".to_string(),
mar: tmp.join("test.mar"),
from_installer: PathBuf::from("/nonexistent/installer.tar.xz"),
updater_package: PathBuf::from("/nonexistent/installer.tar.xz"),
locale: "en-US".to_string(),
};
let result = run_tests(
&PathBuf::from("/fake/check_updates.sh"), "linux",
&PathBuf::from("/fake/to_installer"), "release", "firefox",
None,
&vec![],
vec![test],
&tmp.to_path_buf(),
&artifacts.to_path_buf(),
&FakeRunner(0), 1,
);
assert!(result.is_err()); let e = result.unwrap_err();
assert!(e.to_string().contains("No such file or directory"));
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.