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

potiuk 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 c497c5dea6 Fix side-effect of setting log handlers in tests (#43175)
c497c5dea6 is described below

commit c497c5dea686f2337d94094ef3aa25b271ac0881
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri Oct 18 23:00:42 2024 +0200

    Fix side-effect of setting log handlers in tests (#43175)
    
    In some of our tests, handlers for loggers were set to the
    "logging.Handler()" and that created a side effect where
    when they were not cleaned by another test, they produced
    `NotImplementedError: emit must be implemented by Handler subclasses`
    message - because indeed `logging.Handler()` emit message
    raised NotImplementedError.
    
    The problem was that those tests (and some others) cleaned-up
    the handlers only BEFORE the tests but not AFTER - so if it
    happened that the same "test worker" run another test afterwards,
    that cleaned the handlers but did not set the handlers back, the
    side effect was gone. But when xdist arranged the tests to
    different pytest workers, the Handlers could remain registered
    in loggers so other tests could attempt to emit some logs
    in case they logged something to a parent logger.
    
    The fix applied is to create a fixture that cleans all loggers
    BEFORE and AFTER a test - and add the fixture to tests that
    modify handlers.
---
 tests/cli/commands/test_task_command.py  |  2 +-
 tests/conftest.py                        | 23 +++++++++++++++++++++++
 tests/jobs/test_triggerer_job.py         |  4 +---
 tests/jobs/test_triggerer_job_logging.py | 20 ++++----------------
 tests_common/test_utils/log_handlers.py  | 21 +++++++++++++++++++++
 5 files changed, 50 insertions(+), 20 deletions(-)

diff --git a/tests/cli/commands/test_task_command.py 
b/tests/cli/commands/test_task_command.py
index 67204a1ad7..1de3f7b68d 100644
--- a/tests/cli/commands/test_task_command.py
+++ b/tests/cli/commands/test_task_command.py
@@ -1070,7 +1070,7 @@ class TestLoggerMutationHelper:
         assert tgt.propagate is False if target_name else True  # root 
propagate unchanged
         assert tgt.level == -1
 
-    def test_apply_no_replace(self):
+    def test_apply_no_replace(self, clear_all_logger_handlers):
         """
         Handlers, level and propagate should be applied on target.
         """
diff --git a/tests/conftest.py b/tests/conftest.py
index c5c6441f0b..de13fe99c4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -16,12 +16,15 @@
 # under the License.
 from __future__ import annotations
 
+import logging
 import os
 import sys
 from typing import TYPE_CHECKING
 
 import pytest
 
+from tests_common.test_utils.log_handlers import non_pytest_handlers
+
 # We should set these before loading _any_ of the rest of airflow so that the
 # unit test mode config is set as early as possible.
 assert "airflow" not in sys.modules, "No airflow module can be imported before 
these lines"
@@ -58,6 +61,26 @@ def reset_environment():
             os.environ[key] = init_env[key]
 
 
+def remove_non_pytest_log_handlers(logger, *classes):
+    for handler in non_pytest_handlers(logger.handlers):
+        logger.removeHandler(handler)
+
+
+def remove_all_non_pytest_log_handlers():
+    # Remove handlers from the root logger
+    remove_non_pytest_log_handlers(logging.getLogger())
+    # Remove handlers from all other loggers
+    for logger_name in logging.root.manager.loggerDict:
+        remove_non_pytest_log_handlers(logging.getLogger(logger_name))
+
+
[email protected]
+def clear_all_logger_handlers():
+    remove_all_non_pytest_log_handlers()
+    yield
+    remove_all_non_pytest_log_handlers()
+
+
 if TYPE_CHECKING:
     # Static checkers do not know about pytest fixtures' types and return,
     # In case if them distributed through third party packages.
diff --git a/tests/jobs/test_triggerer_job.py b/tests/jobs/test_triggerer_job.py
index cc457687b0..3ad03b0c35 100644
--- a/tests/jobs/test_triggerer_job.py
+++ b/tests/jobs/test_triggerer_job.py
@@ -49,6 +49,7 @@ from airflow.utils.types import DagRunType
 
 from tests.core.test_logging_config import reset_logging
 from tests_common.test_utils.db import clear_db_dags, clear_db_runs
+from tests_common.test_utils.log_handlers import non_pytest_handlers
 
 pytestmark = pytest.mark.db_test
 
@@ -772,9 +773,6 @@ def test_queue_listener():
     importlib.reload(airflow_local_settings)
     configure_logging()
 
-    def non_pytest_handlers(val):
-        return [h for h in val if "pytest" not in h.__module__]
-
     import logging
 
     log = logging.getLogger()
diff --git a/tests/jobs/test_triggerer_job_logging.py 
b/tests/jobs/test_triggerer_job_logging.py
index 335f9e6b81..5e6a8e3ac8 100644
--- a/tests/jobs/test_triggerer_job_logging.py
+++ b/tests/jobs/test_triggerer_job_logging.py
@@ -32,10 +32,7 @@ from airflow.utils.log.logging_mixin import 
RedirectStdHandler
 from airflow.utils.log.trigger_handler import DropTriggerLogsFilter, 
TriggererHandlerWrapper
 
 from tests_common.test_utils.config import conf_vars
-
-
-def non_pytest_handlers(val):
-    return [h for h in val if "pytest" not in h.__module__]
+from tests_common.test_utils.log_handlers import non_pytest_handlers
 
 
 def assert_handlers(logger, *classes):
@@ -44,12 +41,6 @@ def assert_handlers(logger, *classes):
     return handlers
 
 
-def clear_logger_handlers(log):
-    for h in log.handlers[:]:
-        if "pytest" not in h.__module__:
-            log.removeHandler(h)
-
-
 @pytest.fixture(autouse=True)
 def reload_triggerer_job():
     importlib.reload(triggerer_job_runner)
@@ -64,7 +55,6 @@ def test_configure_trigger_log_handler_file():
     """
     # reset logging
     root_logger = logging.getLogger()
-    clear_logger_handlers(root_logger)
     configure_logging()
 
     # before config
@@ -168,15 +158,12 @@ not_found_message = ["Could not find log handler suitable 
for individual trigger
         ("non_file_task_handler", logging.Handler, not_found_message),
     ],
 )
-def test_configure_trigger_log_handler_not_file_task_handler(cfg, cls, msg):
+def test_configure_trigger_log_handler_not_file_task_handler(cfg, cls, msg, 
clear_all_logger_handlers):
     """
     No root handler configured.
     When non FileTaskHandler is configured, don't modify.
     When an incompatible subclass of FileTaskHandler is configured, don't 
modify.
     """
-    # reset handlers
-    root_logger = logging.getLogger()
-    clear_logger_handlers(root_logger)
 
     with conf_vars(
         {
@@ -190,6 +177,7 @@ def 
test_configure_trigger_log_handler_not_file_task_handler(cfg, cls, msg):
         configure_logging()
 
     # no root handlers
+    root_logger = logging.getLogger()
     assert_handlers(root_logger)
 
     # default task logger
@@ -328,7 +316,7 @@ root_not_file_task = {
 }
 
 
-def test_configure_trigger_log_handler_root_not_file_task():
+def 
test_configure_trigger_log_handler_root_not_file_task(clear_all_logger_handlers):
     """
     root: A handler that doesn't support trigger or inherit FileTaskHandler
     task: Supports triggerer
diff --git a/tests_common/test_utils/log_handlers.py 
b/tests_common/test_utils/log_handlers.py
new file mode 100644
index 0000000000..b5335c4e1b
--- /dev/null
+++ b/tests_common/test_utils/log_handlers.py
@@ -0,0 +1,21 @@
+# 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
+
+
+def non_pytest_handlers(handlers):
+    return [h for h in handlers if "pytest" not in h.__module__]

Reply via email to