← Files astronomer-dataARCHIVED FILE

skills/migrating-dagster-to-airflow/scripts/tests/test_validate_dag.py

4.51 KB · Sep 30, 2026 · 23:17 UTC

↓ Download file

"""Gate 1 and Gate 3 tests for validate_dag.py.

Gate 1 (ast.parse + optional ruff) and Gate 3 (structural asserts against a
stubbed DagBag) need no airflow. The airflow-dependent path (Gate 2 / real
DagBag) is import-guarded and skips cleanly when airflow is absent."""

import pytest

import validate_dag


# --- Gate 1: python import + lint ------------------------------------------


def _project(tmp_path, files):
    (tmp_path / "dags").mkdir()
    (tmp_path / "include").mkdir()
    for rel, body in files.items():
        (tmp_path / rel).write_text(body)
    return str(tmp_path)


def test_gate1_pass(tmp_path):
    proj = _project(tmp_path, {"dags/good.py": "x = 1 + 1\n"})
    assert validate_dag.gate1_import_lint(proj)["status"] == "pass"


def test_gate1_syntax_error_fails(tmp_path):
    proj = _project(tmp_path, {"dags/bad.py": "def broken(:\n"})
    r = validate_dag.gate1_import_lint(proj)
    assert r["status"] == "fail"
    assert r["details"]["parse_errors"]


def test_gate1_no_files_fails(tmp_path):
    (tmp_path / "dags").mkdir()
    assert validate_dag.gate1_import_lint(str(tmp_path))["status"] == "fail"


# --- Gate 3: structural asserts against a stubbed DagBag --------------------


class _Asset:
    def __init__(self, uri):
        self.uri = uri


class _Task:
    def __init__(self, task_id, downstream=(), outlets=()):
        self.task_id = task_id
        self.downstream_task_ids = set(downstream)
        self.outlets = list(outlets)


class _Dag:
    def __init__(self, tasks, schedule="@daily"):
        self.tasks = tasks
        self.schedule = schedule


class _DagBag:
    def __init__(self, dags):
        self._dags = dags

    def get_dag(self, dag_id):
        return self._dags.get(dag_id)


def _dag():
    return _Dag(
        [_Task("extract", ["load"]), _Task("load", outlets=[_Asset("s3://out")])]
    )


def test_gate3_structure_pass():
    manifest = {
        "units": {
            "asset:orders": {
                "dag_id": "orders",
                "task_count": 2,
                "edges": [["extract", "load"]],
                "schedule": "@daily",
                "asset_outlets": ["s3://out"],
            }
        }
    }
    r = validate_dag.gate3_structure(_DagBag({"orders": _dag()}), manifest, None)
    assert r["status"] == "pass"
    assert r["details"]["runtime_attribute_probe"]["schedule_attr"] == "schedule"


def test_gate3_structure_mismatch_fails():
    manifest = {
        "units": {
            "asset:orders": {
                "dag_id": "orders",
                "task_count": 99,
                "edges": [["a", "b"]],
            }
        }
    }
    r = validate_dag.gate3_structure(_DagBag({"orders": _dag()}), manifest, None)
    assert r["status"] == "fail"


def test_gate3_all_missing_dag_id_is_loud_fail():
    manifest = {"units": {"asset:a": {"dag_id": None}, "op:b": {"dag_id": None}}}
    r = validate_dag.gate3_structure(_DagBag({}), manifest, None)
    assert r["status"] == "fail"
    assert r["details"]["skipped_no_dag_id_count"] == 2


def test_gate3_target_none_skipped_silently():
    manifest = {"units": {"io:x": {"target": "none"}}}
    r = validate_dag.gate3_structure(_DagBag({}), manifest, None)
    # a deliberately DAG-less unit is not counted as a planning gap
    assert r["details"]["skipped_no_dag_id_count"] == 0
    assert r["status"] == "skip"


def test_gate3_asset_schedule_and_timetable_type():
    class CronPartitionTimetable:  # class name is what timetable_type matches
        def __repr__(self):
            return "CronPartitionTimetable('15 * * * *')"

    dag = _Dag([_Task("a")], schedule="[Asset(uri='s3://raw')]")
    dag.timetable = CronPartitionTimetable()
    manifest = {
        "units": {
            "asset:daily": {
                "dag_id": "daily",
                "asset_schedule": ["s3://raw"],
                "timetable_type": "CronPartitionTimetable",
            }
        }
    }
    r = validate_dag.gate3_structure(_DagBag({"daily": dag}), manifest, None)
    checks = r["details"]["units"][0]["checks"]
    assert checks["asset_schedule"]["ok"] and checks["timetable_type"]["ok"]


# --- Gate 2 / real DagBag: needs airflow, skip cleanly without it ----------


def test_gate2_real_dagbag_requires_airflow(tmp_path):
    pytest.importorskip("airflow")
    (tmp_path / "dags").mkdir()
    (tmp_path / "dags" / "d.py").write_text("x = 1\n")
    dagbag, import_path, err = validate_dag.load_dagbag(str(tmp_path))
    r = validate_dag.gate2_dagbag(str(tmp_path), dagbag, import_path, err)
    assert r["gate"] == 2 and r["status"] in ("pass", "fail", "skip")

SHA-256: 6077e681989192ccb92e73a7922ce98effa5077c007d5d18b9830c56313ad115