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 268ad8b156a Stop running duplicate virtualenv operator tests (#73500)
268ad8b156a is described below
commit 268ad8b156a5e3b589cea610cf63816192071e76
Author: Kaxil Naik <[email protected]>
AuthorDate: Mon Sep 21 21:03:36 2026 +0100
Stop running duplicate virtualenv operator tests (#73500)
The virtualenv and external-python operator tests spent most of their
time re-running the same subprocess round trip through four operator
classes, and the branch variants inherited environment-variable tests
whose outcome the environment could not influence. Run the shared
subprocess behaviour once, test skip_on_exit_code normalisation without
a subprocess, and drop the serializer parameter that duplicated pickle.
---
.../tests/unit/standard/operators/test_python.py | 380 ++++++++-------------
1 file changed, 151 insertions(+), 229 deletions(-)
diff --git a/providers/standard/tests/unit/standard/operators/test_python.py
b/providers/standard/tests/unit/standard/operators/test_python.py
index d8228b1032f..46f3a77a8e5 100644
--- a/providers/standard/tests/unit/standard/operators/test_python.py
+++ b/providers/standard/tests/unit/standard/operators/test_python.py
@@ -1025,78 +1025,18 @@ class BaseTestPythonVirtualenvOperator(BasePythonTest):
def test_template_fields(self):
assert
set(PythonOperator.template_fields).issubset(PythonVirtualenvOperator.template_fields)
- def test_fail(self):
- def f():
- raise RuntimeError
-
- with pytest.raises(CalledProcessError):
- self.run_as_task(f)
-
- def test_fail_with_message(self):
- def f():
- raise RuntimeError("Custom error message")
-
- with pytest.raises(AirflowException, match="Custom error message"):
- self.run_as_task(f)
-
- def test_string_args(self):
- def f():
- print(virtualenv_string_args)
- if virtualenv_string_args[0] != virtualenv_string_args[2]:
- raise RuntimeError
-
- self.run_as_task(f, string_args=[1, 2, 1])
-
- def test_with_args(self):
- def f(a, b, c=False, d=False):
- if a == 0 and b == 1 and c and not d:
- return True
- raise RuntimeError
-
- self.run_as_task(f, op_args=[0, 1], op_kwargs={"c": True})
-
- def test_return_none(self):
- def f():
- return None
-
- ti = self.run_as_task(f, return_ti=True)
- assert self._pull_xcom(ti) is None
-
- def test_return_false(self):
- def f():
- return False
-
- ti = self.run_as_task(f, return_ti=True)
-
- assert self._pull_xcom(ti) is False
-
def test_lambda(self):
with pytest.raises(
ValueError, match="PythonVirtualenvOperator only supports
functions for python_callable arg"
):
PythonVirtualenvOperator(python_callable=lambda x: 4,
task_id=self.task_id)
- def test_nonimported_as_arg(self):
- def f(_):
- return None
-
- self.run_as_task(f, op_args=[datetime.now(tz=_timezone.utc)])
-
- def test_context(self):
- def f(templates_dict):
- return templates_dict["ds"]
-
- # the callable receives (and returns) the rendered templates_dict value
- ti = self.run_as_task(f, return_ti=True, templates_dict={"ds": "{{ ds
}}"})
- assert self._pull_xcom(ti) == self.ds_templated
-
@pytest.mark.parametrize(
"serializer",
[
pytest.param("pickle", id="pickle"),
pytest.param("dill", marks=DILL_MARKER, id="dill"),
pytest.param("cloudpickle", marks=CLOUDPICKLE_MARKER,
id="cloudpickle"),
- pytest.param(None, id="default"),
],
)
def test_deepcopy(self, serializer):
@@ -1188,43 +1128,23 @@ class BaseTestPythonVirtualenvOperator(BasePythonTest):
assert set(context) == declared_keys
@pytest.mark.parametrize(
- ("kwargs", "actual_exit_code", "expected_state"),
+ ("skip_on_exit_code", "expected"),
[
- ({}, 0, TaskInstanceState.SUCCESS),
- ({}, 100, TaskInstanceState.FAILED),
- ({}, 101, TaskInstanceState.FAILED),
- ({"skip_on_exit_code": None}, 0, TaskInstanceState.SUCCESS),
- ({"skip_on_exit_code": None}, 100, TaskInstanceState.FAILED),
- ({"skip_on_exit_code": None}, 101, TaskInstanceState.FAILED),
- ({"skip_on_exit_code": 100}, 0, TaskInstanceState.SUCCESS),
- ({"skip_on_exit_code": 100}, 100, TaskInstanceState.SKIPPED),
- ({"skip_on_exit_code": 100}, 101, TaskInstanceState.FAILED),
- ({"skip_on_exit_code": 0}, 0, TaskInstanceState.SKIPPED),
- ({"skip_on_exit_code": [100]}, 0, TaskInstanceState.SUCCESS),
- ({"skip_on_exit_code": [100]}, 100, TaskInstanceState.SKIPPED),
- ({"skip_on_exit_code": [100]}, 101, TaskInstanceState.FAILED),
- ({"skip_on_exit_code": [100, 102]}, 101, TaskInstanceState.FAILED),
- ({"skip_on_exit_code": (100,)}, 0, TaskInstanceState.SUCCESS),
- ({"skip_on_exit_code": (100,)}, 100, TaskInstanceState.SKIPPED),
- ({"skip_on_exit_code": (100,)}, 101, TaskInstanceState.FAILED),
+ pytest.param(None, [], id="none"),
+ pytest.param(100, [100], id="int"),
+ pytest.param([100, 102], [100, 102], id="list"),
+ pytest.param((100,), (100,), id="tuple"),
],
)
- def test_on_skip_exit_code(self, kwargs, actual_exit_code, expected_state):
- def f(exit_code):
- if exit_code != 0:
- raise SystemExit(exit_code)
+ def test_skip_on_exit_code_normalization(self, skip_on_exit_code,
expected):
+ """A bare exit code is wrapped in a list; containers and None are kept
as-is (None becomes empty)."""
- if expected_state == TaskInstanceState.FAILED:
- with pytest.raises(CalledProcessError):
- self.run_as_task(f, op_kwargs={"exit_code": actual_exit_code},
**kwargs)
- else:
- ti = self.run_as_task(
- f,
- return_ti=True,
- op_kwargs={"exit_code": actual_exit_code},
- **kwargs,
- )
- assert ti.state == expected_state
+ def f(): ...
+
+ op = self.opcls(
+ task_id="task", python_callable=f,
skip_on_exit_code=skip_on_exit_code, **self.default_kwargs()
+ )
+ assert op.skip_on_exit_code == expected
@pytest.mark.parametrize(
"serializer",
@@ -1254,48 +1174,6 @@ class BaseTestPythonVirtualenvOperator(BasePythonTest):
self.run_as_task(f, op_args=[42], serializer=serializer)
assert f"Unable to import `{serializer}` module." in caplog.text
- def test_environment_variables(self):
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- ti = self.run_as_task(f, env_vars={"MY_ENV_VAR": "ABCDE"},
return_ti=True)
- assert self._pull_xcom(ti) == "ABCDE"
-
- def test_environment_variables_with_inherit_env_true(self, monkeypatch):
- monkeypatch.setenv("MY_ENV_VAR", "QWERT")
-
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- ti = self.run_as_task(f, inherit_env=True, return_ti=True)
- assert self._pull_xcom(ti) == "QWERT"
-
- def test_environment_variables_with_inherit_env_false(self, monkeypatch):
- monkeypatch.setenv("MY_ENV_VAR", "TYUIO")
-
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- with pytest.raises(AirflowException):
- self.run_as_task(f, inherit_env=False)
-
- def test_environment_variables_overriding(self, monkeypatch):
- monkeypatch.setenv("MY_ENV_VAR", "ABCDE")
-
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- ti = self.run_as_task(f, env_vars={"MY_ENV_VAR": "EFGHI"},
inherit_env=True, return_ti=True)
- assert self._pull_xcom(ti) == "EFGHI"
-
venv_cache_path = tempfile.mkdtemp(prefix="venv_cache_path")
@@ -1462,7 +1340,6 @@ class TestPythonVirtualenvOperator(_VenvTestBase):
pytest.param("pickle", [], id="pickle"),
pytest.param("dill", ["dill"], marks=DILL_MARKER, id="dill"),
pytest.param("cloudpickle", ["cloudpickle"],
marks=CLOUDPICKLE_MARKER, id="cloudpickle"),
- pytest.param(None, [], id="default"),
],
)
def test_no_system_site_packages(self, serializer, extra_requirements):
@@ -1514,7 +1391,6 @@ class TestPythonVirtualenvOperator(_VenvTestBase):
pytest.param("pickle", [], id="pickle"),
pytest.param("dill", ["dill"], marks=DILL_MARKER, id="dill"),
pytest.param("cloudpickle", ["cloudpickle"],
marks=CLOUDPICKLE_MARKER, id="cloudpickle"),
- pytest.param(None, [], id="default"),
],
)
def test_unpinned_requirements(self, serializer, extra_requirements):
@@ -1534,7 +1410,6 @@ class TestPythonVirtualenvOperator(_VenvTestBase):
pytest.param("pickle", [], id="pickle"),
pytest.param("dill", ["dill"], marks=DILL_MARKER, id="dill"),
pytest.param("cloudpickle", ["cloudpickle"],
marks=CLOUDPICKLE_MARKER, id="cloudpickle"),
- pytest.param(None, [], id="default"),
],
)
def test_range_requirements(self, serializer, extra_requirements):
@@ -1582,7 +1457,6 @@ class TestPythonVirtualenvOperator(_VenvTestBase):
pytest.param("pickle", id="pickle"),
pytest.param("dill", marks=DILL_MARKER, id="dill"),
pytest.param("cloudpickle", marks=CLOUDPICKLE_MARKER,
id="cloudpickle"),
- pytest.param(None, id="default"),
],
)
def test_templated_requirements_file(self, serializer):
@@ -1605,7 +1479,6 @@ class TestPythonVirtualenvOperator(_VenvTestBase):
pytest.param("pickle", [], id="pickle"),
pytest.param("dill", ["dill"], marks=DILL_MARKER, id="dill"),
pytest.param("cloudpickle", ["cloudpickle"],
marks=CLOUDPICKLE_MARKER, id="cloudpickle"),
- pytest.param(None, [], id="default"),
],
)
def test_python_3_serializers(self, serializer, extra_requirements):
@@ -1821,7 +1694,6 @@ class TestPythonVirtualenvOperator(_VenvTestBase):
pytest.param("pickle", id="pickle"),
pytest.param("dill", marks=DILL_MARKER, id="dill"),
pytest.param("cloudpickle", marks=CLOUDPICKLE_MARKER,
id="cloudpickle"),
- pytest.param(None, id="default"),
],
)
def test_base_context(self, serializer):
@@ -2093,12 +1965,149 @@ class TestPythonVirtualenvOperator(_VenvTestBase):
pycache_cleanup_mock.assert_called_once_with(expected_cleanup_path)
+class _SubprocessBehaviourTests:
+ """Tests for the subprocess round trip shared by every venv-style operator.
+
+ Argument pickling, string args, environment handling, error propagation
and result
+ return all live in
``_BasePythonVirtualenvOperator._execute_python_callable_in_subprocess``.
+ They need a real interpreter subprocess but no virtualenv, so they run
once, on
+ ``ExternalPythonOperator``; the virtualenv-specific classes only test what
differs.
+ """
+
+ def test_fail(self):
+ def f():
+ raise RuntimeError
+
+ with pytest.raises(CalledProcessError):
+ self.run_as_task(f)
+
+ def test_fail_with_message(self):
+ def f():
+ raise RuntimeError("Custom error message")
+
+ with pytest.raises(AirflowException, match="Custom error message"):
+ self.run_as_task(f)
+
+ def test_string_args(self):
+ def f():
+ print(virtualenv_string_args)
+ if virtualenv_string_args[0] != virtualenv_string_args[2]:
+ raise RuntimeError
+
+ self.run_as_task(f, string_args=[1, 2, 1])
+
+ def test_with_args(self):
+ def f(a, b, c=False, d=False):
+ if a == 0 and b == 1 and c and not d:
+ return True
+ raise RuntimeError
+
+ self.run_as_task(f, op_args=[0, 1], op_kwargs={"c": True})
+
+ def test_return_none(self):
+ def f():
+ return None
+
+ ti = self.run_as_task(f, return_ti=True)
+ assert self._pull_xcom(ti) is None
+
+ def test_return_false(self):
+ def f():
+ return False
+
+ ti = self.run_as_task(f, return_ti=True)
+
+ assert self._pull_xcom(ti) is False
+
+ def test_nonimported_as_arg(self):
+ def f(_):
+ return None
+
+ self.run_as_task(f, op_args=[datetime.now(tz=_timezone.utc)])
+
+ def test_context(self):
+ def f(templates_dict):
+ return templates_dict["ds"]
+
+ # the callable receives (and returns) the rendered templates_dict value
+ ti = self.run_as_task(f, return_ti=True, templates_dict={"ds": "{{ ds
}}"})
+ assert self._pull_xcom(ti) == self.ds_templated
+
+ @pytest.mark.parametrize(
+ ("kwargs", "actual_exit_code", "expected_state"),
+ [
+ ({}, 0, TaskInstanceState.SUCCESS),
+ ({}, 100, TaskInstanceState.FAILED),
+ ({"skip_on_exit_code": 100}, 100, TaskInstanceState.SKIPPED),
+ ({"skip_on_exit_code": 0}, 0, TaskInstanceState.SKIPPED),
+ ],
+ )
+ def test_on_skip_exit_code(self, kwargs, actual_exit_code, expected_state):
+ def f(exit_code):
+ if exit_code != 0:
+ raise SystemExit(exit_code)
+
+ if expected_state == TaskInstanceState.FAILED:
+ with pytest.raises(CalledProcessError):
+ self.run_as_task(f, op_kwargs={"exit_code": actual_exit_code},
**kwargs)
+ else:
+ ti = self.run_as_task(
+ f,
+ return_ti=True,
+ op_kwargs={"exit_code": actual_exit_code},
+ **kwargs,
+ )
+ assert ti.state == expected_state
+
+ def test_environment_variables(self):
+ def f():
+ import os
+
+ return os.environ["MY_ENV_VAR"]
+
+ ti = self.run_as_task(f, env_vars={"MY_ENV_VAR": "ABCDE"},
return_ti=True)
+ assert self._pull_xcom(ti) == "ABCDE"
+
+ def test_environment_variables_with_inherit_env_true(self, monkeypatch):
+ monkeypatch.setenv("MY_ENV_VAR", "QWERT")
+
+ def f():
+ import os
+
+ return os.environ["MY_ENV_VAR"]
+
+ ti = self.run_as_task(f, inherit_env=True, return_ti=True)
+ assert self._pull_xcom(ti) == "QWERT"
+
+ def test_environment_variables_with_inherit_env_false(self, monkeypatch):
+ monkeypatch.setenv("MY_ENV_VAR", "TYUIO")
+
+ def f():
+ import os
+
+ return os.environ["MY_ENV_VAR"]
+
+ with pytest.raises(AirflowException):
+ self.run_as_task(f, inherit_env=False)
+
+ def test_environment_variables_overriding(self, monkeypatch):
+ monkeypatch.setenv("MY_ENV_VAR", "ABCDE")
+
+ def f():
+ import os
+
+ return os.environ["MY_ENV_VAR"]
+
+ ti = self.run_as_task(f, env_vars={"MY_ENV_VAR": "EFGHI"},
inherit_env=True, return_ti=True)
+ assert self._pull_xcom(ti) == "EFGHI"
+
+
# when venv tests are run in parallel to other test they create new processes
and this might take
# quite some time in shared docker environment and get some contention even
between different containers
# therefore we have to extend timeouts for those tests
@pytest.mark.execution_timeout(120)
@pytest.mark.external_python_operator
-class TestExternalPythonOperator(_VenvTestBase):
+class TestExternalPythonOperator(_SubprocessBehaviourTests, _VenvTestBase):
pytestmark = _VENV_DB_MARKS
opcls = ExternalPythonOperator
@@ -2196,35 +2205,6 @@ class
BaseTestBranchPythonVirtualenvOperator(BaseTestPythonVirtualenvOperator):
self.branch_1 = EmptyOperator(task_id="branch_1")
self.branch_2 = EmptyOperator(task_id="branch_2")
- # Skip some tests from base class that are not applicable for branching
operators
- # as the branching condition is mandatory but not given by base class
- @pytest.mark.skip(reason="Test is not working for branching operators")
- def test_string_args(self):
- pass
-
- @pytest.mark.skip(reason="Test is not working for branching operators")
- def test_return_none(self):
- pass
-
- @pytest.mark.skip(reason="Test is not working for branching operators")
- def test_nonimported_as_arg(self):
- pass
-
- @pytest.mark.skip(reason="Test is not working for branching operators")
- def test_on_skip_exit_code(self):
- pass
-
- def test_with_args(self):
- def f(a, b, c=False, d=False):
- if a == 0 and b == 1 and c and not d:
- return True
- raise RuntimeError
-
- with pytest.raises(
- AirflowException, match=r"Invalid tasks found: {\(False,
'bool'\)}.|'branch_task_ids'.*task.*"
- ):
- self.run_as_task(f, op_args=[0, 1], op_kwargs={"c": True})
-
def test_return_false(self):
def f():
return False
@@ -2234,64 +2214,6 @@ class
BaseTestBranchPythonVirtualenvOperator(BaseTestPythonVirtualenvOperator):
):
self.run_as_task(f)
- def test_context(self):
- def f(templates_dict):
- return templates_dict["ds"]
-
- with pytest.raises(AirflowException, match="Invalid tasks
found:|'branch_task_ids'.*task.*"):
- self.run_as_task(f, templates_dict={"ds": "{{ ds }}"})
-
- def test_environment_variables(self):
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- with pytest.raises(
- AirflowException,
- match=r"'branch_task_ids'.*task.*",
- ):
- self.run_as_task(f, env_vars={"MY_ENV_VAR": "ABCDE"})
-
- def test_environment_variables_with_inherit_env_true(self, monkeypatch):
- monkeypatch.setenv("MY_ENV_VAR", "QWERT")
-
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- with pytest.raises(
- AirflowException,
- match=r"'branch_task_ids'.*task.*",
- ):
- self.run_as_task(f, inherit_env=True)
-
- def test_environment_variables_with_inherit_env_false(self, monkeypatch):
- monkeypatch.setenv("MY_ENV_VAR", "TYUIO")
-
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- with pytest.raises(AirflowException):
- self.run_as_task(f, inherit_env=False)
-
- def test_environment_variables_overriding(self, monkeypatch):
- monkeypatch.setenv("MY_ENV_VAR", "ABCDE")
-
- def f():
- import os
-
- return os.environ["MY_ENV_VAR"]
-
- with pytest.raises(
- AirflowException,
- match=r"'branch_task_ids'.*task.*",
- ):
- self.run_as_task(f, env_vars={"MY_ENV_VAR": "EFGHI"},
inherit_env=True)
-
def test_with_no_caching(self):
"""
Most of venv tests use caching to speed up the tests. This test
ensures that