Harden workflow test JSON schema: titles, descriptions, optionality

Addresses the workflow-test schema usability regressions flagged against
galaxy-tool-util-schema 0.4.0:

- TestJob.doc: make optional (was incorrectly required).
- TestJob + BaseTestOutputModel: port per-field descriptions from the
  0.2.0 vendored schema.
- Tests: name the top-level RootModel; emit $schema, title
  (GalaxyWorkflowTests), and description.
- Job and assertion_list: promote type aliases to named RootModel
  subclasses so $defs keys aren't mangled.
- Drop field_title_generator from StrictModel/_StrictJobModel/
  ToolSourceBaseModel; add explicit title= on every Field and
  model_config(title=) on every test-schema model class. Codegen
  template emits titles on generated assertion models.
- Fix codegen models_path to point at tool_util_models/assertions.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
John Chilton
2026-04-22 09:53:59 -04:00
co-authored by Claude Opus 4.7
parent 7dc5eab037
commit 9b225459b8
6 changed files with 1063 additions and 254 deletions
+30 -11
View File
@@ -27,7 +27,7 @@ from galaxy.tool_util.verify.asserts import assertion_module_and_functions
from galaxy.tool_util.verify.asserts._types import AssertionParameter as AssertionParameterAnnotation
from galaxy.util.commands import shell
models_path = os.path.join(os.path.dirname(__file__), "assertion_models.py")
models_path = os.path.join(os.path.dirname(__file__), "..", "..", "tool_util_models", "assertions.py")
galaxy_xsd_path = os.path.join(os.path.dirname(__file__), "..", "xsd", "galaxy.xsd")
Children = Literal["allowed", "required", "forbidden"]
@@ -114,17 +114,19 @@ def check_non_negative_if_int(v: typing.Any):
class base_{{assertion.name}}_model(AssertionModel):
'''base model for {{assertion.name}} describing attributes.'''
model_config = ConfigDict(extra="forbid", title="base_{{assertion.name}}_model")
{% for parameter in assertion.parameters %}
{% if not parameter.is_deprecated %}
{{ parameter.name }}: {{ parameter.type_str }} = Field(
{{ parameter.field_default_str }},
title="{{ parameter.title }}",
description={{ assertion.name }}_{{ parameter.name }}_description,
)
{% endif %}
{% endfor %}
{% if assertion.children in ["required", "allowed"] %}
children: typing.Optional["assertion_list"] = None
asserts: typing.Optional["assertion_list"] = None
children: typing.Optional["assertion_list"] = Field(None, title="Children")
asserts: typing.Optional["assertion_list"] = Field(None, title="Asserts")
{% if assertion.children == "required" %}
@model_validator(mode='before')
@@ -139,17 +141,19 @@ class base_{{assertion.name}}_model(AssertionModel):
class base_{{assertion.name}}_model_relaxed(AssertionModel):
'''base model for {{assertion.name}} describing attributes.'''
model_config = ConfigDict(extra="forbid", title="base_{{assertion.name}}_model_relaxed")
{% for parameter in assertion.parameters %}
{% if not parameter.is_deprecated %}
{{ parameter.name }}: {{ parameter.lax_type_str }} = Field(
{{ parameter.field_default_str }},
title="{{ parameter.title }}",
description={{ assertion.name }}_{{ parameter.name }}_description,
)
{% endif %}
{% endfor %}
{% if assertion.children in ["required", "allowed"] %}
children: typing.Optional["assertion_list"] = None
asserts: typing.Optional["assertion_list"] = None
children: typing.Optional["assertion_list"] = Field(None, title="Children")
asserts: typing.Optional["assertion_list"] = Field(None, title="Asserts")
{% if assertion.children == "required" %}
@model_validator(mode='before')
@@ -164,15 +168,18 @@ class base_{{assertion.name}}_model_relaxed(AssertionModel):
class {{assertion.name}}_model(base_{{assertion.name}}_model):
r\"\"\"{{ assertion.docstring }}\"\"\"
that: Literal["{{assertion.name}}"] = "{{assertion.name}}"
model_config = ConfigDict(extra="forbid", title="{{ assertion.title }}")
that: Literal["{{assertion.name}}"] = Field("{{assertion.name}}", title="That")
class {{assertion.name}}_model_nested(AssertionModel):
r\"\"\"Nested version of this assertion model.\"\"\"
{{assertion.name}}: base_{{assertion.name}}_model
model_config = ConfigDict(extra="forbid", title="{{ assertion.title }} (Nested)")
{{assertion.name}}: base_{{assertion.name}}_model = Field(..., title="{{ assertion.title }}")
class {{assertion.name}}_model_relaxed(base_{{assertion.name}}_model_relaxed):
r\"\"\"{{ assertion.docstring }}\"\"\"
that: Literal["{{assertion.name}}"] = "{{assertion.name}}"
model_config = ConfigDict(extra="forbid", title="{{ assertion.title }} (Relaxed)")
that: Literal["{{assertion.name}}"] = Field("{{assertion.name}}", title="That")
{% endfor %}
any_assertion_model_flat = Annotated[typing.Union[
@@ -193,15 +200,19 @@ any_assertion_model_flat_relaxed = Annotated[typing.Union[
{% endfor %}
], Field(discriminator="that")]
assertion_list = RootModel[typing.List[typing.Union[any_assertion_model_flat, any_assertion_model_nested]]]
class assertion_list(RootModel[typing.List[typing.Union[any_assertion_model_flat, any_assertion_model_nested]]]):
model_config = ConfigDict(title="assertion_list")
# used to model what the XML conversion should look like - not meant to be consumed outside of
# of Galaxy internals / linting.
relaxed_assertion_list = RootModel[typing.List[any_assertion_model_flat_relaxed]]
class relaxed_assertion_list(RootModel[typing.List[any_assertion_model_flat_relaxed]]):
model_config = ConfigDict(title="relaxed_assertion_list")
class assertion_dict(AssertionModel):
model_config = ConfigDict(extra="forbid", title="assertion_dict")
{% for assertion in assertions %}
{{assertion.name}}: typing.Optional[base_{{assertion.name}}_model] = None
{{assertion.name}}: typing.Optional[base_{{assertion.name}}_model] = Field(None, title="{{ assertion.title }}")
{% endfor %}
@@ -338,6 +349,10 @@ class AssertionParameter:
self.type = type
self.default_value = default_value
@property
def title(self) -> str:
return " ".join(w.capitalize() for w in self.name.split("_"))
@property
def description(self) -> str:
type = self.type
@@ -490,6 +505,10 @@ class Assertion:
self.children = children
self.module_and_function = module_and_function
@property
def title(self) -> str:
return "Assert " + " ".join(w.capitalize() for w in self.name.split("_"))
def arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=DESCRIPTION)
+205 -36
View File
@@ -172,33 +172,153 @@ class ParsedTool(ToolSourceBaseModel):
class BaseTestOutputModel(StrictModel):
file: Optional[str] = None
path: Optional[str] = None
location: Optional[AnyUrl] = None
ftype: Optional[str] = None
sort: Optional[bool] = None
compare: Optional[OutputCompareType] = None
checksum: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
asserts: Optional[assertions] = None
delta: Optional[int] = None
delta_frac: Optional[float] = None
lines_diff: Optional[int] = None
decompress: Optional[bool] = None
model_config = ConfigDict(extra="forbid", title="BaseTestOutputModel")
file: Annotated[
Optional[str],
Field(
title="File",
description=(
"Name of the output file stored in the target `test-data` directory that will be used to "
"compare against the results of executing the tool via the functional test framework."
),
),
] = None
path: Annotated[
Optional[str],
Field(title="Path", description="Filesystem path to a local output file used for comparison."),
] = None
location: Annotated[
Optional[AnyUrl],
Field(
title="Location",
description=(
"URL that points to a remote output file that will be downloaded and used for output "
"comparison. Use only when the file cannot be included in the `test-data` folder. May be "
"combined with `file` (downloads when missing on disk) or used alone (filename inferred "
"from the URL). A `checksum` is also used to verify the download when provided."
),
),
] = None
ftype: Annotated[
Optional[str],
Field(
title="File Type",
description=(
"If specified, this value is checked against the corresponding output's data type. "
"If these do not match, the test will fail."
),
),
] = None
sort: Annotated[
Optional[bool],
Field(
title="Sort",
description=(
"Applies only if `compare` is `diff`, `re_match` or `re_match_multiline`. Sorts the lines "
"of the history data set before comparison; for `diff` and `re_match` the local file is "
"also sorted. Useful for non-deterministic output."
),
),
] = None
compare: Annotated[
Optional[OutputCompareType],
Field(
title="Compare",
description="Comparison mode used when matching the output against the reference file.",
),
] = None
checksum: Annotated[
Optional[str],
Field(
title="Checksum",
description=(
"The target output's checksum should match the value specified here, in the form "
"`hash_type$hash_value` (e.g. `sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041`). Useful "
"for large static files where uploading the whole file is inconvenient."
),
),
] = None
metadata: Annotated[
Optional[Dict[str, Any]],
Field(
title="Metadata",
description="Mapping of metadata keys to expected values for this output.",
),
] = None
asserts: Annotated[
Optional[assertions],
Field(title="Asserts", description="Assertions about the content of the output."),
] = None
delta: Annotated[
Optional[int],
Field(
title="Delta",
description=(
"If `compare` is set to `sim_size`, the maximum allowed absolute size difference (in "
"bytes) between the generated data set and the reference file in `test-data/`. Default "
"is 10000 bytes. Can be combined with `delta_frac`."
),
),
] = None
delta_frac: Annotated[
Optional[float],
Field(
title="Delta Frac",
description=(
"If `compare` is set to `sim_size`, the maximum allowed relative size difference between "
"the generated data set and the reference file in `test-data/`. 0.1 means the generated "
"file can differ by at most 10%. Default is not to check for relative size difference. "
"Can be combined with `delta`."
),
),
] = None
lines_diff: Annotated[
Optional[int],
Field(
title="Lines Diff",
description=(
"Applies when `compare` is set to `diff`, `re_match`, or `contains`. For `diff`, the "
"number of lines of difference to allow (a modified line counts as two: one added, one "
"removed)."
),
),
] = None
decompress: Annotated[
Optional[bool],
Field(
title="Decompress",
description=(
"If true, decompress files before comparison. Applies to assertions expressed with "
"`assert_contents` or `compare` set to anything but `sim_size`. Useful for testing "
"compressed outputs that are non-deterministic despite having deterministic decompressed "
"contents. By default, only files compressed with bz2, gzip and zip are automatically "
"decompressed."
),
),
] = None
class TestDataOutputAssertions(BaseTestOutputModel):
class_: Optional[Literal["File"]] = Field("File", alias="class")
model_config = ConfigDict(extra="forbid", title="TestDataOutputAssertions")
class_: Optional[Literal["File"]] = Field("File", alias="class", title="Class")
class TestCollectionCollectionElementAssertions(StrictModel):
class_: Optional[Literal["Collection"]] = Field("Collection", alias="class")
elements: Optional[Dict[str, "TestCollectionElementAssertion"]] = None
element_tests: Optional[Dict[str, "TestCollectionElementAssertion"]] = None
model_config = ConfigDict(extra="forbid", title="TestCollectionCollectionElementAssertions")
class_: Optional[Literal["Collection"]] = Field("Collection", alias="class", title="Class")
elements: Annotated[
Optional[Dict[str, "TestCollectionElementAssertion"]],
Field(title="Elements"),
] = None
element_tests: Annotated[
Optional[Dict[str, "TestCollectionElementAssertion"]],
Field(title="Element Tests"),
] = None
class TestCollectionDatasetElementAssertions(BaseTestOutputModel):
class_: Optional[Literal["File"]] = Field("File", alias="class")
model_config = ConfigDict(extra="forbid", title="TestCollectionDatasetElementAssertions")
class_: Optional[Literal["File"]] = Field("File", alias="class", title="Class")
def _discriminate_collection_element(v):
@@ -224,16 +344,24 @@ TestCollectionCollectionElementAssertions.model_rebuild()
class CollectionAttributes(StrictModel):
collection_type: CollectionType = None
model_config = ConfigDict(extra="forbid", title="CollectionAttributes")
collection_type: Annotated[CollectionType, Field(title="Collection Type")] = None
class TestCollectionOutputAssertions(StrictModel):
class_: Optional[Literal["Collection"]] = Field("Collection", alias="class")
elements: Optional[Dict[str, TestCollectionElementAssertion]] = None
element_tests: Optional[Dict[str, "TestCollectionElementAssertion"]] = None
element_count: Optional[int] = None
attributes: Optional[CollectionAttributes] = None
collection_type: CollectionType = None
model_config = ConfigDict(extra="forbid", title="TestCollectionOutputAssertions")
class_: Optional[Literal["Collection"]] = Field("Collection", alias="class", title="Class")
elements: Annotated[
Optional[Dict[str, TestCollectionElementAssertion]],
Field(title="Elements"),
] = None
element_tests: Annotated[
Optional[Dict[str, "TestCollectionElementAssertion"]],
Field(title="Element Tests"),
] = None
element_count: Annotated[Optional[int], Field(title="Element Count")] = None
attributes: Annotated[Optional[CollectionAttributes], Field(title="Attributes")] = None
collection_type: Annotated[CollectionType, Field(title="Collection Type")] = None
TestOutputLiteral = Union[bool, int, float, str]
@@ -267,23 +395,25 @@ TestInputValue = Union[bool, int, float, str, List[Any], Dict[str, Any]]
class YamlTestCredentialValue(StrictModel):
name: Annotated[str, Field(description="Name of the credential variable or secret.")]
value: Annotated[str, Field(description="Value of the credential variable or secret.")]
model_config = ConfigDict(extra="forbid", title="YamlTestCredentialValue")
name: Annotated[str, Field(title="Name", description="Name of the credential variable or secret.")]
value: Annotated[str, Field(title="Value", description="Value of the credential variable or secret.")]
class YamlTestCredential(StrictModel):
name: Annotated[str, Field(description="Name of the credentials group.")]
model_config = ConfigDict(extra="forbid", title="YamlTestCredential")
name: Annotated[str, Field(title="Name", description="Name of the credentials group.")]
variables: Annotated[
List[YamlTestCredentialValue],
Field(description="Variables exposed to the tool environment."),
Field(title="Variables", description="Variables exposed to the tool environment."),
] = []
secrets: Annotated[
List[YamlTestCredentialValue],
Field(description="Secrets exposed to the tool environment."),
Field(title="Secrets", description="Secrets exposed to the tool environment."),
] = []
version: Annotated[
Optional[str],
Field(description="Version of the credential definition."),
Field(title="Version", description="Version of the credential definition."),
] = None
@@ -341,13 +471,52 @@ JobDict = Dict[str, Any]
class TestJob(StrictModel):
doc: Optional[str]
job: Job
outputs: Dict[str, TestOutputAssertions]
expect_failure: Optional[bool] = False
model_config = ConfigDict(extra="forbid", title="TestJob")
doc: Annotated[
Optional[str],
Field(title="Doc", description="Describes the purpose of the test."),
] = None
job: Annotated[
Job,
Field(
title="Job",
description=(
"Defines the job to execute. Can be a path to a file or an inline dictionary describing "
"the job inputs."
),
),
]
outputs: Annotated[
Dict[str, TestOutputAssertions],
Field(
title="Outputs",
description=(
"Defines assertions about outputs (datasets, collections or parameters). Each key "
"corresponds to a labeled output; values are dictionaries describing the expected output."
),
),
]
expect_failure: Annotated[
Optional[bool],
Field(
title="Expect Failure",
description="If true, the workflow is expected to produce an error.",
),
] = False
Tests = RootModel[List[TestJob]]
class Tests(RootModel[List[TestJob]]):
model_config = ConfigDict(
title="GalaxyWorkflowTests",
json_schema_extra={
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": (
"Galaxy workflow tests file — a YAML list of test entries asserting the expected "
"inputs and outputs of a workflow run."
),
},
)
# TODO: typed dict versions of all thee above for verify code - make this Dict[str, Any] here more
# specific.
+2 -2
View File
@@ -11,11 +11,11 @@ from typing_extensions import Annotated
class ToolSourceBaseModel(BaseModel):
model_config = ConfigDict(field_title_generator=lambda field_name, field_info: field_name.lower())
pass
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid", field_title_generator=lambda field_name, field_info: field_name.lower())
model_config = ConfigDict(extra="forbid")
def _check_collection_type(v: str) -> str:
File diff suppressed because it is too large Load Diff
+52 -43
View File
@@ -43,60 +43,65 @@ class _StrictJobModel(StrictModel):
model_config = ConfigDict(
extra="forbid",
populate_by_name=True,
field_title_generator=lambda field_name, field_info: field_name.lower(),
)
class HashEntry(_StrictJobModel):
hash_function: HashFunctionNames
hash_value: str
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="HashEntry")
hash_function: Annotated[HashFunctionNames, Field(title="Hash Function")]
hash_value: Annotated[str, Field(title="Hash Value")]
class BaseFile(_StrictJobModel):
"""Fields common to every ``class: File`` variant."""
class_: Literal["File"] = Field(alias="class")
filetype: Optional[str] = None
dbkey: Optional[str] = None
decompress: Optional[bool] = None
to_posix_lines: Optional[bool] = None
space_to_tab: Optional[bool] = None
deferred: Optional[bool] = None
name: Optional[str] = None
info: Optional[str] = None
tags: Optional[list[str]] = None
hashes: Optional[list[HashEntry]] = None
identifier: Optional[str] = None
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="BaseFile")
class_: Literal["File"] = Field(alias="class", title="Class")
filetype: Annotated[Optional[str], Field(title="File Type")] = None
dbkey: Annotated[Optional[str], Field(title="Dbkey")] = None
decompress: Annotated[Optional[bool], Field(title="Decompress")] = None
to_posix_lines: Annotated[Optional[bool], Field(title="To POSIX Lines")] = None
space_to_tab: Annotated[Optional[bool], Field(title="Space To Tab")] = None
deferred: Annotated[Optional[bool], Field(title="Deferred")] = None
name: Annotated[Optional[str], Field(title="Name")] = None
info: Annotated[Optional[str], Field(title="Info")] = None
tags: Annotated[Optional[list[str]], Field(title="Tags")] = None
hashes: Annotated[Optional[list[HashEntry]], Field(title="Hashes")] = None
identifier: Annotated[Optional[str], Field(title="Identifier")] = None
class LocationFile(BaseFile):
location: str
path: Optional[str] = None
contents: Optional[str] = None
composite_data: Optional[list[str]] = None
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="LocationFile")
location: Annotated[str, Field(title="Location")]
path: Annotated[Optional[str], Field(title="Path")] = None
contents: Annotated[Optional[str], Field(title="Contents")] = None
composite_data: Annotated[Optional[list[str]], Field(title="Composite Data")] = None
class PathFile(BaseFile):
path: str
location: Optional[str] = None
contents: Optional[str] = None
composite_data: Optional[list[str]] = None
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="PathFile")
path: Annotated[str, Field(title="Path")]
location: Annotated[Optional[str], Field(title="Location")] = None
contents: Annotated[Optional[str], Field(title="Contents")] = None
composite_data: Annotated[Optional[list[str]], Field(title="Composite Data")] = None
class ContentsFile(BaseFile):
"""CWL File literal — content inlined as a string."""
contents: str
path: Optional[str] = None
location: Optional[str] = None
composite_data: Optional[list[str]] = None
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="ContentsFile")
contents: Annotated[str, Field(title="Contents")]
path: Annotated[Optional[str], Field(title="Path")] = None
location: Annotated[Optional[str], Field(title="Location")] = None
composite_data: Annotated[Optional[list[str]], Field(title="Composite Data")] = None
class CompositeDataFile(BaseFile):
composite_data: list[str]
path: Optional[str] = None
location: Optional[str] = None
contents: Optional[str] = None
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="CompositeDataFile")
composite_data: Annotated[list[str], Field(title="Composite Data")]
path: Annotated[Optional[str], Field(title="Path")] = None
location: Annotated[Optional[str], Field(title="Location")] = None
contents: Annotated[Optional[str], Field(title="Contents")] = None
def _discriminate_file(v):
@@ -133,12 +138,13 @@ File = Annotated[
class Collection(_StrictJobModel):
class_: Literal["Collection"] = Field(alias="class")
collection_type: CollectionType = None
name: Optional[str] = None
identifier: Optional[str] = None
elements: Optional[list["CollectionElement"]] = None
rows: Optional[dict[str, list]] = None
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="Collection")
class_: Literal["Collection"] = Field(alias="class", title="Class")
collection_type: Annotated[CollectionType, Field(title="Collection Type")] = None
name: Annotated[Optional[str], Field(title="Name")] = None
identifier: Annotated[Optional[str], Field(title="Identifier")] = None
elements: Annotated[Optional[list["CollectionElement"]], Field(title="Elements")] = None
rows: Annotated[Optional[dict[str, list]], Field(title="Rows")] = None
CollectionElement = Annotated[
@@ -155,11 +161,12 @@ class Directory(_StrictJobModel):
does not use it.
"""
class_: Literal["Directory"] = Field(alias="class")
path: Optional[str] = None
location: Optional[str] = None
filetype: Optional[str] = None
name: Optional[str] = None
model_config = ConfigDict(extra="forbid", populate_by_name=True, title="Directory")
class_: Literal["Directory"] = Field(alias="class", title="Class")
path: Annotated[Optional[str], Field(title="Path")] = None
location: Annotated[Optional[str], Field(title="Location")] = None
filetype: Annotated[Optional[str], Field(title="File Type")] = None
name: Annotated[Optional[str], Field(title="Name")] = None
# JobParamValue is non-recursive at the list axis: a job-param list may contain
@@ -179,4 +186,6 @@ JobParamValue = Union[
list[Union[File, str, int, float, bool, None]],
]
Job = RootModel[dict[str, JobParamValue]]
class Job(RootModel[dict[str, JobParamValue]]):
model_config = ConfigDict(title="Job")
@@ -93,3 +93,81 @@ def test_json_schema_emits_discriminator_for_outputs():
schema = Tests.model_json_schema()
dumped = json.dumps(schema)
assert "discriminator" in dumped or '"oneOf"' in dumped
def test_doc_is_optional_on_test_job():
tests = Tests.model_validate([{"job": {}, "outputs": {}}])
assert tests.root[0].doc is None
schema = Tests.model_json_schema()
test_job = schema["$defs"]["TestJob"]
assert "doc" not in test_job.get("required", [])
def test_test_job_top_level_properties_have_descriptions():
schema = Tests.model_json_schema()
props = schema["$defs"]["TestJob"]["properties"]
for name in ("doc", "job", "outputs", "expect_failure"):
assert props[name].get("description"), f"missing description on TestJob.{name}"
def test_test_job_titles_are_human_readable_not_lowercase():
schema = Tests.model_json_schema()
props = schema["$defs"]["TestJob"]["properties"]
assert props["doc"]["title"] == "Doc"
assert props["outputs"]["title"] == "Outputs"
assert props["expect_failure"]["title"] == "Expect Failure"
def test_assertion_model_titles_are_human_readable():
schema = Tests.model_json_schema()
defs = schema["$defs"]
assert defs["has_text_model"]["title"] == "Assert Has Text"
assert defs["has_text_model_nested"]["title"] == "Assert Has Text (Nested)"
# Every field in has_text_model has an explicit title.
for name, prop in defs["has_text_model"]["properties"].items():
assert prop.get("title"), f"has_text_model.{name} missing title"
def test_test_data_output_assertions_field_titles():
schema = Tests.model_json_schema()
props = schema["$defs"]["TestDataOutputAssertions"]["properties"]
assert props["lines_diff"]["title"] == "Lines Diff"
assert props["class"]["title"] == "Class"
assert props["ftype"]["title"] == "File Type"
def test_top_level_schema_has_title_description_and_schema():
schema = Tests.model_json_schema()
assert schema.get("title") == "GalaxyWorkflowTests"
assert schema.get("description")
assert schema.get("$schema") == "https://json-schema.org/draft/2020-12/schema"
def test_root_model_wrappers_have_clean_defs_keys():
schema = Tests.model_json_schema()
defs = schema["$defs"]
assert "Job" in defs
assert "assertion_list" in defs
# None of the RootModel-auto-generated mangled names should appear.
mangled = [k for k in defs if k.startswith("RootModel_")]
assert not mangled, f"unexpected mangled RootModel defs: {mangled}"
def test_test_data_output_assertions_properties_have_descriptions():
schema = Tests.model_json_schema()
props = schema["$defs"]["TestDataOutputAssertions"]["properties"]
for name in (
"asserts",
"metadata",
"file",
"ftype",
"sort",
"checksum",
"compare",
"lines_diff",
"decompress",
"delta",
"delta_frac",
"location",
):
assert props[name].get("description"), f"missing description on TestDataOutputAssertions.{name}"