Skip to content
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
20 changes: 18 additions & 2 deletions conda_recipe_v2_schema/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,9 +500,21 @@ class DownstreamTestElement(StrictBaseModel):
)


class FileChecks(StrictBaseModel):
exists: ConditionalList[NonEmptyStr] | None = Field(
default=[],
description="Files or glob patterns that must exist anywhere inside the package.",
)
not_exists: ConditionalList[NonEmptyStr] | None = Field(
default=[],
description="Files or glob patterns that must NOT exist anywhere inside the package.",
)


class PackageContentTestInner(StrictBaseModel):
files: ConditionalList[NonEmptyStr] | None = Field(
default=[], description="Files that should be in the package"
files: ConditionalList[NonEmptyStr] | FileChecks | None = Field(
default=None,
description="Files expectations for the whole package. Can be a list of files/globs or an object with exists/not_exists.",
)
include: ConditionalList[NonEmptyStr] | None = Field(
default=[],
Expand All @@ -520,6 +532,10 @@ class PackageContentTestInner(StrictBaseModel):
default=[],
description="Files that should be in the `lib/` folder of the package. This folder is found under `$PREFIX/lib` on Unix and %PREFIX%/Library/lib on Windows.",
)
strict: bool = Field(
default=False,
description="When true, the package must not contain any files other than those specified.",
)


class PackageContentTest(StrictBaseModel):
Expand Down
82 changes: 80 additions & 2 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,75 @@
"title": "DynamicLinking",
"type": "object"
},
"FileChecks": {
"additionalProperties": false,
"properties": {
"exists": {
"anyOf": [
{
"minLength": 1,
"type": "string"
},
{
"$ref": "#/$defs/IfStatement"
},
{
"items": {
"anyOf": [
{
"minLength": 1,
"type": "string"
},
{
"$ref": "#/$defs/IfStatement"
}
]
},
"type": "array"
},
{
"type": "null"
}
],
"default": [],
"description": "Files or glob patterns that must exist anywhere inside the package.",
"title": "Exists"
},
"not_exists": {
"anyOf": [
{
"minLength": 1,
"type": "string"
},
{
"$ref": "#/$defs/IfStatement"
},
{
"items": {
"anyOf": [
{
"minLength": 1,
"type": "string"
},
{
"$ref": "#/$defs/IfStatement"
}
]
},
"type": "array"
},
{
"type": "null"
}
],
"default": [],
"description": "Files or glob patterns that must NOT exist anywhere inside the package.",
"title": "Not Exists"
}
},
"title": "FileChecks",
"type": "object"
},
"FileScript": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -2945,12 +3014,15 @@
},
"type": "array"
},
{
"$ref": "#/$defs/FileChecks"
},
{
"type": "null"
}
],
"default": [],
"description": "Files that should be in the package",
"default": null,
"description": "Files expectations for the whole package. Can be a list of files/globs or an object with exists/not_exists.",
"title": "Files"
},
"include": {
Expand Down Expand Up @@ -3076,6 +3148,12 @@
"default": [],
"description": "Files that should be in the `lib/` folder of the package. This folder is found under `$PREFIX/lib` on Unix and %PREFIX%/Library/lib on Windows.",
"title": "Lib"
},
"strict": {
"default": false,
"description": "When true, the package must not contain any files other than those specified.",
"title": "Strict",
"type": "boolean"
}
},
"title": "PackageContentTestInner",
Expand Down
80 changes: 80 additions & 0 deletions tests/test_recipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,83 @@ def test_r_test_invalid_missing_libraries():
recipe_dict = yaml.safe_load(recipe_yaml)
with pytest.raises(PydanticValidationError):
Recipe.validate_python(recipe_dict)


def test_package_contents_strict_valid(recipe_schema):
"""Recipes with a boolean strict flag should validate successfully."""
for strict_val in (True, False):
recipe_yaml = f"""
package:
name: test
version: 1.0.0
tests:
- package_contents:
strict: {str(strict_val).lower()}
files:
- foo.txt
"""
recipe_dict = yaml.safe_load(recipe_yaml)

Recipe.validate_python(recipe_dict)

validate(instance=recipe_dict, schema=recipe_schema)


def test_package_contents_strict_invalid_type(recipe_schema):
"""Non-boolean values for strict should fail validation."""
recipe_yaml = """
package:
name: test
version: 1.0.0
tests:
- package_contents:
strict: 123
"""
recipe_dict = yaml.safe_load(recipe_yaml)

with pytest.raises(PydanticValidationError):
Recipe.validate_python(recipe_dict)

with pytest.raises(ValidationError):
validate(instance=recipe_dict, schema=recipe_schema)


def test_package_contents_exists_and_not_exists_valid(recipe_schema):
"""Recipes using files.exists / files.not_exists should validate successfully."""
recipe_yaml = """
package:
name: test
version: 1.0.0
tests:
- package_contents:
files:
exists:
- bar.txt
not_exists:
- secret.key
- '*.pem'
"""
recipe_dict = yaml.safe_load(recipe_yaml)

Recipe.validate_python(recipe_dict)
validate(instance=recipe_dict, schema=recipe_schema)


def test_package_contents_exists_invalid_type(recipe_schema):
"""Non-string/non-list values for exists should fail validation."""
recipe_yaml = """
package:
name: test
version: 1.0.0
tests:
- package_contents:
files:
exists: 123
"""
recipe_dict = yaml.safe_load(recipe_yaml)

with pytest.raises(PydanticValidationError):
Recipe.validate_python(recipe_dict)

with pytest.raises(ValidationError):
validate(instance=recipe_dict, schema=recipe_schema)