use fs_err as fs;
use rattler_build_jinja::Variable;
use rattler_build_recipe::stage1::{
TestType,
tests::{CommandsTest, DownstreamTest, PerlTest, PythonTest, PythonVersion, RTest, RubyTest},
};
use rattler_build_script::{EnvironmentIsolation, Script, ScriptContent};
use rattler_build_types::NormalizedKey;
use rattler_conda_types::{
Channel, ChannelUrl, MatchSpec, ParseStrictness, Platform,
compression_level::CompressionLevel,
package::{CondaArchiveIdentifier, IndexJson, PackageFile},
};
use rattler_index::{IndexFsConfig, index_fs};
use rattler_package_streaming::write::write_conda_package;
use rattler_shell::{
activation::ActivationError,
shell::{Shell, ShellEnum},
};
use rattler_solve::{ChannelPriority, SolveStrategy};
use std::collections::BTreeMap;
use std::fmt::Write;
use std::{
collections::HashMap,
io::Write as _,
path::{Path, PathBuf},
str::FromStr,
};
use tempfile::TempDir;
use walkdir::WalkDir;
use rattler::package_cache::PackageCache;
use rattler_cache::validation::ValidationMode;
use crate::{
env_vars, metadata::PlatformWithVirtualPackages, render::solver::create_environment,
source::copy_dir::CopyDir, tool_configuration,
};
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum TestError {
#[error("failed package content tests: {0}")]
PackageContentTestFailed(String),
#[error("failed package content tests: {0}")]
PackageContentTestFailedStr(&'static str),
#[error("failed to get environment `PREFIX` variable")]
PrefixEnvironmentVariableNotFound,
#[error("failed to build glob from pattern")]
GlobError(#[from] globset::Error),
#[error("failed to run test: {0}")]
TestFailed(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("failed to write testing script: {0}")]
FailedToWriteScript(#[from] std::fmt::Error),
#[error("failed to parse MatchSpec: {0}")]
MatchSpecParse(String),
#[error("failed to setup test environment: {0}")]
TestEnvironmentSetup(String),
#[error("failed to setup test environment: {0}")]
TestEnvironmentActivation(#[from] ActivationError),
#[error("failed to parse tests from `info/tests/tests.yaml`: {0}")]
TestYamlParseError(#[from] serde_yaml::Error),
#[error("failed to parse JSON from test files: {0}")]
TestJSONParseError(#[from] serde_json::Error),
#[error("failed to parse MatchSpec from test files: {0}")]
TestMatchSpecParseError(#[from] rattler_conda_types::ParseMatchSpecError),
#[error("missing package file name")]
MissingPackageFileName,
#[error("archive type not supported")]
ArchiveTypeNotSupported,
#[error("could not determine target platform from package file (no index.json?)")]
CouldNotDetermineTargetPlatform,
#[error(
"no tests found in package. Expected `info/test/` (conda-build format) or `info/tests/tests.yaml` (rattler-build format)"
)]
NoTestsFound,
}
fn create_conda_from_directory(
package_dir: &Path,
output_dir: &Path,
) -> Result<PathBuf, TestError> {
let index_json = IndexJson::from_package_directory(package_dir)?;
let identifier = format!(
"{}-{}-{}",
index_json.name.as_normalized(),
index_json.version,
index_json.build
);
let archive_name = format!("{}.conda", identifier);
let archive_path = output_dir.join(&archive_name);
let files: Vec<PathBuf> = WalkDir::new(package_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| e.into_path())
.collect();
let mut file = fs::File::create(&archive_path)?;
write_conda_package(
&mut file,
package_dir,
&files,
CompressionLevel::default(),
None,
&identifier,
None,
None,
)
.map_err(|e| TestError::TestFailed(format!("failed to create .conda from directory: {}", e)))?;
Ok(archive_path)
}
#[derive(Debug)]
enum Tests {
Commands(PathBuf),
Python(PathBuf),
}
impl Tests {
async fn run(
&self,
environment: &Path,
cwd: &Path,
pkg_vars: &HashMap<String, String>,
resolved_records: &[rattler_conda_types::RepoDataRecord],
config: &TestConfiguration,
) -> Result<(), TestError> {
tracing::info!("Testing commands:");
let target_platform = config.target_platform.unwrap_or(Platform::current());
let build_platform = config.current_platform.platform;
let host_platform = config
.host_platform
.as_ref()
.map(|p| p.platform)
.unwrap_or(target_platform);
let platform = Platform::current();
let mut env_vars = env_vars::os_vars(environment, &platform, config.env_isolation);
if config.env_isolation == EnvironmentIsolation::None {
env_vars.retain(|key, _| key != ShellEnum::default().path_var(&platform));
}
env_vars.extend(env_vars::test_vars(
target_platform,
build_platform,
host_platform,
));
env_vars.extend(env_vars::python_vars_from_records(
resolved_records,
environment,
platform,
));
env_vars.extend(pkg_vars.iter().map(|(k, v)| (k.clone(), Some(v.clone()))));
env_vars.insert(
"PREFIX".to_string(),
Some(environment.to_string_lossy().to_string()),
);
let tmp_dir = tempfile::tempdir()?;
CopyDir::new(cwd, tmp_dir.path()).run().map_err(|e| {
TestError::IoError(std::io::Error::other(format!(
"Failed to copy test files: {}",
e
)))
})?;
match self {
Tests::Commands(path) => {
let script = Script {
content: ScriptContent::Path(path.clone()),
..Script::default()
};
script
.run_script(
env_vars,
tmp_dir.path(),
cwd,
environment,
None,
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
}
Tests::Python(path) => {
let script = Script {
content: ScriptContent::Path(path.clone()),
interpreter: Some("python".into()),
..Script::default()
};
script
.run_script(
env_vars,
tmp_dir.path(),
cwd,
environment,
None,
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
}
}
Ok(())
}
}
async fn legacy_tests_from_folder(pkg: &Path) -> Result<(PathBuf, Vec<Tests>), std::io::Error> {
let mut tests = Vec::new();
let test_folder = pkg.join("info/test");
if !test_folder.exists() {
return Ok((test_folder, tests));
}
let mut read_dir = tokio::fs::read_dir(&test_folder).await?;
while let Some(entry) = read_dir.next_entry().await? {
let path = entry.path();
if path.is_dir() {
continue;
}
let Some(file_name) = path.file_name() else {
continue;
};
if file_name.eq("run_test.sh") || file_name.eq("run_test.bat") {
tracing::info!("test {}", file_name.to_string_lossy());
tests.push(Tests::Commands(path));
} else if file_name.eq("run_test.py") {
tracing::info!("test {}", file_name.to_string_lossy());
tests.push(Tests::Python(path));
}
}
Ok((test_folder, tests))
}
#[derive(Clone)]
pub struct TestConfiguration {
pub test_prefix: PathBuf,
pub target_platform: Option<Platform>,
pub host_platform: Option<PlatformWithVirtualPackages>,
pub current_platform: PlatformWithVirtualPackages,
pub keep_test_prefix: bool,
pub test_index: Option<usize>,
pub channels: Vec<ChannelUrl>,
pub channel_priority: ChannelPriority,
pub solve_strategy: SolveStrategy,
pub tool_configuration: tool_configuration::Configuration,
pub output_dir: PathBuf,
pub exclude_newer: Option<chrono::DateTime<chrono::Utc>>,
pub env_isolation: EnvironmentIsolation,
}
fn env_vars_from_package(index_json: &IndexJson) -> HashMap<String, String> {
let mut res = HashMap::new();
res.insert(
"PKG_NAME".to_string(),
index_json.name.as_normalized().to_string(),
);
res.insert("PKG_VERSION".to_string(), index_json.version.to_string());
res.insert("PKG_BUILD_STRING".to_string(), index_json.build.clone());
res.insert(
"PKG_BUILDNUM".to_string(),
index_json.build_number.to_string(),
);
res.insert(
"PKG_BUILD_NUMBER".to_string(),
index_json.build_number.to_string(),
);
res
}
fn env_vars_from_hash_input(package_folder: &Path) -> HashMap<String, Option<String>> {
let hash_input_path = package_folder.join("info/hash_input.json");
let content = fs::read_to_string(&hash_input_path).unwrap_or_default();
let variant: BTreeMap<NormalizedKey, Variable> =
serde_json::from_str(&content).unwrap_or_default();
env_vars::env_vars_from_variant(&variant)
}
#[async_recursion::async_recursion]
pub async fn run_test(
package_file: &Path,
config: &TestConfiguration,
downstream_package: Option<PathBuf>,
) -> Result<(), TestError> {
let tmp_repo = tempfile::tempdir()?;
fs::create_dir_all(&config.test_prefix)?;
let _temp_archive_dir;
let package_file = if package_file.is_dir() {
tracing::info!(
"Input is a directory, creating temporary .conda archive from '{}'",
package_file.display()
);
_temp_archive_dir = tempfile::tempdir()?;
create_conda_from_directory(package_file, _temp_archive_dir.path())?
} else {
package_file.to_path_buf()
};
let package_file = package_file.as_path();
let target_platform = if let Some(tp) = config.target_platform {
tp
} else {
let index_json: IndexJson =
rattler_package_streaming::seek::read_package_file(package_file)
.map_err(|_| TestError::CouldNotDetermineTargetPlatform)?;
let subdir = index_json
.subdir
.ok_or(TestError::CouldNotDetermineTargetPlatform)?;
Platform::from_str(&subdir).map_err(|_| TestError::CouldNotDetermineTargetPlatform)?
};
let subdir = tmp_repo.path().join(target_platform.to_string());
fs::create_dir_all(&subdir)?;
fs::copy(
package_file,
subdir.join(
package_file
.file_name()
.ok_or(TestError::MissingPackageFileName)?,
),
)?;
if let Some(ref downstream_package) = downstream_package {
fs::copy(
downstream_package,
subdir.join(
downstream_package
.file_name()
.ok_or(TestError::MissingPackageFileName)?,
),
)?;
}
let package_file = downstream_package.as_deref().unwrap_or(package_file);
let index_config = IndexFsConfig {
channel: tmp_repo.path().to_path_buf(),
target_platform: Some(target_platform),
repodata_patch: None,
write_zst: false,
write_shards: false,
force: false,
max_parallel: num_cpus::get_physical(),
multi_progress: None,
};
index_fs(index_config)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
let pkg = CondaArchiveIdentifier::try_from_path(package_file)
.ok_or_else(|| TestError::TestFailed("could not get archive identifier".to_string()))?;
let temp_cache_dir = tempfile::tempdir()?;
let global_cache_dir = rattler_cache::default_cache_dir()
.map_err(|e| TestError::TestFailed(format!("failed to determine cache directory: {e}")))?
.join(rattler_cache::PACKAGE_CACHE_DIR);
let temp_package_cache = PackageCache::new_layered(
[temp_cache_dir.path().to_path_buf(), global_cache_dir],
false,
ValidationMode::default(),
);
let cache_metadata = temp_package_cache
.get_or_fetch_from_path(package_file, None)
.await
.map_err(|e| TestError::TestFailed(format!("failed to cache package: {e}")))?;
let package_folder = cache_metadata.path().to_path_buf();
let mut channels = config.channels.clone();
channels.insert(0, Channel::from_directory(tmp_repo.path()).base_url);
let host_platform = config.host_platform.clone().unwrap_or_else(|| {
if target_platform == Platform::NoArch {
config.current_platform.clone()
} else {
PlatformWithVirtualPackages {
platform: target_platform,
virtual_packages: config.current_platform.virtual_packages.clone(),
}
}
});
let config = TestConfiguration {
target_platform: Some(target_platform),
host_platform: Some(host_platform.clone()),
channels,
tool_configuration: tool_configuration::Configuration {
package_cache: temp_package_cache,
..config.tool_configuration.clone()
},
..config.clone()
};
tracing::info!("Collecting tests from '{}'", package_folder.display());
let index_json = IndexJson::from_package_directory(&package_folder)?;
let mut env = env_vars_from_package(&index_json);
env.extend(
env_vars_from_hash_input(&package_folder)
.into_iter()
.filter_map(|(k, v)| v.map(|v| (k, v))),
);
let has_legacy_tests = package_folder.join("info/test").exists();
let has_modern_tests = package_folder.join("info/tests/tests.yaml").exists();
if has_legacy_tests {
let prefix =
TempDir::with_prefix_in(format!("test_{}", pkg.identifier.name), &config.test_prefix)?
.keep();
tracing::info!("Creating test environment in '{}'", prefix.display());
let test_dep_json = PathBuf::from("info/test/test_time_dependencies.json");
let test_dependencies: Vec<String> = if package_folder.join(&test_dep_json).exists() {
serde_json::from_str(&fs::read_to_string(package_folder.join(&test_dep_json))?)?
} else {
Vec::new()
};
let mut dependencies: Vec<MatchSpec> = test_dependencies
.iter()
.map(|s| MatchSpec::from_str(s, ParseStrictness::Lenient))
.collect::<Result<Vec<_>, _>>()?;
let match_spec = MatchSpec::from_str(
format!(
"{}={}={}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
)
.as_str(),
ParseStrictness::Lenient,
)
.map_err(|e| TestError::MatchSpecParse(e.to_string()))?;
dependencies.push(match_spec);
let resolved_records = create_environment(
"test",
&dependencies,
&host_platform,
&prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await
.map_err(|e| TestError::TestEnvironmentSetup(format!("{e:?}")))?;
let (test_folder, tests) = legacy_tests_from_folder(&package_folder).await?;
for test in tests {
test.run(&prefix, &test_folder, &env, &resolved_records, &config)
.await?;
}
tracing::info!(
"{} all tests passed!",
console::style(console::Emoji("✔", "")).green()
);
if prefix.exists() {
fs::remove_dir_all(prefix)?;
}
}
if has_modern_tests {
let tests = fs::read_to_string(package_folder.join("info/tests/tests.yaml"))?;
let tests: Vec<TestType> = serde_yaml::from_str(&tests)?;
if let Some(test_index) = config.test_index
&& test_index >= tests.len()
{
return Err(TestError::TestFailed(format!(
"Test index {} out of range (0..{})",
test_index,
tests.len()
)));
}
let tests = if let Some(test_index) = config.test_index {
vec![tests[test_index].clone()]
} else {
tests
};
for test in tests {
let test_prefix = TempDir::with_prefix_in(
format!("test_{}", pkg.identifier.name),
&config.test_prefix,
)?
.keep();
match test {
TestType::Commands(c) => {
run_commands_test(&c, &pkg, &package_folder, &test_prefix, &config, &env)
.await?
}
TestType::Python { python } => {
run_python_test(&python, &pkg, &package_folder, &test_prefix, &config).await?
}
TestType::Perl { perl } => {
run_perl_test(&perl, &pkg, &package_folder, &test_prefix, &config).await?
}
TestType::R { r } => {
run_r_test(&r, &pkg, &package_folder, &test_prefix, &config).await?
}
TestType::Ruby { ruby } => {
run_ruby_test(&ruby, &pkg, &package_folder, &test_prefix, &config).await?
}
TestType::Downstream(downstream) if downstream_package.is_none() => {
run_downstream_test(&downstream, &pkg, package_file, &test_prefix, &config)
.await?
}
TestType::Downstream(_) => {
tracing::info!(
"Skipping downstream test as we are already testing a downstream package"
)
}
TestType::PackageContents { .. } => {}
}
if !config.keep_test_prefix {
fs::remove_dir_all(test_prefix)?;
}
}
tracing::info!(
"{} all tests passed!",
console::style(console::Emoji("✔", "")).green()
);
}
Ok(())
}
async fn run_python_test(
python_test: &PythonTest,
pkg: &CondaArchiveIdentifier,
path: &Path,
prefix: &Path,
config: &TestConfiguration,
) -> Result<(), TestError> {
let pkg_id = format!(
"{}-{}-{}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
);
let span = tracing::info_span!("Running python test(s)", span_color = pkg_id);
let _guard = span.enter();
let match_spec = MatchSpec::from_str(
format!(
"{}={}={}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
)
.as_str(),
ParseStrictness::Lenient,
)?;
let mut dependencies_map: HashMap<String, Vec<MatchSpec>> = match &python_test.python_version {
PythonVersion::Multiple(versions) => versions
.iter()
.map(|version| {
(
version.clone(),
vec![
MatchSpec::from_str(
&format!("python={}", version),
ParseStrictness::Lenient,
)
.unwrap(),
match_spec.clone(),
],
)
})
.collect(),
PythonVersion::Single(version) => HashMap::from([(
version.clone(),
vec![
MatchSpec::from_str(&format!("python={}", version), ParseStrictness::Lenient)
.unwrap(),
match_spec,
],
)]),
PythonVersion::None => HashMap::from([("".to_string(), vec![match_spec])]),
};
if python_test.pip_check {
dependencies_map
.iter_mut()
.for_each(|(_, v)| v.push("pip".parse().unwrap()));
}
for (python_version, dependencies) in dependencies_map {
run_python_test_inner(
python_test,
python_version,
dependencies,
path,
prefix,
config,
)
.await?;
}
Ok(())
}
async fn run_python_test_inner(
python_test: &PythonTest,
python_version: String,
dependencies: Vec<MatchSpec>,
path: &Path,
prefix: &Path,
config: &TestConfiguration,
) -> Result<(), TestError> {
let span_message = match python_version.as_str() {
"" => "Testing with default python version".to_string(),
_ => format!("Testing with python {}", python_version),
};
let span = tracing::info_span!("", message = %span_message);
let _guard = span.enter();
let test_prefix = prefix.join("test_env");
create_environment(
"test",
&dependencies,
config
.host_platform
.as_ref()
.unwrap_or(&config.current_platform),
&test_prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await
.map_err(|e| TestError::TestEnvironmentSetup(format!("{e:?}")))?;
let mut imports = String::new();
for import in &python_test.imports {
writeln!(imports, "import {}", import)?;
}
let script = Script {
content: ScriptContent::Command(imports),
interpreter: Some("python".into()),
..Script::default()
};
let platform = Platform::current();
let test_env_vars = env_vars::os_vars(&test_prefix, &platform, config.env_isolation);
let test_dir = prefix.join("test");
fs::create_dir_all(&test_dir)?;
script
.run_script(
test_env_vars.clone(),
&test_dir,
path,
&test_prefix,
None,
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
tracing::info!(
"{} python imports test passed!",
console::style(console::Emoji("✔", "")).green()
);
if python_test.pip_check {
let script = Script {
content: ScriptContent::Command("pip check".into()),
..Script::default()
};
script
.run_script(
test_env_vars,
path,
path,
&test_prefix,
None,
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
tracing::info!(
"{} pip check passed!",
console::style(console::Emoji("✔", "")).green()
);
}
Ok(())
}
async fn run_perl_test(
perl_test: &PerlTest,
pkg: &CondaArchiveIdentifier,
path: &Path,
prefix: &Path,
config: &TestConfiguration,
) -> Result<(), TestError> {
let pkg_id = format!(
"{}-{}-{}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
);
let span = tracing::info_span!("Running perl test", span_color = pkg_id);
let _guard = span.enter();
let match_spec = MatchSpec::from_str(
format!(
"{}={}={}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
)
.as_str(),
ParseStrictness::Lenient,
)?;
let dependencies = vec!["perl".parse().unwrap(), match_spec];
let test_prefix = prefix.join("test_env");
create_environment(
"test",
&dependencies,
config
.host_platform
.as_ref()
.unwrap_or(&config.current_platform),
&test_prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await
.map_err(|e| TestError::TestEnvironmentSetup(format!("{e:?}")))?;
let mut imports = String::new();
tracing::info!("Testing perl imports:\n");
for module in &perl_test.uses {
writeln!(imports, "use {};", module)?;
tracing::info!(" use {};", module);
}
tracing::info!("\n");
let script = Script {
content: ScriptContent::Command(imports.clone()),
interpreter: Some("perl".into()),
..Script::default()
};
let platform = Platform::current();
let test_env_vars = env_vars::os_vars(&test_prefix, &platform, config.env_isolation);
let test_folder = prefix.join("test_files");
fs::create_dir_all(&test_folder)?;
script
.run_script(
test_env_vars,
&test_folder,
path,
&test_prefix,
None,
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
Ok(())
}
async fn run_commands_test(
commands_test: &CommandsTest,
pkg: &CondaArchiveIdentifier,
path: &Path,
test_directory: &Path,
config: &TestConfiguration,
pkg_vars: &HashMap<String, String>,
) -> Result<(), TestError> {
let deps = commands_test.requirements.clone();
let pkg_str = pkg.to_string();
let span =
tracing::info_span!("Running script test for", recipe = %pkg_str, span_color = pkg_str);
let _guard = span.enter();
let build_prefix = if !deps.build.is_empty() {
tracing::info!("Installing build dependencies");
let build_prefix = test_directory.join("test_build_env");
let build_dependencies: Vec<MatchSpec> = deps
.build
.iter()
.map(|d| d.as_match_spec().clone())
.collect();
create_environment(
"test",
&build_dependencies,
&config.current_platform,
&build_prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await
.map_err(|e| TestError::TestEnvironmentSetup(format!("{e:?}")))?;
Some(build_prefix)
} else {
None
};
let mut dependencies: Vec<MatchSpec> =
deps.run.iter().map(|d| d.as_match_spec().clone()).collect();
dependencies.push(MatchSpec::from_str(
format!(
"{}={}={}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
)
.as_str(),
ParseStrictness::Lenient,
)?);
let platform = config
.host_platform
.as_ref()
.unwrap_or(&config.current_platform);
let run_prefix = test_directory.join("test_run_env");
let resolved_records = create_environment(
"test",
&dependencies,
platform,
&run_prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await
.map_err(|e| TestError::TestEnvironmentSetup(format!("{e:?}")))?;
let target_platform = config.target_platform.unwrap_or(Platform::current());
let build_platform = config.current_platform.platform;
let host_platform = config
.host_platform
.as_ref()
.map(|p| p.platform)
.unwrap_or(target_platform);
let platform = Platform::current();
let mut env_vars = env_vars::os_vars(&run_prefix, &platform, config.env_isolation);
if config.env_isolation == EnvironmentIsolation::None {
env_vars.retain(|key, _| key != ShellEnum::default().path_var(&platform));
}
env_vars.extend(env_vars::test_vars(
target_platform,
build_platform,
host_platform,
));
env_vars.extend(env_vars::python_vars_from_records(
&resolved_records,
&run_prefix,
platform,
));
env_vars.extend(pkg_vars.iter().map(|(k, v)| (k.clone(), Some(v.clone()))));
env_vars.insert(
"PREFIX".to_string(),
Some(run_prefix.to_string_lossy().to_string()),
);
let test_dir = test_directory.join("test");
CopyDir::new(path, &test_dir).run().map_err(|e| {
TestError::IoError(std::io::Error::other(format!(
"Failed to copy test files: {}",
e
)))
})?;
tracing::info!("Testing commands:");
commands_test
.script
.run_script(
env_vars,
&test_dir,
path,
&run_prefix,
build_prefix.as_ref(),
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
Ok(())
}
async fn run_downstream_test(
downstream_test: &DownstreamTest,
pkg: &CondaArchiveIdentifier,
path: &Path,
prefix: &Path,
config: &TestConfiguration,
) -> Result<(), TestError> {
let downstream_spec = downstream_test.downstream.clone();
let pkg_id = format!(
"{}-{}-{}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
);
let span = tracing::info_span!(
"Running downstream test for",
package = downstream_spec,
span_color = pkg_id
);
let _guard = span.enter();
let match_specs = [
MatchSpec::from_str(&downstream_spec, ParseStrictness::Lenient)?,
MatchSpec::from_str(
format!(
"{}={}={}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
)
.as_str(),
ParseStrictness::Lenient,
)?,
];
let resolved = create_environment(
"test",
&match_specs,
&config.current_platform,
prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await;
match resolved {
Ok(solution) => {
let spec_name = match match_specs[0].name.clone().into_exact() {
Some(name) => name,
None => {
return Err(TestError::TestFailed(
"Expected exact package name in matchspec".to_string(),
));
}
};
let downstream_package = solution
.iter()
.find(|s| s.package_record.name == spec_name)
.ok_or_else(|| {
TestError::TestFailed(
"Could not find package in the resolved environment".to_string(),
)
})?;
let temp_dir = tempfile::tempdir()?;
let package_file = temp_dir
.path()
.join(downstream_package.identifier.to_file_name());
if downstream_package.url.scheme() == "file" {
fs::copy(
downstream_package.url.to_file_path().unwrap(),
&package_file,
)?;
} else {
let package_dl = reqwest::get(downstream_package.url.clone()).await.unwrap();
let mut file = fs::File::create(&package_file)?;
let bytes = package_dl.bytes().await.unwrap();
file.write_all(&bytes)?;
}
tracing::info!("Running downstream test with {:?}", &package_file);
run_test(path, config, Some(package_file.clone()))
.await
.inspect_err(|_| {
tracing::error!("Downstream test with {:?} failed", &package_file);
})?;
}
Err(e) => {
tracing::warn!(
"Downstream test could not run. Environment might be unsolvable: {:?}",
e
);
}
}
Ok(())
}
async fn run_r_test(
r_test: &RTest,
pkg: &CondaArchiveIdentifier,
path: &Path,
prefix: &Path,
config: &TestConfiguration,
) -> Result<(), TestError> {
let pkg_id = format!(
"{}-{}-{}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
);
let span = tracing::info_span!("Running R test", span_color = pkg_id);
let _guard = span.enter();
let match_spec = MatchSpec::from_str(
format!(
"{}={}={}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
)
.as_str(),
ParseStrictness::Lenient,
)?;
let dependencies = vec!["r-base".parse().unwrap(), match_spec];
let test_prefix = prefix.join("test_env");
create_environment(
"test",
&dependencies,
config
.host_platform
.as_ref()
.unwrap_or(&config.current_platform),
&test_prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await
.map_err(|e| TestError::TestEnvironmentSetup(format!("{e:?}")))?;
let mut libraries = String::new();
tracing::info!("Testing R libraries:\n");
for library in &r_test.libraries {
writeln!(libraries, "library({})", library)?;
tracing::info!(" library({})", library);
}
tracing::info!("\n");
let script = Script {
content: ScriptContent::Command(libraries.clone()),
interpreter: Some("rscript".into()),
..Script::default()
};
let platform = Platform::current();
let test_env_vars = env_vars::os_vars(&test_prefix, &platform, config.env_isolation);
let test_folder = prefix.join("test_files");
fs::create_dir_all(&test_folder)?;
script
.run_script(
test_env_vars,
&test_folder,
path,
&test_prefix,
None,
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
Ok(())
}
async fn run_ruby_test(
ruby_test: &RubyTest,
pkg: &CondaArchiveIdentifier,
path: &Path,
prefix: &Path,
config: &TestConfiguration,
) -> Result<(), TestError> {
let pkg_id = format!(
"{}-{}-{}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
);
let span = tracing::info_span!("Running Ruby test", span_color = pkg_id);
let _guard = span.enter();
let match_spec = MatchSpec::from_str(
format!(
"{}={}={}",
pkg.identifier.name, pkg.identifier.version, pkg.identifier.build_string
)
.as_str(),
ParseStrictness::Lenient,
)?;
let dependencies = vec!["ruby".parse().unwrap(), match_spec];
let test_prefix = prefix.join("test_env");
create_environment(
"test",
&dependencies,
config
.host_platform
.as_ref()
.unwrap_or(&config.current_platform),
&test_prefix,
&config.channels,
&config.tool_configuration,
config.channel_priority,
config.solve_strategy,
config.exclude_newer,
)
.await
.map_err(|e| TestError::TestEnvironmentSetup(format!("{e:?}")))?;
let mut requires = String::new();
tracing::info!("Testing Ruby requires:\n");
for module in &ruby_test.requires {
writeln!(requires, "require '{}'", module)?;
tracing::info!(" require '{}'", module);
}
tracing::info!("\n");
let script = Script {
content: ScriptContent::Command(requires.clone()),
interpreter: Some("ruby".into()),
..Script::default()
};
let platform = Platform::current();
let test_env_vars = env_vars::os_vars(&test_prefix, &platform, config.env_isolation);
let test_folder = prefix.join("test_files");
fs::create_dir_all(&test_folder)?;
script
.run_script(
test_env_vars,
&test_folder,
path,
&test_prefix,
None,
None::<fn(&str) -> Result<String, String>>,
None,
config.env_isolation,
)
.await
.map_err(|e| TestError::TestFailed(e.to_string()))?;
Ok(())
}