This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 6f5d5d06b17 Align standard provider tests with source paths (#73267)
6f5d5d06b17 is described below

commit 6f5d5d06b17751d61b9dea4f7ffca898875298e1
Author: Keith <[email protected]>
AuthorDate: Mon Oct 5 05:10:35 2026 +0900

    Align standard provider tests with source paths (#73267)
    
    The provider structure check treated EmptyOperator and ExternalTaskSensor 
as untested because their test paths did not mirror the source modules. 
Aligning those paths removes the stale exceptions while preserving the existing 
AirflowException allowlist for the renamed test.
    
    Test Plan:
    
    ```
    AIRFLOW_HOME=/private/tmp/codex-airflow-73267 uv run --project 
providers/standard pytest 
providers/standard/tests/unit/standard/operators/test_empty.py -q
    AIRFLOW_HOME=/private/tmp/codex-airflow-73267 uv run --project 
providers/standard pytest 
providers/standard/tests/unit/standard/sensors/test_external_task.py::TestExternalDagLink
 -q
    AIRFLOW_HOME=/private/tmp/codex-airflow-73267 uv run --project airflow-core 
pytest 
airflow-core/tests/unit/always/test_project_structure.py::TestProjectStructure::test_providers_modules_should_have_tests
 -q
    ```
    
    Authored with assistance from Codex.
---
 .../tests/unit/always/test_project_structure.py    |  2 -
 generated/known_airflow_exceptions.txt             |  2 +-
 .../tests/unit/standard/operators/test_empty.py    | 56 ++++++++++++++++++++++
 ...ternal_task_sensor.py => test_external_task.py} | 32 +++++++++++++
 4 files changed, 89 insertions(+), 3 deletions(-)

diff --git a/airflow-core/tests/unit/always/test_project_structure.py 
b/airflow-core/tests/unit/always/test_project_structure.py
index 224b1aa7157..c1279931048 100644
--- a/airflow-core/tests/unit/always/test_project_structure.py
+++ b/airflow-core/tests/unit/always/test_project_structure.py
@@ -126,8 +126,6 @@ class TestProjectStructure:
             
"providers/google/tests/unit/google/cloud/utils/test_bigquery_get_data.py",
             
"providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_adls.py",
             
"providers/snowflake/tests/unit/snowflake/triggers/test_snowflake_trigger.py",
-            "providers/standard/tests/unit/standard/operators/test_empty.py",
-            
"providers/standard/tests/unit/standard/sensors/test_external_task.py",
         ]
         modules_files: list[pathlib.Path] = list(
             
AIRFLOW_PROVIDERS_ROOT_PATH.glob("**/src/airflow/providers/**/*.py")
diff --git a/generated/known_airflow_exceptions.txt 
b/generated/known_airflow_exceptions.txt
index 8fdef403b6b..d1200225357 100644
--- a/generated/known_airflow_exceptions.txt
+++ b/generated/known_airflow_exceptions.txt
@@ -385,7 +385,7 @@ 
providers/standard/src/airflow/providers/standard/operators/trigger_dagrun.py::6
 providers/standard/src/airflow/providers/standard/sensors/filesystem.py::1
 providers/standard/src/airflow/providers/standard/utils/skipmixin.py::3
 providers/standard/tests/unit/standard/operators/test_python.py::1
-providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py::1
+providers/standard/tests/unit/standard/sensors/test_external_task.py::1
 providers/tableau/src/airflow/providers/tableau/hooks/tableau.py::1
 providers/tableau/src/airflow/providers/tableau/operators/tableau.py::3
 providers/telegram/src/airflow/providers/telegram/hooks/telegram.py::6
diff --git a/providers/standard/tests/unit/standard/operators/test_empty.py 
b/providers/standard/tests/unit/standard/operators/test_empty.py
new file mode 100644
index 00000000000..a5dd8846fd0
--- /dev/null
+++ b/providers/standard/tests/unit/standard/operators/test_empty.py
@@ -0,0 +1,56 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+
+from airflow.providers.standard.operators.empty import EmptyOperator
+
+from tests_common.test_utils.version_compat import AIRFLOW_V_3_2_PLUS
+
+
+class TestEmptyOperator:
+    def test_execute_is_a_no_op(self):
+        assert EmptyOperator(task_id="empty").execute(context={}) is None
+
+    def test_is_marked_as_empty_operator(self):
+        assert EmptyOperator.inherits_from_empty_operator is True
+        assert EmptyOperator(task_id="empty").inherits_from_empty_operator is 
True
+
+    @pytest.mark.skipif(not AIRFLOW_V_3_2_PLUS, 
reason="TaskInstance.is_task_schedulable added in 3.2")
+    @pytest.mark.parametrize(
+        ("extra_kwargs", "schedulable"),
+        [
+            pytest.param({}, False, id="plain-empty-task-is-short-circuited"),
+            pytest.param(
+                {"on_execute_callback": [lambda ctx: None]}, True, 
id="execute-callback-forces-scheduling"
+            ),
+            pytest.param(
+                {"on_success_callback": [lambda ctx: None]}, True, 
id="success-callback-forces-scheduling"
+            ),
+            pytest.param({"outlets": [object()]}, True, 
id="outlets-force-scheduling"),
+            pytest.param({"inlets": [object()]}, True, 
id="inlets-force-scheduling"),
+        ],
+    )
+    def test_scheduler_short_circuit_contract(self, extra_kwargs, schedulable):
+        """A trivial EmptyOperator is marked success without being scheduled; 
side effects opt out."""
+        from airflow.models.taskinstance import TaskInstance
+
+        task = EmptyOperator(task_id="empty", **extra_kwargs)
+
+        assert TaskInstance.is_task_schedulable(task) is schedulable
diff --git 
a/providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py 
b/providers/standard/tests/unit/standard/sensors/test_external_task.py
similarity index 98%
rename from 
providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py
rename to providers/standard/tests/unit/standard/sensors/test_external_task.py
index 4ef032c92da..5b1dbeda303 100644
--- 
a/providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py
+++ b/providers/standard/tests/unit/standard/sensors/test_external_task.py
@@ -2409,3 +2409,35 @@ def dag_bag_head_tail_mapped_tasks(session):
         dag_bag.bag_dag(dag=dag, root_dag=dag)
 
     return dag_bag
+
+
[email protected](not AIRFLOW_V_3_0_PLUS, reason="URL layout is different on 
Airflow 2")
+class TestExternalDagLink:
+    @pytest.mark.parametrize(
+        "operator_class",
+        [
+            pytest.param(ExternalTaskSensor, id="sensor"),
+            pytest.param(ExternalTaskMarker, id="marker"),
+        ],
+    )
+    def test_link_points_to_external_dag_run(self, operator_class, dag_maker):
+        from airflow.configuration import conf
+
+        with dag_maker("test_external_dag_link", serialized=True):
+            task = operator_class(
+                task_id="task_with_link",
+                external_dag_id="external_dag",
+                external_task_id="external_task",
+            )
+
+        dr = dag_maker.create_dagrun()
+        ti = dr.get_task_instance(task.task_id)
+
+        assert len(task.operator_extra_links) == 1
+        link = task.operator_extra_links[0]
+        assert link.name == "External DAG"
+
+        url = link.get_link(operator=task, ti_key=ti.key)
+
+        base_url = conf.get("api", "base_url", fallback="/").lower()
+        assert url == f"{base_url}dags/external_dag/runs/{dr.run_id}"

Reply via email to