#![forbid(unsafe_code)]
mod git;
use std::{borrow::Cow, env, ffi::OsString, fs, io, path::Path, path::PathBuf};
use tinyjson::JsonValue;
#[derive(Debug)]
pub struct Files {
key: usize,
}
#[derive(Debug)]
enum Managed {
Files(PathBuf),
}
type FsItem<'lt> = &'lt mut PathBuf;
#[derive(Debug)]
pub struct FsData {
map: Vec<PathBuf>,
}
#[derive(Debug)]
enum Source {
VcsFromManifest {
commit_id: git::CommitId,
git: git::Git,
datadir: PathBuf,
},
Local(git::Git),
}
#[derive(Default, Debug)]
struct Resources<'paths> {
relative_files: Vec<Managed>,
unmanaged: Vec<FsItem<'paths>>,
}
#[must_use = "This is only a builder. Call `build` to perform validation/fetch/etc."]
#[derive(Debug)]
pub struct Setup<'paths> {
repository: OsString,
manifest: &'static str,
source: Source,
resources: Resources<'paths>,
pack_objects: Option<OsString>,
}
#[doc(hidden)]
pub struct EnvOptions {
pub pkg_repository: &'static str,
pub manifest_dir: &'static str,
pub target_tmpdir: Option<&'static str>,
}
#[macro_export]
macro_rules! setup {
() => {
$crate::_setup($crate::EnvOptions {
pkg_repository: env!("CARGO_PKG_REPOSITORY"),
manifest_dir: env!("CARGO_MANIFEST_DIR"),
target_tmpdir: option_env!("CARGO_TARGET_TMPDIR"),
})
};
}
#[doc(hidden)]
pub fn _setup(options: EnvOptions) -> Setup<'static> {
let EnvOptions {
pkg_repository: repository,
manifest_dir: manifest,
target_tmpdir: tmpdir,
} = options;
if repository.is_empty() {
inconclusive(&mut "The crate must have a valid URL in `package.repository`");
}
let repository = OsString::from(repository);
let integration_test_tempdir = tmpdir.map(Path::new);
let vcs_info_path = env::var_os("CARGO_XTEST_VCS_INFO");
let force_vcs = vcs_info_path.is_some();
let vcs_info_path = vcs_info_path.as_ref().map_or_else(
|| Path::new(manifest).join(".cargo_vcs_info.json"),
PathBuf::from,
);
let (source, pack_objects);
if vcs_info_path.exists() {
trait GetKey {
fn get_key(&self, key: &str) -> Option<&Self>;
}
impl GetKey for JsonValue {
fn get_key(&self, key: &str) -> Option<&Self> {
self.get::<std::collections::HashMap<_, _>>()?.get(key)
}
}
let data =
fs::read_to_string(vcs_info_path).unwrap_or_else(|mut err| inconclusive(&mut err));
let vcs: JsonValue = data
.parse()
.unwrap_or_else(|mut err| inconclusive(&mut err));
let commit_id = vcs
.get_key("git")
.unwrap_or_else(|| inconclusive(&mut "VCS does not contain a git section."))
.get_key("sha1")
.unwrap_or_else(|| inconclusive(&mut "VCS commit ID not recognized."))
.get::<String>()
.map(|id| git::CommitId::from(&**id))
.unwrap_or_else(|| inconclusive(&mut "VCS commit ID is not a string"));
let git = git::Git::new().unwrap_or_else(|mut err| inconclusive(&mut err));
let datadir = integration_test_tempdir
.map(Cow::Borrowed)
.or_else(|| {
let environment_temp = std::env::var_os("CARGO_XTEST_DATA_TMPDIR")
.or_else(|| std::env::var_os("TMPDIR"))
.map(PathBuf::from)?;
Some(Cow::Owned(environment_temp))
})
.expect("This setup must only be called in an integration test or benchmark, or with an explicit TMPDIR")
.into_owned();
pack_objects = std::env::var_os("CARGO_XTEST_DATA_PACK_OBJECTS");
source = Source::VcsFromManifest {
commit_id,
git,
datadir,
};
} else if force_vcs {
inconclusive(&mut format!(
"Expected VCS info at {}",
vcs_info_path.display()
));
} else {
let git = git::Git::new().unwrap_or_else(|mut err| inconclusive(&mut err));
source = Source::Local(git);
pack_objects = std::env::var_os("CARGO_XTEST_DATA_PACK_OBJECTS");
};
if repository.is_empty() {
inconclusive(&mut "The repository must have a valid URL");
}
Setup {
repository,
manifest,
source,
resources: Resources::default(),
pack_objects,
}
}
impl<'lt> Setup<'lt> {
pub fn rewrite(mut self, iter: impl IntoIterator<Item = &'lt mut PathBuf>) -> Self {
self.resources.unmanaged.extend(iter);
self
}
pub fn add(&mut self, path: impl AsRef<Path>) -> Files {
fn path_impl(resources: &mut Resources, path: &Path) -> usize {
let item = Managed::Files(path.to_owned());
let key = resources.relative_files.len();
resources.relative_files.push(item);
key
}
let key = path_impl(&mut self.resources, path.as_ref());
Files { key }
}
pub fn build(self) -> FsData {
let mut map;
match self.source {
Source::Local(git) => {
let dir = git::CrateDir::new(self.manifest, &git);
let datapath = Path::new(self.manifest);
dir.tracked(&git, &mut self.resources.path_specs());
if let Some(pack_objects) = self.pack_objects {
std::fs::create_dir_all(&pack_objects)
.unwrap_or_else(|mut err| inconclusive(&mut err));
dir.pack_objects(&git, &mut self.resources.path_specs(), pack_objects);
}
map = vec![];
self.resources.relative_files.iter().for_each(|path| {
map.push(datapath.join(path.as_path()));
});
self.resources
.unmanaged
.into_iter()
.for_each(|item| set_root(datapath, item));
}
Source::VcsFromManifest {
commit_id,
datadir,
git,
} => {
let origin = git::Origin {
url: self.repository,
};
let gitpath = datadir.join("xtest-data-git");
let datapath = unique_dir(&datadir, "xtest-data-tree")
.unwrap_or_else(|mut err| inconclusive(&mut err));
let shallow;
if let Some(pack_objects) = self.pack_objects {
shallow = git.bare(gitpath, &commit_id);
shallow.unpack(&git, &pack_objects);
} else {
panic!("Requested test data from {} but have no packed artifacts to load. Provide an explicit path to a directory to unpack via the `CARGO_XTEST_DATA_PACK_OBJECTS` environment variable", Path::new(&origin.url).display());
}
shallow.checkout(
&git,
&datapath,
&commit_id,
&mut self.resources.path_specs(),
);
map = vec![];
self.resources.relative_files.iter().for_each(|path| {
map.push(datapath.join(path.as_path()));
});
self.resources
.unmanaged
.into_iter()
.for_each(|item| set_root(&datapath, item));
}
}
FsData { map }
}
}
impl Resources<'_> {
pub fn path_specs(&self) -> impl Iterator<Item = git::PathSpec<'_>> {
let values = self.relative_files.iter().map(Managed::as_path_spec);
let unmanaged = self.unmanaged.iter().map(|x| git::PathSpec::Path(&**x));
values.chain(unmanaged)
}
}
impl FsData {
pub fn path(&self, file: &Files) -> &Path {
self.map.get(file.key).unwrap().as_path()
}
}
impl Managed {
pub fn as_path(&self) -> &Path {
match self {
Managed::Files(path) => path,
}
}
fn as_path_spec(&self) -> git::PathSpec<'_> {
match self {
Managed::Files(path) => git::PathSpec::Path(path),
}
}
}
fn set_root(path: &Path, dir: &mut PathBuf) {
*dir = path.join(&*dir)
}
fn unique_dir(base: &Path, prefix: &str) -> Result<PathBuf, std::io::Error> {
let mut rng = nanorand::tls::tls_rng();
assert!(matches!(
Path::new(prefix).components().next(),
Some(std::path::Component::Normal(_))
));
assert!(Path::new(prefix).components().nth(1).is_none());
let mut buffer = prefix.to_string();
let mut generate_name = move || -> PathBuf {
use nanorand::Rng;
const TABLE: &str = "0123456789abcdef";
let num: [u8; 8] = rng.rand();
buffer.clear();
buffer.push_str(prefix);
for byte in num {
let (low, hi) = (usize::from(byte & 0xf), usize::from((byte >> 4) & 0xf));
buffer.push_str(&TABLE[low..low + 1]);
buffer.push_str(&TABLE[hi..hi + 1]);
}
base.join(&buffer)
};
loop {
let path = generate_name();
match fs::create_dir(&path) {
Ok(_) => return Ok(path),
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
Err(other) => return Err(other),
}
}
}
#[cold]
#[track_caller]
fn inconclusive(err: &mut dyn std::fmt::Display) -> ! {
eprintln!("xtest-data failed to setup.");
eprintln!("Information: {}", err);
panic!();
}