Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce python_version in tests[0].python #1170

Merged
merged 9 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tutorials/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ requirements:
tests:
- script:
- bibtex-tidy --version
```
```
1 change: 0 additions & 1 deletion scripts/activate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ export CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS="-C link-arg=-Wl,-rpath,$CONDA
export CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS="-C link-arg=-Wl,-rpath,$CONDA_PREFIX/lib"

export RATTLER_BUILD_PATH="$PIXI_PROJECT_ROOT/target-pixi/release/rattler-build"

71 changes: 66 additions & 5 deletions src/package_test/run_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ use url::Url;
use crate::{
env_vars,
metadata::PlatformWithVirtualPackages,
recipe::parser::{CommandsTest, DownstreamTest, PythonTest, Script, ScriptContent, TestType},
recipe::parser::{
CommandsTest, DownstreamTest, PythonTest, PythonVersion, Script, ScriptContent, TestType,
},
render::solver::create_environment,
source::copy_dir::CopyDir,
tool_configuration,
Expand Down Expand Up @@ -461,18 +463,78 @@ impl PythonTest {
prefix: &Path,
config: &TestConfiguration,
) -> Result<(), TestError> {
let span = tracing::info_span!("Running python test");
let span = tracing::info_span!("Running python test(s)");
let _guard = span.enter();

// The version spec of the package being built
let match_spec = MatchSpec::from_str(
format!("{}={}={}", pkg.name, pkg.version, pkg.build_string).as_str(),
ParseStrictness::Lenient,
)?;
let mut dependencies = vec![match_spec];

// The dependencies for the test environment
// - python_version: null -> { "": ["mypackage=xx=xx"]}
// - python_version: 3.12 -> { "3.12": ["python=3.12", "mypackage=xx=xx"]}
// - python_version: [3.12, 3.13] -> { "3.12": ["python=3.12", "mypackage=xx=xx"], "3.13": ["python=3.13", "mypackage=xx=xx"]}
let mut dependencies_map: HashMap<String, Vec<MatchSpec>> = match &self.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])]),
};

// Add `pip` if pip_check is set to true
if self.pip_check {
dependencies.push(MatchSpec::from_str("pip", ParseStrictness::Strict).unwrap());
dependencies_map.iter_mut().for_each(|(_, v)| {
v.push(MatchSpec::from_str("pip", ParseStrictness::Strict).unwrap())
});
}

// Run tests for each python version
for (python_version, dependencies) in dependencies_map {
self.run_test_inner(python_version, dependencies, path, prefix, config)
.await?;
}

Ok(())
}

async fn run_test_inner(
&self,
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();

create_environment(
"test",
&dependencies,
Expand Down Expand Up @@ -526,7 +588,6 @@ impl PythonTest {
console::style(console::Emoji("✔", "")).green()
);
}

Ok(())
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/recipe/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub use self::{
source::{GitRev, GitSource, GitUrl, PathSource, Source, UrlSource},
test::{
CommandsTest, CommandsTestFiles, CommandsTestRequirements, DownstreamTest,
PackageContentsTest, PythonTest, TestType,
PackageContentsTest, PythonTest, PythonVersion, TestType,
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: src/recipe/parser/test.rs
assertion_line: 505
expression: yaml_serde
snapshot_kind: text
---
- python:
imports:
- pandas
python_version: '3.10'
- python:
imports:
- pandas
python_version:
- '3.10'
- '3.12'
116 changes: 113 additions & 3 deletions src/recipe/parser/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,26 @@ fn is_true(value: &bool) -> bool {
*value
}

/// The Python version(s) to test the imports against.
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PythonVersion {
/// A single python version
Single(String),
/// Multiple python versions
Multiple(Vec<String>),
/// No python version specified
#[default]
None,
}

impl PythonVersion {
/// Check if the python version is none
pub fn is_none(&self) -> bool {
matches!(self, PythonVersion::None)
}
}

/// A special Python test that checks if the imports are available and runs `pip check`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PythonTest {
Expand All @@ -86,13 +106,17 @@ pub struct PythonTest {
/// Whether to run `pip check` or not (default to true)
#[serde(default = "pip_check_true", skip_serializing_if = "is_true")]
pub pip_check: bool,
/// Python version(s) to test against. If not specified, the default python version is used.
#[serde(default, skip_serializing_if = "PythonVersion::is_none")]
pub python_version: PythonVersion,
}

impl Default for PythonTest {
fn default() -> Self {
Self {
imports: Vec::new(),
pip_check: true,
python_version: PythonVersion::None,
}
}
}
Expand Down Expand Up @@ -206,7 +230,6 @@ impl TryConvertNode<TestType> for RenderedMappingNode {

self.iter().map(|(key, value)| {
let key_str = key.as_str();

match key_str {
"python" => {
let python = as_mapping(value, key_str)?.try_convert(key_str)?;
Expand Down Expand Up @@ -245,7 +268,7 @@ impl TryConvertNode<PythonTest> for RenderedMappingNode {
fn try_convert(&self, _name: &str) -> Result<PythonTest, Vec<PartialParsingError>> {
let mut python_test = PythonTest::default();

validate_keys!(python_test, self.iter(), imports, pip_check);
validate_keys!(python_test, self.iter(), imports, pip_check, python_version);

if python_test.imports.is_empty() {
Err(vec![_partialerror!(
Expand All @@ -259,6 +282,35 @@ impl TryConvertNode<PythonTest> for RenderedMappingNode {
}
}

impl TryConvertNode<PythonVersion> for RenderedNode {
fn try_convert(&self, _name: &str) -> Result<PythonVersion, Vec<PartialParsingError>> {
let python_version = match self {
RenderedNode::Mapping(_) => Err(vec![_partialerror!(
*self.span(),
ErrorKind::InvalidField("expected string, sequence or null".into()),
)])?,
RenderedNode::Scalar(version) => PythonVersion::Single(version.to_string()),
RenderedNode::Sequence(versions) => versions
.iter()
.map(|v| {
v.as_scalar()
.ok_or_else(|| {
vec![_partialerror!(
*self.span(),
ErrorKind::InvalidField("invalid value".into()),
)]
})
.map(|s| s.to_string())
})
.collect::<Result<Vec<String>, _>>()
.map(PythonVersion::Multiple)?,
RenderedNode::Null(_) => PythonVersion::None,
};

Ok(python_version)
}
}

///////////////////////////
/// Downstream Test ///
///////////////////////////
Expand Down Expand Up @@ -372,7 +424,10 @@ mod test {
use super::TestType;
use insta::assert_snapshot;

use crate::recipe::custom_yaml::{RenderedNode, TryConvertNode};
use crate::recipe::{
custom_yaml::{RenderedNode, TryConvertNode},
parser::test::PythonVersion,
};

#[test]
fn test_parsing() {
Expand Down Expand Up @@ -424,4 +479,59 @@ mod test {
let yaml_serde = serde_yaml::to_string(&tests).unwrap();
assert_snapshot!(yaml_serde);
}

#[test]
fn test_python_parsing() {
let test_section = r#"
tests:
- python:
imports:
- pandas
python_version: "3.10"
- python:
imports:
- pandas
python_version: ["3.10", "3.12"]
"#;

// parse the YAML
let yaml_root = RenderedNode::parse_yaml(0, test_section)
.map_err(|err| vec![err])
.unwrap();
let tests_node = yaml_root.as_mapping().unwrap().get("tests").unwrap();
let tests: Vec<TestType> = tests_node.try_convert("tests").unwrap();

let yaml_serde = serde_yaml::to_string(&tests).unwrap();
assert_snapshot!(yaml_serde);

// from yaml
let tests: Vec<TestType> = serde_yaml::from_str(&yaml_serde).unwrap();
let t = tests.first();

match t {
Some(TestType::Python { python }) => {
assert_eq!(python.imports, vec!["pandas"]);
assert!(python.pip_check);
assert_eq!(
python.python_version,
PythonVersion::Single("3.10".to_string())
);
}
_ => panic!("expected python test"),
}

let t2 = tests.get(1);

match t2 {
Some(TestType::Python { python }) => {
assert_eq!(python.imports, vec!["pandas"]);
assert!(python.pip_check);
assert_eq!(
python.python_version,
PythonVersion::Multiple(vec!["3.10".to_string(), "3.12".to_string()])
);
}
_ => panic!("expected python test"),
}
}
}
2 changes: 1 addition & 1 deletion test-data/recipes/pin_subpackage/recipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ outputs:
noarch: generic
requirements:
run:
- ${{ pin_subpackage(name, exact=true) }}
- ${{ pin_subpackage(name, exact=true) }}
Loading