From 7c71f62c271aa7edf1e36e3ff30c9b5f4c19c166 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 28 Aug 2026 09:04:49 +0000 Subject: [PATCH 1/5] Scaffold PR4: deterministic unit-test generation Empty commit to open the stacked draft PR. Follow-up commits will generate the per-resource test_resources.py cases (dict + dataclass examples) from the codegen model. Co-authored-by: Isaac From 43774c727e8d412d06be8c0f6202665602f2308b Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 28 Aug 2026 11:22:34 +0000 Subject: [PATCH 2/5] Test the dataclass path in test_add_resource_type test_add_resource_type and test_add_resource_type_dict had byte-identical bodies, both feeding dict_example, so the add_(dataclass) normalization path went untested for the parametrized resources. Feed dataclass_example to the non-_dict variant, mirroring test_add_job vs test_add_job_dict. Co-authored-by: Isaac --- python/databricks_tests/core/test_resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/databricks_tests/core/test_resources.py b/python/databricks_tests/core/test_resources.py index ee2ab7ec40..e27b4331db 100644 --- a/python/databricks_tests/core/test_resources.py +++ b/python/databricks_tests/core/test_resources.py @@ -163,7 +163,7 @@ def test_add_resource_type(tc: TestCase, tpe: _ResourceType): resources, **{ "resource_name": "my_resource", - tpe.singular_name: tc.dict_example, + tpe.singular_name: tc.dataclass_example, }, ) From 2f2a0217a182c211145b36e815a4913436b55807 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 28 Aug 2026 11:22:56 +0000 Subject: [PATCH 3/5] Generate per-resource unit-test cases from the codegen model Stop hand-writing a TestCase per resource in test_resources.py. A new codegen step (generated_test_cases.py, rendered from test_case.py.tmpl) synthesizes dict_example and dataclass_example for every wired resource from the schema model and writes one file per resource under databricks_tests/core/_generated/, collected into test_cases. A newly wired resource now gets its unit-test coverage for free. dict_example and dataclass_example are rendered two independent ways from one synthesized value tree, so the dict->dataclass _transform assertion stays meaningful. Field policy: required fields fully expanded, plus optional composite fields on the resource itself; nested objects contribute only their required fields, which bounds example size and avoids the recursive Task/ForEachTask schema. Optional scalar, deprecated, and private-preview fields are omitted. The hand-written TestCase dataclass moves to _resource_test_case.py so the generated modules can import it without a cycle. Co-authored-by: Isaac --- python/Taskfile.yml | 2 + .../codegen/codegen/generated_test_cases.py | 278 ++++++++++++++++++ python/codegen/codegen/main.py | 4 + python/codegen/codegen/test_case.py.tmpl | 16 + python/databricks_tests/.gitattributes | 4 + .../core/_generated/__init__.py | 21 ++ .../core/_generated/alerts.py | 58 ++++ .../core/_generated/catalogs.py | 35 +++ .../databricks_tests/core/_generated/jobs.py | 92 ++++++ .../core/_generated/pipelines.py | 65 ++++ .../core/_generated/schemas.py | 32 ++ .../core/_generated/volumes.py | 35 +++ .../core/_resource_test_case.py | 12 + .../databricks_tests/core/test_resources.py | 129 +------- 14 files changed, 659 insertions(+), 124 deletions(-) create mode 100644 python/codegen/codegen/generated_test_cases.py create mode 100644 python/codegen/codegen/test_case.py.tmpl create mode 100644 python/databricks_tests/.gitattributes create mode 100644 python/databricks_tests/core/_generated/__init__.py create mode 100644 python/databricks_tests/core/_generated/alerts.py create mode 100644 python/databricks_tests/core/_generated/catalogs.py create mode 100644 python/databricks_tests/core/_generated/jobs.py create mode 100644 python/databricks_tests/core/_generated/pipelines.py create mode 100644 python/databricks_tests/core/_generated/schemas.py create mode 100644 python/databricks_tests/core/_generated/volumes.py create mode 100644 python/databricks_tests/core/_resource_test_case.py diff --git a/python/Taskfile.yml b/python/Taskfile.yml index 602e92028a..49621efd6a 100644 --- a/python/Taskfile.yml +++ b/python/Taskfile.yml @@ -78,6 +78,8 @@ tasks: -exec rm -rf {} \; # core/ is hand-written except for the generated wiring under _generated/. - rm -rf databricks/bundles/core/_generated + # test_resources.py is hand-written except for the generated TestCase data. + - rm -rf databricks_tests/core/_generated - cd codegen && uv run -m pytest codegen_tests - cd codegen && uv run -m codegen.main --output .. # Generated code is fixed and formatted by the global ruff (see ../ruff.toml). diff --git a/python/codegen/codegen/generated_test_cases.py b/python/codegen/codegen/generated_test_cases.py new file mode 100644 index 0000000000..7decc29a89 --- /dev/null +++ b/python/codegen/codegen/generated_test_cases.py @@ -0,0 +1,278 @@ +""" +Generates the per-resource TestCase data driving databricks_tests/core/test_resources.py. + +For every wired resource a file _generated/.py is written (rendered from +test_case.py.tmpl) exposing _test_case() -> (TestCase, _ResourceType). The generated +_generated/__init__.py collects them into `test_cases`, which test_resources.py imports +and parametrizes its per-resource tests off. + +dict_example and dataclass_example are synthesized from one value tree and rendered two +independent ways -- a dict literal and a constructor expression -- so the dict->dataclass +_transform assertion in test_resources.py stays meaningful (the two forms don't share the +runtime transform path). + +Field policy: all required fields (fully expanded), plus optional composite fields +(nested dataclass / list / map / enum) on the resource itself; nested objects contribute +only their required fields, which keeps examples bounded and avoids recursive schemas +(e.g. jobs Task -> ForEachTask -> Task, reachable only through an optional field). Optional +scalar, deprecated, and experimental fields are omitted. +""" + +from dataclasses import dataclass +from pathlib import Path +from string import Template +from typing import Union + +import codegen.jsonschema as openapi +import codegen.packages as packages +from codegen.generated_enum import _camel_to_upper_snake +from codegen.generated_wiring import _WiredResource, _wired_resources + +HEADER = "# Code generated by pydabs-codegen. DO NOT EDIT.\n\n" + +_TEST_CASE_TEMPLATE = Template( + (Path(__file__).parent / "test_case.py.tmpl").read_text() +) + + +# Synthesized value tree. Each node renders both as a dict literal (dict_example) +# and as a constructor expression (dataclass_example). + + +@dataclass +class _Scalar: + dict_src: str + dataclass_src: str + + +@dataclass +class _Enum: + value: str + class_name: str + module: str + member: str + + +@dataclass +class _Object: + class_name: str + module: str + fields: "list[tuple[str, _Value]]" + + +@dataclass +class _List: + item: "_Value" + + +@dataclass +class _Map: + key: str + value: "_Value" + + +_Value = Union[_Scalar, _Enum, _Object, _List, _Map] + + +def _ref_name(ref: str) -> str: + return ref.split("/")[-1] + + +def _is_composite(ref: str) -> bool: + if ref.startswith("#/$defs/slice/") or ref.startswith("#/$defs/map/"): + return True + + return _ref_name(ref) not in packages.PRIMITIVES + + +def _synth_scalar(name: str, hint: str) -> _Scalar: + if name == "string": + return _Scalar(f'"{hint}"', f'"{hint}"') + if name in ("integer", "int", "int64"): + return _Scalar("0", "0") + if name in ("number", "float", "float64"): + return _Scalar("0.0", "0.0") + if name in ("boolean", "bool"): + return _Scalar("True", "True") + + raise ValueError(f"Unknown primitive: {name}") + + +def _synth_ref( + namespace: str, + ref: str, + hint: str, + schemas: dict[str, openapi.Schema], + visiting: set[str], +) -> _Value: + if ref.startswith("#/$defs/slice/"): + element_ref = ref.replace("#/$defs/slice/", "#/$defs/") + + return _List(_synth_ref(namespace, element_ref, hint, schemas, visiting)) + + if ref.startswith("#/$defs/map/"): + # generate_type only ever produces dict[str, str] maps (map/string). + if ref != "#/$defs/map/string": + raise ValueError(f"Unsupported map ref: {ref}") + + return _Map("key", _Scalar('"value"', '"value"')) + + name = _ref_name(ref) + if name in packages.PRIMITIVES: + return _synth_scalar(name, hint) + + schema = schemas[name] + class_name = packages.get_class_name(ref) + module = packages.get_package(namespace, ref) + assert module + + if schema.type == openapi.SchemaType.STRING: + value = schema.enum[0] + + return _Enum(value, class_name, module, _camel_to_upper_snake(value)) + + # Only reachable through required fields at this depth (see _synth_object); a + # required cycle has no finite value, so fail loudly instead of looping. + if name in visiting: + raise ValueError(f"Required-field cycle through '{name}'") + + return _synth_object(namespace, name, schema, schemas, visiting, top_level=False) + + +def _synth_object( + namespace: str, + schema_name: str, + schema: openapi.Schema, + schemas: dict[str, openapi.Schema], + visiting: set[str], + top_level: bool, +) -> _Object: + visiting = visiting | {schema_name} + fields: list[tuple[str, _Value]] = [] + + for field_name, prop in schema.properties.items(): + required = field_name in schema.required + + if not required: + # Nested objects contribute only required fields; on the resource + # itself, also include stable optional composite fields. + if not top_level: + continue + if not _is_composite(prop.ref): + continue + if prop.deprecated or prop.stage == openapi.LaunchStage.PRIVATE_PREVIEW: + continue + + value = _synth_ref(namespace, prop.ref, field_name, schemas, visiting) + fields.append((field_name, value)) + + return _Object( + packages.get_class_name(schema_name), _module_of(namespace, schema_name), fields + ) + + +def _module_of(namespace: str, schema_name: str) -> str: + module = packages.get_package(namespace, schema_name) + assert module + + return module + + +def _render_dict(value: _Value) -> str: + if isinstance(value, _Scalar): + return value.dict_src + if isinstance(value, _Enum): + return f'"{value.value}"' + if isinstance(value, _List): + return f"[{_render_dict(value.item)}]" + if isinstance(value, _Map): + return "{" + f'"{value.key}": {_render_dict(value.value)}' + "}" + + fields = ", ".join( + f'"{name}": {_render_dict(child)}' for name, child in value.fields + ) + + return "{" + fields + "}" + + +def _render_dataclass(value: _Value) -> str: + if isinstance(value, _Scalar): + return value.dataclass_src + if isinstance(value, _Enum): + return f"{value.class_name}.{value.member}" + if isinstance(value, _List): + return f"[{_render_dataclass(value.item)}]" + if isinstance(value, _Map): + return "{" + f'"{value.key}": {_render_dataclass(value.value)}' + "}" + + fields = ", ".join( + f"{name}={_render_dataclass(child)}" for name, child in value.fields + ) + + return f"{value.class_name}({fields})" + + +def _collect_imports(value: _Value, out: set[tuple[str, str]]) -> None: + if isinstance(value, _Enum): + out.add((value.module, value.class_name)) + elif isinstance(value, _Object): + out.add((value.module, value.class_name)) + for _, child in value.fields: + _collect_imports(child, out) + elif isinstance(value, _List): + _collect_imports(value.item, out) + elif isinstance(value, _Map): + _collect_imports(value.value, out) + + +def write_test_cases(output: str, schemas: dict[str, openapi.Schema]): + resources = _wired_resources() + + generated_path = Path(output) / "databricks_tests" / "core" / "_generated" + generated_path.mkdir(parents=True, exist_ok=True) + + plural_to_ref = {ns: ref for ref, ns in packages.RESOURCE_NAMESPACE.items()} + + for r in resources: + resource_ref = plural_to_ref[r.plural_name] + schema = schemas[resource_ref] + + example = _synth_object( + r.plural_name, resource_ref, schema, schemas, set(), top_level=True + ) + + imports: set[tuple[str, str]] = set() + _collect_imports(example, imports) + model_imports = "\n".join( + f"from {module} import {class_name}" + for module, class_name in sorted(imports) + ) + + code = _TEST_CASE_TEMPLATE.substitute( + singular=r.singular_name, + plural=r.plural_name, + model_imports=model_imports, + dict_example=_render_dict(example), + dataclass_example=_render_dataclass(example), + ) + (generated_path / f"{r.plural_name}.py").write_text(HEADER + code) + + (generated_path / "__init__.py").write_text(HEADER + _collector_code(resources)) + + print(f"Writing test cases into {generated_path}") + + +def _collector_code(resources: list[_WiredResource]) -> str: + module_imports = "\n".join(f" {r.plural_name}," for r in resources) + entries = "\n".join(f" {r.plural_name}._test_case()," for r in resources) + + return f"""from databricks_tests.core._generated import ( +{module_imports} +) + +__all__ = ["test_cases"] + +test_cases = [ +{entries} +] +""" diff --git a/python/codegen/codegen/main.py b/python/codegen/codegen/main.py index 7927da8596..924ec269e8 100644 --- a/python/codegen/codegen/main.py +++ b/python/codegen/codegen/main.py @@ -8,6 +8,7 @@ import codegen.generated_dataclass_patch as generated_dataclass_patch import codegen.generated_enum as generated_enum import codegen.generated_imports as generated_imports +import codegen.generated_test_cases as generated_test_cases import codegen.generated_wiring as generated_wiring import codegen.jsonschema as openapi import codegen.jsonschema_patch as openapi_patch @@ -52,6 +53,9 @@ def main(output: str): # decorators, and the core package __init__). generated_wiring.write_wiring(output) + # Generate the per-resource TestCase data driving test_resources.py. + generated_test_cases.write_test_cases(output, schemas) + def _transitively_mark_deprecated_and_private( roots: list[str], diff --git a/python/codegen/codegen/test_case.py.tmpl b/python/codegen/codegen/test_case.py.tmpl new file mode 100644 index 0000000000..8abe9d1ce5 --- /dev/null +++ b/python/codegen/codegen/test_case.py.tmpl @@ -0,0 +1,16 @@ +from databricks.bundles.core import Resources, ${singular}_mutator +from databricks.bundles.core._generated.${plural} import _resource_type +from databricks_tests.core._resource_test_case import TestCase +$model_imports + + +def _test_case(): + return ( + TestCase( + add_resource=Resources.add_${singular}, + dict_example=$dict_example, + dataclass_example=$dataclass_example, + mutator=${singular}_mutator, + ), + _resource_type(), + ) diff --git a/python/databricks_tests/.gitattributes b/python/databricks_tests/.gitattributes new file mode 100644 index 0000000000..810eb0c20e --- /dev/null +++ b/python/databricks_tests/.gitattributes @@ -0,0 +1,4 @@ +# Generated by pydabs-codegen (see python/codegen). The per-resource TestCase +# data under core/_generated/ drives the parametrized tests in test_resources.py; +# the rest of databricks_tests/ is hand-written. +core/_generated/** linguist-generated=true diff --git a/python/databricks_tests/core/_generated/__init__.py b/python/databricks_tests/core/_generated/__init__.py new file mode 100644 index 0000000000..9cf2cc18ce --- /dev/null +++ b/python/databricks_tests/core/_generated/__init__.py @@ -0,0 +1,21 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from databricks_tests.core._generated import ( + alerts, + catalogs, + jobs, + pipelines, + schemas, + volumes, +) + +__all__ = ["test_cases"] + +test_cases = [ + alerts._test_case(), + catalogs._test_case(), + jobs._test_case(), + pipelines._test_case(), + schemas._test_case(), + volumes._test_case(), +] diff --git a/python/databricks_tests/core/_generated/alerts.py b/python/databricks_tests/core/_generated/alerts.py new file mode 100644 index 0000000000..ec85f6daea --- /dev/null +++ b/python/databricks_tests/core/_generated/alerts.py @@ -0,0 +1,58 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from databricks.bundles.alerts._models.alert import Alert +from databricks.bundles.alerts._models.alert_v2_evaluation import AlertV2Evaluation +from databricks.bundles.alerts._models.alert_v2_operand_column import ( + AlertV2OperandColumn, +) +from databricks.bundles.alerts._models.alert_v2_run_as import AlertV2RunAs +from databricks.bundles.alerts._models.comparison_operator import ComparisonOperator +from databricks.bundles.alerts._models.cron_schedule import CronSchedule +from databricks.bundles.alerts._models.lifecycle import Lifecycle +from databricks.bundles.alerts._models.permission import Permission +from databricks.bundles.alerts._models.permission_level import PermissionLevel +from databricks.bundles.core import Resources, alert_mutator +from databricks.bundles.core._generated.alerts import _resource_type +from databricks_tests.core._resource_test_case import TestCase + + +def _test_case(): + return ( + TestCase( + add_resource=Resources.add_alert, + dict_example={ + "display_name": "display_name", + "evaluation": { + "comparison_operator": "LESS_THAN", + "source": {"name": "name"}, + }, + "lifecycle": {}, + "permissions": [{"level": "CAN_MANAGE"}], + "query_text": "query_text", + "run_as": {}, + "schedule": { + "quartz_cron_schedule": "quartz_cron_schedule", + "timezone_id": "timezone_id", + }, + "warehouse_id": "warehouse_id", + }, + dataclass_example=Alert( + display_name="display_name", + evaluation=AlertV2Evaluation( + comparison_operator=ComparisonOperator.LESS_THAN, + source=AlertV2OperandColumn(name="name"), + ), + lifecycle=Lifecycle(), + permissions=[Permission(level=PermissionLevel.CAN_MANAGE)], + query_text="query_text", + run_as=AlertV2RunAs(), + schedule=CronSchedule( + quartz_cron_schedule="quartz_cron_schedule", + timezone_id="timezone_id", + ), + warehouse_id="warehouse_id", + ), + mutator=alert_mutator, + ), + _resource_type(), + ) diff --git a/python/databricks_tests/core/_generated/catalogs.py b/python/databricks_tests/core/_generated/catalogs.py new file mode 100644 index 0000000000..177ada2042 --- /dev/null +++ b/python/databricks_tests/core/_generated/catalogs.py @@ -0,0 +1,35 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from databricks.bundles.catalogs._models.catalog import Catalog +from databricks.bundles.catalogs._models.encryption_settings import EncryptionSettings +from databricks.bundles.catalogs._models.lifecycle import Lifecycle +from databricks.bundles.catalogs._models.privilege_assignment import PrivilegeAssignment +from databricks.bundles.core import Resources, catalog_mutator +from databricks.bundles.core._generated.catalogs import _resource_type +from databricks_tests.core._resource_test_case import TestCase + + +def _test_case(): + return ( + TestCase( + add_resource=Resources.add_catalog, + dict_example={ + "grants": [{}], + "lifecycle": {}, + "managed_encryption_settings": {}, + "name": "name", + "options": {"key": "value"}, + "properties": {"key": "value"}, + }, + dataclass_example=Catalog( + grants=[PrivilegeAssignment()], + lifecycle=Lifecycle(), + managed_encryption_settings=EncryptionSettings(), + name="name", + options={"key": "value"}, + properties={"key": "value"}, + ), + mutator=catalog_mutator, + ), + _resource_type(), + ) diff --git a/python/databricks_tests/core/_generated/jobs.py b/python/databricks_tests/core/_generated/jobs.py new file mode 100644 index 0000000000..3a9dd6cec6 --- /dev/null +++ b/python/databricks_tests/core/_generated/jobs.py @@ -0,0 +1,92 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from databricks.bundles.core import Resources, job_mutator +from databricks.bundles.core._generated.jobs import _resource_type +from databricks.bundles.jobs._models.continuous import Continuous +from databricks.bundles.jobs._models.cron_schedule import CronSchedule +from databricks.bundles.jobs._models.git_provider import GitProvider +from databricks.bundles.jobs._models.git_source import GitSource +from databricks.bundles.jobs._models.job import Job +from databricks.bundles.jobs._models.job_cluster import JobCluster +from databricks.bundles.jobs._models.job_email_notifications import ( + JobEmailNotifications, +) +from databricks.bundles.jobs._models.job_environment import JobEnvironment +from databricks.bundles.jobs._models.job_notification_settings import ( + JobNotificationSettings, +) +from databricks.bundles.jobs._models.job_parameter_definition import ( + JobParameterDefinition, +) +from databricks.bundles.jobs._models.job_permission import JobPermission +from databricks.bundles.jobs._models.job_permission_level import JobPermissionLevel +from databricks.bundles.jobs._models.job_run_as import JobRunAs +from databricks.bundles.jobs._models.jobs_health_rules import JobsHealthRules +from databricks.bundles.jobs._models.lifecycle import Lifecycle +from databricks.bundles.jobs._models.performance_target import PerformanceTarget +from databricks.bundles.jobs._models.queue_settings import QueueSettings +from databricks.bundles.jobs._models.task import Task +from databricks.bundles.jobs._models.trigger_configuration import TriggerConfiguration +from databricks.bundles.jobs._models.trigger_settings import TriggerSettings +from databricks.bundles.jobs._models.webhook_notifications import WebhookNotifications +from databricks_tests.core._resource_test_case import TestCase + + +def _test_case(): + return ( + TestCase( + add_resource=Resources.add_job, + dict_example={ + "continuous": {}, + "email_notifications": {}, + "environments": [{"environment_key": "environment_key"}], + "git_source": {"git_provider": "gitHub", "git_url": "git_url"}, + "health": {}, + "job_clusters": [{"job_cluster_key": "job_cluster_key"}], + "lifecycle": {}, + "notification_settings": {}, + "parameters": [{"default": "default", "name": "name"}], + "performance_target": "PERFORMANCE_OPTIMIZED", + "permissions": [{"level": "CAN_MANAGE"}], + "queue": {"enabled": True}, + "run_as": {}, + "schedule": { + "quartz_cron_expression": "quartz_cron_expression", + "timezone_id": "timezone_id", + }, + "tags": {"key": "value"}, + "tasks": [{"task_key": "task_key"}], + "trigger": {}, + "triggers": [{}], + "webhook_notifications": {}, + }, + dataclass_example=Job( + continuous=Continuous(), + email_notifications=JobEmailNotifications(), + environments=[JobEnvironment(environment_key="environment_key")], + git_source=GitSource( + git_provider=GitProvider.GIT_HUB, git_url="git_url" + ), + health=JobsHealthRules(), + job_clusters=[JobCluster(job_cluster_key="job_cluster_key")], + lifecycle=Lifecycle(), + notification_settings=JobNotificationSettings(), + parameters=[JobParameterDefinition(default="default", name="name")], + performance_target=PerformanceTarget.PERFORMANCE_OPTIMIZED, + permissions=[JobPermission(level=JobPermissionLevel.CAN_MANAGE)], + queue=QueueSettings(enabled=True), + run_as=JobRunAs(), + schedule=CronSchedule( + quartz_cron_expression="quartz_cron_expression", + timezone_id="timezone_id", + ), + tags={"key": "value"}, + tasks=[Task(task_key="task_key")], + trigger=TriggerSettings(), + triggers=[TriggerConfiguration()], + webhook_notifications=WebhookNotifications(), + ), + mutator=job_mutator, + ), + _resource_type(), + ) diff --git a/python/databricks_tests/core/_generated/pipelines.py b/python/databricks_tests/core/_generated/pipelines.py new file mode 100644 index 0000000000..a4e6557331 --- /dev/null +++ b/python/databricks_tests/core/_generated/pipelines.py @@ -0,0 +1,65 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from databricks.bundles.core import Resources, pipeline_mutator +from databricks.bundles.core._generated.pipelines import _resource_type +from databricks.bundles.pipelines._models.event_log_spec import EventLogSpec +from databricks.bundles.pipelines._models.filters import Filters +from databricks.bundles.pipelines._models.ingestion_pipeline_definition import ( + IngestionPipelineDefinition, +) +from databricks.bundles.pipelines._models.lifecycle import Lifecycle +from databricks.bundles.pipelines._models.notifications import Notifications +from databricks.bundles.pipelines._models.pipeline import Pipeline +from databricks.bundles.pipelines._models.pipeline_cluster import PipelineCluster +from databricks.bundles.pipelines._models.pipeline_library import PipelineLibrary +from databricks.bundles.pipelines._models.pipeline_permission import PipelinePermission +from databricks.bundles.pipelines._models.pipeline_permission_level import ( + PipelinePermissionLevel, +) +from databricks.bundles.pipelines._models.pipelines_environment import ( + PipelinesEnvironment, +) +from databricks.bundles.pipelines._models.run_as import RunAs +from databricks_tests.core._resource_test_case import TestCase + + +def _test_case(): + return ( + TestCase( + add_resource=Resources.add_pipeline, + dict_example={ + "clusters": [{}], + "configuration": {"key": "value"}, + "environment": {}, + "event_log": {}, + "filters": {}, + "ingestion_definition": {}, + "libraries": [{}], + "lifecycle": {}, + "notifications": [{}], + "parameters": {"key": "value"}, + "permissions": [{"level": "CAN_MANAGE"}], + "run_as": {}, + "tags": {"key": "value"}, + }, + dataclass_example=Pipeline( + clusters=[PipelineCluster()], + configuration={"key": "value"}, + environment=PipelinesEnvironment(), + event_log=EventLogSpec(), + filters=Filters(), + ingestion_definition=IngestionPipelineDefinition(), + libraries=[PipelineLibrary()], + lifecycle=Lifecycle(), + notifications=[Notifications()], + parameters={"key": "value"}, + permissions=[ + PipelinePermission(level=PipelinePermissionLevel.CAN_MANAGE) + ], + run_as=RunAs(), + tags={"key": "value"}, + ), + mutator=pipeline_mutator, + ), + _resource_type(), + ) diff --git a/python/databricks_tests/core/_generated/schemas.py b/python/databricks_tests/core/_generated/schemas.py new file mode 100644 index 0000000000..49adceab52 --- /dev/null +++ b/python/databricks_tests/core/_generated/schemas.py @@ -0,0 +1,32 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from databricks.bundles.core import Resources, schema_mutator +from databricks.bundles.core._generated.schemas import _resource_type +from databricks.bundles.schemas._models.lifecycle import Lifecycle +from databricks.bundles.schemas._models.privilege_assignment import PrivilegeAssignment +from databricks.bundles.schemas._models.schema import Schema +from databricks_tests.core._resource_test_case import TestCase + + +def _test_case(): + return ( + TestCase( + add_resource=Resources.add_schema, + dict_example={ + "catalog_name": "catalog_name", + "grants": [{}], + "lifecycle": {}, + "name": "name", + "properties": {"key": "value"}, + }, + dataclass_example=Schema( + catalog_name="catalog_name", + grants=[PrivilegeAssignment()], + lifecycle=Lifecycle(), + name="name", + properties={"key": "value"}, + ), + mutator=schema_mutator, + ), + _resource_type(), + ) diff --git a/python/databricks_tests/core/_generated/volumes.py b/python/databricks_tests/core/_generated/volumes.py new file mode 100644 index 0000000000..bf8b434ade --- /dev/null +++ b/python/databricks_tests/core/_generated/volumes.py @@ -0,0 +1,35 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from databricks.bundles.core import Resources, volume_mutator +from databricks.bundles.core._generated.volumes import _resource_type +from databricks.bundles.volumes._models.lifecycle import Lifecycle +from databricks.bundles.volumes._models.privilege_assignment import PrivilegeAssignment +from databricks.bundles.volumes._models.volume import Volume +from databricks.bundles.volumes._models.volume_type import VolumeType +from databricks_tests.core._resource_test_case import TestCase + + +def _test_case(): + return ( + TestCase( + add_resource=Resources.add_volume, + dict_example={ + "catalog_name": "catalog_name", + "grants": [{}], + "lifecycle": {}, + "name": "name", + "schema_name": "schema_name", + "volume_type": "MANAGED", + }, + dataclass_example=Volume( + catalog_name="catalog_name", + grants=[PrivilegeAssignment()], + lifecycle=Lifecycle(), + name="name", + schema_name="schema_name", + volume_type=VolumeType.MANAGED, + ), + mutator=volume_mutator, + ), + _resource_type(), + ) diff --git a/python/databricks_tests/core/_resource_test_case.py b/python/databricks_tests/core/_resource_test_case.py new file mode 100644 index 0000000000..a9755e8e95 --- /dev/null +++ b/python/databricks_tests/core/_resource_test_case.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Callable + +from databricks.bundles.core._resource import Resource + + +@dataclass(kw_only=True) +class TestCase: + add_resource: Callable + dict_example: dict + dataclass_example: Resource + mutator: Callable diff --git a/python/databricks_tests/core/test_resources.py b/python/databricks_tests/core/test_resources.py index e27b4331db..ed50243d78 100644 --- a/python/databricks_tests/core/test_resources.py +++ b/python/databricks_tests/core/test_resources.py @@ -1,134 +1,15 @@ -from dataclasses import dataclass, replace -from typing import Callable +from dataclasses import replace import pytest -from databricks.bundles.alerts._models.alert import Alert -from databricks.bundles.alerts._models.alert_v2_evaluation import AlertV2Evaluation -from databricks.bundles.alerts._models.alert_v2_operand_column import ( - AlertV2OperandColumn, -) -from databricks.bundles.alerts._models.comparison_operator import ComparisonOperator -from databricks.bundles.alerts._models.cron_schedule import CronSchedule -from databricks.bundles.catalogs._models.catalog import Catalog -from databricks.bundles.core import ( - Location, - Resources, - Severity, - alert_mutator, - catalog_mutator, - job_mutator, - pipeline_mutator, - schema_mutator, - volume_mutator, -) +from databricks.bundles.core import Location, Resources, Severity from databricks.bundles.core._bundle import Bundle -from databricks.bundles.core._resource import Resource from databricks.bundles.core._resource_mutator import ResourceMutator from databricks.bundles.core._resource_type import _ResourceType from databricks.bundles.jobs._models.job import Job -from databricks.bundles.pipelines._models.pipeline import Pipeline -from databricks.bundles.schemas._models.schema import Schema -from databricks.bundles.volumes._models.volume import Volume - - -@dataclass(kw_only=True) -class TestCase: - add_resource: Callable - dict_example: dict - dataclass_example: Resource - mutator: Callable - - -resource_types = {tpe.resource_type: tpe for tpe in _ResourceType.all()} -test_cases = [ - ( - TestCase( - add_resource=Resources.add_job, - dict_example={"name": "My job"}, - dataclass_example=Job(name="My job"), - mutator=job_mutator, - ), - resource_types[Job], - ), - ( - TestCase( - add_resource=Resources.add_pipeline, - dict_example={"name": "My pipeline"}, - dataclass_example=Pipeline(name="My pipeline"), - mutator=pipeline_mutator, - ), - resource_types[Pipeline], - ), - ( - TestCase( - add_resource=Resources.add_volume, - dict_example={ - "name": "My Volume", - "catalog_name": "my_catalog", - "schema_name": "my_schema", - }, - dataclass_example=Volume( - catalog_name="my_catalog", - name="My Volume", - schema_name="my_schema", - ), - mutator=volume_mutator, - ), - resource_types[Volume], - ), - ( - TestCase( - add_resource=Resources.add_schema, - dict_example={"catalog_name": "my_catalog", "name": "my_schema"}, - dataclass_example=Schema(catalog_name="my_catalog", name="my_schema"), - mutator=schema_mutator, - ), - resource_types[Schema], - ), - ( - TestCase( - add_resource=Resources.add_alert, - dict_example={ - "display_name": "My Alert", - "query_text": "SELECT 1", - "warehouse_id": "my_warehouse", - "evaluation": { - "comparison_operator": "GREATER_THAN", - "source": {"name": "column_1"}, - }, - "schedule": { - "quartz_cron_schedule": "0 0 0 * * ?", - "timezone_id": "UTC", - }, - }, - dataclass_example=Alert( - display_name="My Alert", - query_text="SELECT 1", - warehouse_id="my_warehouse", - evaluation=AlertV2Evaluation( - comparison_operator=ComparisonOperator.GREATER_THAN, - source=AlertV2OperandColumn(name="column_1"), - ), - schedule=CronSchedule( - quartz_cron_schedule="0 0 0 * * ?", - timezone_id="UTC", - ), - ), - mutator=alert_mutator, - ), - resource_types[Alert], - ), - ( - TestCase( - add_resource=Resources.add_catalog, - dict_example={"name": "my_catalog"}, - dataclass_example=Catalog(name="my_catalog"), - mutator=catalog_mutator, - ), - resource_types[Catalog], - ), -] +from databricks_tests.core._generated import test_cases +from databricks_tests.core._resource_test_case import TestCase + test_case_ids = [tpe.plural_name for _, tpe in test_cases] From 64b730df00276fbfd5165a56ab5b1b62c7df45a7 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 28 Aug 2026 11:45:17 +0000 Subject: [PATCH 4/5] Fix ruff lint in the test-case generator The generator source lives outside databricks/databricks_tests, so pydabs-codegen's targeted ruff --fix does not reach it, but the root ruff check does. Sort imports and merge the two startswith calls into a single tuple call. No change to generated output. Co-authored-by: Isaac --- python/codegen/codegen/generated_test_cases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/codegen/codegen/generated_test_cases.py b/python/codegen/codegen/generated_test_cases.py index 7decc29a89..1de481b5d0 100644 --- a/python/codegen/codegen/generated_test_cases.py +++ b/python/codegen/codegen/generated_test_cases.py @@ -26,7 +26,7 @@ import codegen.jsonschema as openapi import codegen.packages as packages from codegen.generated_enum import _camel_to_upper_snake -from codegen.generated_wiring import _WiredResource, _wired_resources +from codegen.generated_wiring import _wired_resources, _WiredResource HEADER = "# Code generated by pydabs-codegen. DO NOT EDIT.\n\n" @@ -79,7 +79,7 @@ def _ref_name(ref: str) -> str: def _is_composite(ref: str) -> bool: - if ref.startswith("#/$defs/slice/") or ref.startswith("#/$defs/map/"): + if ref.startswith(("#/$defs/slice/", "#/$defs/map/")): return True return _ref_name(ref) not in packages.PRIMITIVES From 055d41fa3647a8a1aada840b24a41b5ee2fc734e Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Fri, 28 Aug 2026 12:17:15 +0000 Subject: [PATCH 5/5] added explainatory comments --- .../codegen/codegen/generated_test_cases.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/python/codegen/codegen/generated_test_cases.py b/python/codegen/codegen/generated_test_cases.py index 1de481b5d0..c3984879c4 100644 --- a/python/codegen/codegen/generated_test_cases.py +++ b/python/codegen/codegen/generated_test_cases.py @@ -75,10 +75,18 @@ class _Map: def _ref_name(ref: str) -> str: + """Last path segment of a JSON-schema ref -- the schema name. + + :param ref: a JSON-schema reference, e.g. "#/$defs/.../jobs.Task" or "#/$defs/string". + """ return ref.split("/")[-1] def _is_composite(ref: str) -> bool: + """Whether a ref is a composite type (list, map, object, or enum) rather than a scalar. + + :param ref: the JSON-schema reference of a field's type. + """ if ref.startswith(("#/$defs/slice/", "#/$defs/map/")): return True @@ -86,6 +94,11 @@ def _is_composite(ref: str) -> bool: def _synth_scalar(name: str, hint: str) -> _Scalar: + """Placeholder value for a primitive (str -> hint, int -> 0, float -> 0.0, bool -> True). + + :param name: the primitive's schema name, e.g. "string", "int", "boolean". + :param hint: enclosing field name, used as the string placeholder so examples read meaningfully. + """ if name == "string": return _Scalar(f'"{hint}"', f'"{hint}"') if name in ("integer", "int", "int64"): @@ -105,6 +118,14 @@ def _synth_ref( schemas: dict[str, openapi.Schema], visiting: set[str], ) -> _Value: + """Synthesize a value node for whatever type a ref points at: list, map, scalar, enum, or nested object. + + :param namespace: the resource's namespace (e.g. "jobs"); selects the module a referenced type is generated into. + :param ref: the JSON-schema reference of the type to synthesize. + :param hint: enclosing field name, passed through as the string placeholder. + :param schemas: all post-patch schemas keyed by schema name, for looking up nested/enum types. + :param visiting: ancestor object names on the current path, used to detect required cycles. + """ if ref.startswith("#/$defs/slice/"): element_ref = ref.replace("#/$defs/slice/", "#/$defs/") @@ -147,6 +168,15 @@ def _synth_object( visiting: set[str], top_level: bool, ) -> _Object: + """Synthesize an object value, choosing fields by policy: all required fields, plus (only at the resource top level) stable optional composite fields. + + :param namespace: the resource's namespace, threaded through to resolve nested types' modules. + :param schema_name: this object's schema name (e.g. "resources.Alert"). + :param schema: the Schema for this object -- its properties and required list. + :param schemas: all post-patch schemas, for recursing into nested types. + :param visiting: ancestor object names on the current path (cycle guard). + :param top_level: True only for the resource itself; when False, all optional fields are dropped. + """ visiting = visiting | {schema_name} fields: list[tuple[str, _Value]] = [] @@ -172,6 +202,11 @@ def _synth_object( def _module_of(namespace: str, schema_name: str) -> str: + """Python module a (non-primitive) schema's generated class lives in; asserts it exists. + + :param namespace: the resource's namespace; the type is generated under databricks.bundles.._models. + :param schema_name: the object/enum schema name to resolve. + """ module = packages.get_package(namespace, schema_name) assert module @@ -179,6 +214,10 @@ def _module_of(namespace: str, schema_name: str) -> str: def _render_dict(value: _Value) -> str: + """Render a synthesized value as a dict-literal source string (the dict_example form). + + :param value: the synthesized value node to render. + """ if isinstance(value, _Scalar): return value.dict_src if isinstance(value, _Enum): @@ -196,6 +235,10 @@ def _render_dict(value: _Value) -> str: def _render_dataclass(value: _Value) -> str: + """Render a synthesized value as a constructor-expression source string (the dataclass_example form). + + :param value: the synthesized value node to render. + """ if isinstance(value, _Scalar): return value.dataclass_src if isinstance(value, _Enum): @@ -213,6 +256,11 @@ def _render_dataclass(value: _Value) -> str: def _collect_imports(value: _Value, out: set[tuple[str, str]]) -> None: + """Collect (module, class_name) pairs the dataclass_example needs, walking nested objects/enums. + + :param value: the synthesized value node to walk. + :param out: set accumulating the (module, class_name) import pairs; mutated in place. + """ if isinstance(value, _Enum): out.add((value.module, value.class_name)) elif isinstance(value, _Object): @@ -226,6 +274,11 @@ def _collect_imports(value: _Value, out: set[tuple[str, str]]) -> None: def write_test_cases(output: str, schemas: dict[str, openapi.Schema]): + """Write one _generated/.py per wired resource plus the collector __init__.py. + + :param output: codegen output root (the python/ directory); files land under databricks_tests/core/_generated. + :param schemas: all post-patch schemas, used to synthesize each resource's dict/dataclass examples. + """ resources = _wired_resources() generated_path = Path(output) / "databricks_tests" / "core" / "_generated" @@ -263,6 +316,10 @@ def write_test_cases(output: str, schemas: dict[str, openapi.Schema]): def _collector_code(resources: list[_WiredResource]) -> str: + """Source for _generated/__init__.py: imports the per-resource modules and assembles `test_cases`. + + :param resources: the wired resources, in the order their test cases are collected. + """ module_imports = "\n".join(f" {r.plural_name}," for r in resources) entries = "\n".join(f" {r.plural_name}._test_case()," for r in resources)