#[cfg(test)]
mod tests {
use super::super::PackageParser;
use super::super::conda::{
CondaEnvironmentYmlParser, CondaMetaYamlParser, apply_jinja2_substitutions,
extract_jinja2_variables, parse_conda_requirement,
};
use crate::models::PackageType;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_conda_meta_yaml_is_match_meta_yaml() {
let valid_path = PathBuf::from("/some/path/meta.yaml");
assert!(CondaMetaYamlParser::is_match(&valid_path));
}
#[test]
fn test_conda_meta_yaml_is_match_meta_yml() {
let valid_path = PathBuf::from("/some/path/meta.yml");
assert!(CondaMetaYamlParser::is_match(&valid_path));
}
#[test]
fn test_conda_meta_yaml_is_match_invalid() {
let invalid_path = PathBuf::from("/some/path/metadata.yaml");
assert!(!CondaMetaYamlParser::is_match(&invalid_path));
let invalid_path2 = PathBuf::from("/some/path/meta.txt");
assert!(!CondaMetaYamlParser::is_match(&invalid_path2));
}
#[test]
fn test_conda_environment_yml_is_match_environment_yaml() {
let valid_path = PathBuf::from("/some/path/environment.yaml");
assert!(CondaEnvironmentYmlParser::is_match(&valid_path));
}
#[test]
fn test_conda_environment_yml_is_match_environment_yml() {
let valid_path = PathBuf::from("/some/path/environment.yml");
assert!(CondaEnvironmentYmlParser::is_match(&valid_path));
}
#[test]
fn test_conda_environment_yml_is_match_conda_yaml() {
let valid_path = PathBuf::from("/some/path/conda.yaml");
assert!(CondaEnvironmentYmlParser::is_match(&valid_path));
}
#[test]
fn test_conda_environment_yml_is_match_env_yaml() {
let valid_path = PathBuf::from("/some/path/env.yaml");
assert!(CondaEnvironmentYmlParser::is_match(&valid_path));
}
#[test]
fn test_conda_environment_yml_is_match_case_insensitive() {
let valid_path = PathBuf::from("/some/path/ENVIRONMENT.YAML");
assert!(CondaEnvironmentYmlParser::is_match(&valid_path));
let valid_path2 = PathBuf::from("/some/path/CONDA.YML");
assert!(CondaEnvironmentYmlParser::is_match(&valid_path2));
}
#[test]
fn test_conda_environment_yml_is_match_invalid() {
let invalid_path = PathBuf::from("/some/path/environment.txt");
assert!(!CondaEnvironmentYmlParser::is_match(&invalid_path));
let invalid_path2 = PathBuf::from("/some/path/requirements.txt");
assert!(!CondaEnvironmentYmlParser::is_match(&invalid_path2));
}
#[test]
fn test_extract_jinja2_variables_simple() {
let content = r#"{% set version = "0.45.0" %}
{% set sha256 = "abc123" %}
package:
name: test"#;
let vars = extract_jinja2_variables(content);
assert_eq!(vars.len(), 2);
assert_eq!(vars.get("version"), Some(&"0.45.0".to_string()));
assert_eq!(vars.get("sha256"), Some(&"abc123".to_string()));
}
#[test]
fn test_extract_jinja2_variables_single_quotes() {
let content = "{% set version = '1.2.3' %}";
let vars = extract_jinja2_variables(content);
assert_eq!(vars.len(), 1);
assert_eq!(vars.get("version"), Some(&"1.2.3".to_string()));
}
#[test]
fn test_extract_jinja2_variables_empty_content() {
let content = "package:\n name: test\nversion: 1.0";
let vars = extract_jinja2_variables(content);
assert_eq!(vars.len(), 0);
}
#[test]
fn test_apply_jinja2_substitutions_simple() {
let mut variables = HashMap::new();
variables.insert("version".to_string(), "0.45.0".to_string());
variables.insert("sha256".to_string(), "abc123".to_string());
let content = r#"{% set version = "0.45.0" %}
package:
version: {{ version }}
source:
sha256: {{ sha256 }}"#;
let result = apply_jinja2_substitutions(content, &variables);
assert!(result.contains("version: 0.45.0"));
assert!(result.contains("sha256: abc123"));
assert!(!result.contains("{% set version"));
}
#[test]
fn test_apply_jinja2_substitutions_with_lower_filter() {
let mut variables = HashMap::new();
variables.insert("name".to_string(), "MyPackage".to_string());
let content = "url: https://example.com/packages/{{ name|lower }}/archive.tar.gz";
let result = apply_jinja2_substitutions(content, &variables);
assert!(result.contains("mypackage"));
assert!(!result.contains("MyPackage"));
}
#[test]
fn test_apply_jinja2_substitutions_unresolved_removed() {
let variables = HashMap::new();
let content = r#"{% set name = "test" %}
package:
name: {{ unresolved_var }}
version: 1.0"#;
let result = apply_jinja2_substitutions(content, &variables);
assert!(!result.contains("unresolved_var"));
}
#[test]
fn test_parse_conda_requirement_pinned_space_separated() {
let dep = parse_conda_requirement("mccortex ==1.0", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.purl, Some("pkg:conda/mccortex@1.0".to_string()));
assert_eq!(dep.extracted_requirement, Some("==1.0".to_string()));
assert_eq!(dep.scope, Some("run".to_string()));
assert_eq!(dep.is_runtime, Some(true));
assert_eq!(dep.is_optional, Some(false));
assert_eq!(dep.is_pinned, Some(false));
}
#[test]
fn test_parse_conda_requirement_pinned_no_space() {
let dep = parse_conda_requirement("mccortex=1.0", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.purl, Some("pkg:conda/mccortex@1.0".to_string()));
assert_eq!(dep.extracted_requirement, Some("=1.0".to_string()));
assert_eq!(dep.scope, Some("run".to_string()));
assert_eq!(dep.is_runtime, Some(true));
assert_eq!(dep.is_pinned, Some(true));
}
#[test]
fn test_parse_conda_requirement_version_constraint() {
let dep = parse_conda_requirement("python >=3.6", "host");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.purl, Some("pkg:conda/python".to_string()));
assert_eq!(dep.extracted_requirement, Some(">=3.6".to_string()));
assert_eq!(dep.scope, Some("host".to_string()));
assert_eq!(dep.is_pinned, Some(false));
}
#[test]
fn test_parse_conda_requirement_with_namespace() {
let dep = parse_conda_requirement("conda-forge::numpy=1.15.4", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(
dep.purl,
Some("pkg:conda/conda-forge/numpy@1.15.4".to_string())
);
assert_eq!(dep.is_pinned, Some(true));
}
#[test]
fn test_parse_conda_requirement_no_version() {
let dep = parse_conda_requirement("bwa", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.purl, Some("pkg:conda/bwa".to_string()));
assert_eq!(dep.extracted_requirement, Some(String::new()));
assert_eq!(dep.is_pinned, Some(false));
}
#[test]
fn test_parse_conda_requirement_build_scope() {
let dep = parse_conda_requirement("cmake", "build");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.is_runtime, Some(false));
assert_eq!(dep.is_optional, Some(true));
}
#[test]
fn test_environment_top_level_dependencies_are_conda_unless_explicitly_pip() {
let temp_dir = TempDir::new().unwrap();
let env_path = temp_dir.path().join("environment.yml");
fs::write(
&env_path,
r#"
name: test-env
channels:
- conda-forge
dependencies:
- conda-forge::numpy
- pandas>=2
- pip:
- ray
"#,
)
.unwrap();
let package_data = CondaEnvironmentYmlParser::extract_first_package(&env_path);
let numpy = package_data
.dependencies
.iter()
.find(|dep| dep.purl.as_deref() == Some("pkg:conda/conda-forge/numpy"))
.expect("conda numpy dependency missing");
assert_eq!(numpy.scope.as_deref(), Some("dependencies"));
let pandas = package_data
.dependencies
.iter()
.find(|dep| dep.purl.as_deref() == Some("pkg:conda/pandas"))
.expect("conda pandas dependency missing");
assert_eq!(pandas.extracted_requirement.as_deref(), Some(">=2"));
let ray = package_data
.dependencies
.iter()
.find(|dep| dep.purl.as_deref() == Some("pkg:pypi/ray"))
.expect("pip ray dependency missing");
assert_eq!(ray.scope.as_deref(), Some("dependencies"));
}
#[test]
fn test_extract_meta_yaml_abeona() {
let path = PathBuf::from("testdata/conda/meta-yaml/abeona/meta.yaml");
let package_data = CondaMetaYamlParser::extract_first_package(&path);
assert_eq!(package_data.package_type, Some(PackageType::Conda));
assert_eq!(package_data.name, Some("abeona".to_string()));
assert_eq!(package_data.version, Some("0.45.0".to_string()));
assert_eq!(
package_data.homepage_url,
Some("https://github.com/winni2k/abeona".to_string())
);
assert_eq!(
package_data.download_url,
Some("https://pypi.io/packages/source/a/abeona/abeona-0.45.0.tar.gz".to_string())
);
assert_eq!(
package_data.sha256,
Some("bc7512f2eef785b037d836f4cc6faded457ac277f75c6e34eccd12da7c85258f".to_string())
);
assert_eq!(
package_data.extracted_license_statement,
Some("Apache Software".to_string())
);
assert_eq!(
package_data.description,
Some(
"A simple transcriptome assembler based on kallisto and Cortex graphs.".to_string()
)
);
assert_eq!(
package_data.vcs_url,
Some("https://github.com/winni2k/abeona".to_string())
);
}
#[test]
fn test_extract_meta_yaml_abeona_dependencies() {
let path = PathBuf::from("testdata/conda/meta-yaml/abeona/meta.yaml");
let package_data = CondaMetaYamlParser::extract_first_package(&path);
let deps = &package_data.dependencies;
assert_eq!(deps.len(), 7);
let mccortex = deps
.iter()
.find(|d| d.purl.as_deref().is_some_and(|p| p.contains("mccortex")));
assert!(mccortex.is_some());
let mccortex = mccortex.unwrap();
assert_eq!(mccortex.extracted_requirement, Some("==1.0".to_string()));
assert_eq!(mccortex.is_runtime, Some(true));
assert_eq!(mccortex.scope, Some("run".to_string()));
let nextflow = deps
.iter()
.find(|d| d.purl.as_deref().is_some_and(|p| p.contains("nextflow")));
assert!(nextflow.is_some());
let nextflow = nextflow.unwrap();
assert_eq!(
nextflow.purl,
Some("pkg:conda/nextflow@19.01.0".to_string())
);
assert_eq!(nextflow.is_pinned, Some(false));
let bwa = deps
.iter()
.find(|d| d.purl.as_deref().is_some_and(|p| p.contains("bwa")));
assert!(bwa.is_some());
let bwa = bwa.unwrap();
assert_eq!(bwa.purl, Some("pkg:conda/bwa".to_string()));
assert_eq!(bwa.extracted_requirement, Some(String::new()));
assert_eq!(bwa.is_pinned, Some(false));
let pandas = deps
.iter()
.find(|d| d.purl.as_deref().is_some_and(|p| p.contains("pandas")));
assert!(pandas.is_some());
}
#[test]
fn test_extract_meta_yaml_abeona_extra_data() {
let path = PathBuf::from("testdata/conda/meta-yaml/abeona/meta.yaml");
let package_data = CondaMetaYamlParser::extract_first_package(&path);
let extra_data = package_data.extra_data.unwrap_or_default();
assert!(extra_data.contains_key("host"));
assert!(extra_data.contains_key("run"));
let host_deps = extra_data.get("host").unwrap().as_array();
assert!(host_deps.is_some());
let host_deps = host_deps.unwrap();
assert!(host_deps.iter().any(|v| v.as_str() == Some("pip")));
assert!(host_deps.iter().any(|v| v.as_str() == Some("python")));
let run_deps = extra_data.get("run").unwrap().as_array();
assert!(run_deps.is_some());
let run_deps = run_deps.unwrap();
assert!(run_deps.iter().any(|v| v.as_str() == Some("python >=3.6")));
}
#[test]
fn test_extract_meta_yaml_abeona_preserves_license_file_reference() {
let path = PathBuf::from("testdata/conda/meta-yaml/abeona/meta.yaml");
let package_data = CondaMetaYamlParser::extract_first_package(&path);
let extra_data = package_data
.extra_data
.as_ref()
.expect("extra_data should exist");
assert_eq!(
extra_data.get("license_file").and_then(|v| v.as_str()),
Some("LICENSE")
);
let referenced_filenames = package_data.license_detections[0].matches[0]
.referenced_filenames
.as_ref()
.expect("referenced_filenames should be present");
assert_eq!(referenced_filenames, &vec!["LICENSE".to_string()]);
}
#[test]
fn test_extract_environment_yaml_ringer() {
let path = PathBuf::from("testdata/conda/conda-yaml/ringer/environment.yaml");
let package_data = CondaEnvironmentYmlParser::extract_first_package(&path);
assert_eq!(package_data.package_type, Some(PackageType::Conda));
assert_eq!(package_data.name, Some("ringer".to_string()));
assert_eq!(package_data.version, None);
assert_eq!(package_data.primary_language, Some("Python".to_string()));
let extra_data = package_data.extra_data.unwrap_or_default();
assert!(extra_data.contains_key("channels"));
let channels = extra_data.get("channels").unwrap().as_array();
assert!(channels.is_some());
let channels = channels.unwrap();
assert_eq!(channels.len(), 3);
assert!(channels.iter().any(|v| v.as_str() == Some("pytorch")));
assert!(channels.iter().any(|v| v.as_str() == Some("conda-forge")));
assert!(channels.iter().any(|v| v.as_str() == Some("huggingface")));
}
#[test]
fn test_extract_environment_yaml_ringer_conda_dependencies() {
let path = PathBuf::from("testdata/conda/conda-yaml/ringer/environment.yaml");
let package_data = CondaEnvironmentYmlParser::extract_first_package(&path);
let deps = &package_data.dependencies;
let pytorch = deps.iter().find(|d| {
d.purl
.as_deref()
.is_some_and(|p| p.contains("pytorch/pytorch"))
});
assert!(pytorch.is_some());
let pytorch = pytorch.unwrap();
assert_eq!(
pytorch.purl,
Some("pkg:conda/pytorch/pytorch@1.12".to_string())
);
assert_eq!(pytorch.extracted_requirement, Some("=1.12".to_string()));
assert_eq!(pytorch.is_pinned, Some(true));
assert_eq!(pytorch.is_runtime, Some(true));
let transformers = deps.iter().find(|d| {
d.purl
.as_deref()
.is_some_and(|p| p.contains("transformers"))
});
assert!(transformers.is_some());
let transformers = transformers.unwrap();
assert_eq!(
transformers.purl,
Some("pkg:conda/huggingface/transformers@4.11.3".to_string())
);
let numpy = deps
.iter()
.find(|d| d.purl.as_deref().is_some_and(|p| p.contains("numpy")));
assert!(numpy.is_some());
let numpy = numpy.unwrap();
assert_eq!(numpy.purl, Some("pkg:conda/conda-forge/numpy".to_string()));
let ray = deps
.iter()
.find(|d| d.purl.as_deref().is_some_and(|p| p.contains("ray")));
assert!(ray.is_some());
let ray = ray.unwrap();
assert_eq!(ray.purl, Some("pkg:pypi/ray".to_string()));
assert_eq!(ray.scope, Some("dependencies".to_string()));
}
#[test]
fn test_extract_environment_yaml_ringer_pip_filtering() {
let path = PathBuf::from("testdata/conda/conda-yaml/ringer/environment.yaml");
let package_data = CondaEnvironmentYmlParser::extract_first_package(&path);
let deps = &package_data.dependencies;
assert!(
!deps
.iter()
.any(|d| d.purl.as_deref().is_some_and(|p| p.contains("python")))
);
assert!(
!deps
.iter()
.any(|d| d.purl.as_deref().is_some_and(|p| p.contains("pip")))
);
}
#[test]
fn test_parse_conda_requirement_with_namespace_pytorch() {
let dep = parse_conda_requirement("pytorch::pytorch=1.12", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.purl, Some("pkg:conda/pytorch/pytorch@1.12".to_string()));
assert_eq!(dep.scope, Some("run".to_string()));
assert_eq!(dep.is_pinned, Some(true));
assert_eq!(
dep.extra_data
.as_ref()
.and_then(|m| m.get("channel"))
.and_then(|v| v.as_str()),
Some("pytorch")
);
}
#[test]
fn test_parse_conda_requirement_with_url_prefix_does_not_become_namespace() {
let dep = parse_conda_requirement("https://104.17.92.24:443::flask=1.0.2", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.purl, Some("pkg:conda/flask@1.0.2".to_string()));
assert_eq!(
dep.extra_data
.as_ref()
.and_then(|m| m.get("channel_url"))
.and_then(|v| v.as_str()),
Some("https://104.17.92.24:443")
);
}
#[test]
fn test_parse_conda_requirement_trimmed() {
let dep = parse_conda_requirement(" cortexpy ==0.45.7 ", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert!(dep.purl.as_deref().is_some_and(|p| p.contains("cortexpy")));
assert_eq!(dep.extracted_requirement, Some("==0.45.7".to_string()));
}
#[test]
fn test_extract_jinja2_variables_with_spaces() {
let content = "{% set version = \"1.2.3\" %}";
let vars = extract_jinja2_variables(content);
assert_eq!(vars.len(), 1);
assert_eq!(vars.get("version"), Some(&"1.2.3".to_string()));
}
#[test]
fn test_apply_jinja2_substitutions_multiple_occurrences() {
let mut variables = HashMap::new();
variables.insert("version".to_string(), "1.0".to_string());
let content = "url: https://example.com/{{ version }}/file-{{ version }}.tar.gz";
let result = apply_jinja2_substitutions(content, &variables);
assert_eq!(result.matches("1.0").count(), 2);
}
#[test]
fn test_parse_conda_requirement_empty_version() {
let dep = parse_conda_requirement("package=", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert!(dep.purl.as_deref().is_some_and(|p| p.contains("package")));
}
#[test]
fn test_conda_environment_yml_is_match_hyphenated() {
let valid_path = PathBuf::from("/some/path/conda-env.yaml");
assert!(CondaEnvironmentYmlParser::is_match(&valid_path));
}
#[test]
fn test_parse_conda_requirement_double_equals() {
let dep = parse_conda_requirement("package ==1.2.3", "run");
assert!(dep.is_some());
let dep = dep.unwrap();
assert_eq!(dep.extracted_requirement, Some("==1.2.3".to_string()));
assert_eq!(dep.is_pinned, Some(false));
}
}