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

potiuk pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new 79d52f5ad08 [v3-3-test] Improve SQLite test DB health checks (#68021) 
(#70733)
79d52f5ad08 is described below

commit 79d52f5ad0849c8b86c5c81e94dab68f3d8c7a56
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu Jul 30 11:26:59 2026 +0200

    [v3-3-test] Improve SQLite test DB health checks (#68021) (#70733)
    
    (cherry picked from commit 84ac434aaa19d520f1c19f4321a3443f4b8f5737)
    
    Co-authored-by: Henry Chen <[email protected]>
---
 airflow-core/src/airflow/utils/db_manager.py       |   8 +
 airflow-core/tests/unit/utils/test_db_manager.py   |   8 +
 devel-common/src/tests_common/pytest_plugin.py     | 178 +++++++-
 .../tests/unit/tests_common/test_pytest_plugin.py  | 463 +++++++++++++++++++++
 4 files changed, 651 insertions(+), 6 deletions(-)

diff --git a/airflow-core/src/airflow/utils/db_manager.py 
b/airflow-core/src/airflow/utils/db_manager.py
index 57173341e88..3fcc0a06ab2 100644
--- a/airflow-core/src/airflow/utils/db_manager.py
+++ b/airflow-core/src/airflow/utils/db_manager.py
@@ -295,6 +295,14 @@ class RunDBManager(LoggingMixin):
         for manager in self._managers:
             RunDBManager._validate(manager)
 
+    def get_required_table_names(self) -> set[str]:
+        """Return table names required by the configured external DB 
managers."""
+        required_tables: set[str] = set()
+        for manager in self._managers:
+            required_tables.update(table.name for table in 
manager.metadata.tables.values())
+            required_tables.add(manager.version_table_name)
+        return required_tables
+
     @staticmethod
     def _validate(manager: BaseDBManager):
         """Validate the external database migration."""
diff --git a/airflow-core/tests/unit/utils/test_db_manager.py 
b/airflow-core/tests/unit/utils/test_db_manager.py
index 1f8f3e7423a..513ecf5855c 100644
--- a/airflow-core/tests/unit/utils/test_db_manager.py
+++ b/airflow-core/tests/unit/utils/test_db_manager.py
@@ -277,6 +277,14 @@ class TestBaseDBManager:
 
 
 class TestRunDBManager:
+    def test_get_required_table_names(self):
+        run_db_manager = _create_run_db_manager(LegacyTablesDBManager)
+
+        assert run_db_manager.get_required_table_names() == {
+            "external_legacy_table",
+            "legacy_alembic_version",
+        }
+
     def test_initdb_and_upgradedb_support_legacy_manager_signatures(self, 
session):
         LegacySignatureExternalManager.initdb_calls = 0
         LegacySignatureExternalManager.upgradedb_calls = 0
diff --git a/devel-common/src/tests_common/pytest_plugin.py 
b/devel-common/src/tests_common/pytest_plugin.py
index eaac4e37fe0..413723ec317 100644
--- a/devel-common/src/tests_common/pytest_plugin.py
+++ b/devel-common/src/tests_common/pytest_plugin.py
@@ -118,6 +118,7 @@ if not keep_env_variables:
                 del os.environ[env_key]
 
 SUPPORTED_DB_BACKENDS = ("sqlite", "postgres", "mysql")
+_SQLITE_DB_HEALTH_REQUIRED_TABLES = {"alembic_version"}
 
 # A bit of a Hack - but we need to check args before they are parsed by pytest 
in order to
 # configure the DB before Airflow gets initialized (which happens at airflow 
import time).
@@ -447,27 +448,192 @@ def initialize_airflow_tests(request):
 
 def _initialize_airflow_db(force_db_init: bool, airflow_home: str | Path):
     db_init_lock_file = Path(airflow_home).joinpath(".airflow_db_initialised")
+    db_health_failure_reason = None
     if not force_db_init and db_init_lock_file.exists():
-        print(
-            "Skipping initializing of the DB as it was initialized already.\n"
-            "You can re-initialize the database by adding --with-db-init flag 
when running tests."
-        )
-        return
+        db_health_failure_reason = _get_airflow_db_health_failure_reason()
+        if db_health_failure_reason:
+            print(
+                "The DB initialization marker exists, but the DB health check 
failed.\n"
+                f"{db_health_failure_reason}\n"
+                "Re-initializing the DB."
+            )
+        else:
+            print(
+                "Skipping initializing of the DB as it was initialized 
already.\n"
+                "You can re-initialize the database by adding --with-db-init 
flag when running tests."
+            )
+            return
 
     from tests_common.test_utils.db import initial_db_init
 
     if force_db_init:
         print("Initializing the DB - forced with --with-db-init flag.")
+    elif db_health_failure_reason:
+        print("Initializing the DB - existing DB did not pass the health 
check.")
     else:
         print(
             "Initializing the DB - first time after entering the container.\n"
             "Initialization can be also forced by adding --with-db-init flag 
when running tests."
         )
 
-    initial_db_init()
+    try:
+        initial_db_init()
+    except _get_db_maintenance_exception_types() as ex:
+        if sqlite_db_file := _get_configured_sqlite_db_file():
+            raise RuntimeError(
+                f"Unable to re-initialize the SQLite test DB. Remove 
`{sqlite_db_file}` and rerun pytest."
+            ) from ex
+        raise
     db_init_lock_file.touch(exist_ok=True)
 
 
+def _get_airflow_db_health_failure_reason() -> str | None:
+    from airflow.configuration import conf
+    from airflow.models import import_all_models
+    from airflow.models.base import Base
+
+    sql_alchemy_conn = conf.get("database", "sql_alchemy_conn")
+    import_all_models()
+    required_tables = set(Base.metadata.tables) | 
_SQLITE_DB_HEALTH_REQUIRED_TABLES
+    table_health_failure_reason = _get_sqlite_db_health_failure_reason(
+        sql_alchemy_conn,
+        required_tables=required_tables,
+    )
+    if table_health_failure_reason:
+        return table_health_failure_reason
+
+    if not _is_sqlite_db(sql_alchemy_conn):
+        return None
+
+    core_migration_health_failure_reason = 
_get_core_migration_health_failure_reason()
+    if core_migration_health_failure_reason:
+        return core_migration_health_failure_reason
+
+    return _get_external_db_manager_health_failure_reason(sql_alchemy_conn)
+
+
+def _is_sqlite_db(sql_alchemy_conn: str) -> bool:
+    from sqlalchemy.engine import make_url
+
+    return make_url(sql_alchemy_conn).get_backend_name() == "sqlite"
+
+
+def _get_core_migration_health_failure_reason() -> str | None:
+    from airflow.utils.db import _configured_alembic_environment
+
+    try:
+        with _configured_alembic_environment() as env:
+            context = env.get_context()
+            source_heads = set(env.script.get_heads())
+            db_heads = set(context.get_current_heads())
+    except _get_db_maintenance_exception_types() as ex:
+        return f"Unable to inspect SQLite test DB core migration state: {ex}"
+
+    if source_heads == db_heads:
+        return None
+
+    db_heads_display = ", ".join(sorted(db_heads)) or "<none>"
+    source_heads_display = ", ".join(sorted(source_heads)) or "<none>"
+    return (
+        "SQLite test DB migration revision does not match the current Airflow 
migration head. "
+        f"DB heads: {db_heads_display}; source heads: {source_heads_display}."
+    )
+
+
+def _get_external_db_manager_health_failure_reason(sql_alchemy_conn: str) -> 
str | None:
+    from airflow import settings
+    from airflow.utils.db_manager import RunDBManager
+
+    external_db_manager = RunDBManager()
+    required_tables = external_db_manager.get_required_table_names()
+    if not required_tables:
+        return None
+
+    table_health_failure_reason = _get_sqlite_db_health_failure_reason(
+        sql_alchemy_conn,
+        required_tables=required_tables,
+    )
+    if table_health_failure_reason:
+        return table_health_failure_reason
+
+    session_factory = settings.Session
+    if session_factory is None:
+        return "Unable to inspect SQLite test DB external DB manager migration 
state: settings.Session is not configured."
+
+    session = session_factory()
+    try:
+        if external_db_manager.check_migration(session):
+            return None
+    except _get_db_maintenance_exception_types() as ex:
+        return f"Unable to inspect SQLite test DB external DB manager 
migration state: {ex}"
+    finally:
+        session.close()
+
+    return "SQLite test DB external DB manager migrations do not match their 
current migration heads."
+
+
+def _get_db_maintenance_exception_types() -> tuple[type[Exception], ...]:
+    from alembic.script.revision import RevisionError
+    from alembic.util.exc import CommandError
+    from sqlalchemy.exc import SQLAlchemyError
+
+    return SQLAlchemyError, CommandError, RevisionError
+
+
+def _get_sqlite_db_health_failure_reason(
+    sql_alchemy_conn: str,
+    *,
+    required_tables: set[str],
+) -> str | None:
+    from sqlalchemy import create_engine, inspect
+    from sqlalchemy.engine import make_url
+    from sqlalchemy.exc import SQLAlchemyError
+
+    url = make_url(sql_alchemy_conn)
+    if url.get_backend_name() != "sqlite":
+        return None
+
+    sqlite_db_file = _get_sqlite_db_file(sql_alchemy_conn)
+    if not sqlite_db_file:
+        return "SQLite test DB uses an in-memory URL, so it must be 
re-initialized for this test run."
+
+    if not sqlite_db_file.exists():
+        return f"SQLite test DB file `{sqlite_db_file}` does not exist."
+
+    engine = create_engine(url)
+    try:
+        try:
+            existing_tables = set(inspect(engine).get_table_names())
+        except SQLAlchemyError as ex:
+            return f"Unable to inspect SQLite test DB schema: {ex}"
+    finally:
+        engine.dispose()
+
+    missing_tables = sorted(required_tables - existing_tables)
+    if missing_tables:
+        missing_table_list = ", ".join(missing_tables[:10])
+        if len(missing_tables) > 10:
+            missing_table_list = f"{missing_table_list}, ... 
({len(missing_tables)} total)"
+        return f"SQLite test DB schema is missing expected tables: 
{missing_table_list}."
+
+    return None
+
+
+def _get_configured_sqlite_db_file() -> Path | None:
+    from airflow.configuration import conf
+
+    return _get_sqlite_db_file(conf.get("database", "sql_alchemy_conn"))
+
+
+def _get_sqlite_db_file(sql_alchemy_conn: str) -> Path | None:
+    from sqlalchemy.engine import make_url
+
+    url = make_url(sql_alchemy_conn)
+    if url.get_backend_name() != "sqlite" or not url.database or url.database 
== ":memory:":
+        return None
+    return Path(url.database)
+
+
 def _initialize_kerberos():
     kerberos = os.environ.get("KRB5_KTNAME")
     if not kerberos:
diff --git a/devel-common/tests/unit/tests_common/test_pytest_plugin.py 
b/devel-common/tests/unit/tests_common/test_pytest_plugin.py
new file mode 100644
index 00000000000..c4fed2466d6
--- /dev/null
+++ b/devel-common/tests/unit/tests_common/test_pytest_plugin.py
@@ -0,0 +1,463 @@
+#
+# 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 sys
+import types
+from contextlib import contextmanager
+from typing import Any
+
+import pytest
+from sqlalchemy import create_engine, text
+from sqlalchemy.exc import SQLAlchemyError
+
+from tests_common import pytest_plugin
+
+
+def _create_sqlite_db(sqlite_db_url: str, table_names: set[str]) -> None:
+    engine = create_engine(sqlite_db_url)
+    try:
+        with engine.begin() as conn:
+            for table_name in table_names:
+                conn.execute(text(f"CREATE TABLE {table_name} (id INTEGER 
PRIMARY KEY)"))
+    finally:
+        engine.dispose()
+
+
+class _FakeMigrationContext:
+    def __init__(self, db_heads: set[str], db_heads_exception: Exception | 
None = None) -> None:
+        self.db_heads = db_heads
+        self.db_heads_exception = db_heads_exception
+
+    def get_current_heads(self) -> set[str]:
+        if self.db_heads_exception:
+            raise self.db_heads_exception
+        return self.db_heads
+
+
+class _FakeAlembicScript:
+    def __init__(self, source_heads: set[str]) -> None:
+        self.source_heads = source_heads
+
+    def get_heads(self) -> set[str]:
+        return self.source_heads
+
+
+class _FakeAlembicEnvironment:
+    def __init__(
+        self, *, db_heads: set[str], source_heads: set[str], 
db_heads_exception: Exception | None = None
+    ) -> None:
+        self.script = _FakeAlembicScript(source_heads)
+        self._context = _FakeMigrationContext(db_heads, db_heads_exception)
+
+    def get_context(self) -> _FakeMigrationContext:
+        return self._context
+
+
+def _install_fake_airflow_db_module(
+    monkeypatch,
+    *,
+    db_heads: set[str],
+    source_heads: set[str],
+    db_heads_exception: Exception | None = None,
+) -> None:
+    @contextmanager
+    def configured_alembic_environment():
+        yield _FakeAlembicEnvironment(
+            db_heads=db_heads,
+            source_heads=source_heads,
+            db_heads_exception=db_heads_exception,
+        )
+
+    fake_airflow_module = types.ModuleType("airflow")
+    fake_utils_module = types.ModuleType("airflow.utils")
+    fake_db_module = types.ModuleType("airflow.utils.db")
+    setattr(fake_db_module, "_configured_alembic_environment", 
configured_alembic_environment)
+    setattr(fake_airflow_module, "utils", fake_utils_module)
+    setattr(fake_utils_module, "db", fake_db_module)
+
+    monkeypatch.setitem(sys.modules, "airflow", fake_airflow_module)
+    monkeypatch.setitem(sys.modules, "airflow.utils", fake_utils_module)
+    monkeypatch.setitem(sys.modules, "airflow.utils.db", fake_db_module)
+
+
+class _FakeConf:
+    def __init__(self, sql_alchemy_conn: str) -> None:
+        self.sql_alchemy_conn = sql_alchemy_conn
+
+    def get(self, section: str, key: str) -> str:
+        return self.sql_alchemy_conn
+
+
+class _FakeMetadata:
+    tables = {"dag": object()}
+
+
+class _FakeBase:
+    metadata = _FakeMetadata()
+
+
+class _FakeTable:
+    def __init__(self, name: str) -> None:
+        self.name = name
+
+
+class _FakeSession:
+    def __init__(self) -> None:
+        self.closed = False
+
+    def close(self) -> None:
+        self.closed = True
+
+
+def _install_fake_airflow_metadata_modules(monkeypatch, *, sql_alchemy_conn: 
str) -> None:
+    fake_airflow_module = types.ModuleType("airflow")
+    fake_configuration_module = types.ModuleType("airflow.configuration")
+    fake_models_module = types.ModuleType("airflow.models")
+    fake_base_module = types.ModuleType("airflow.models.base")
+
+    setattr(fake_configuration_module, "conf", _FakeConf(sql_alchemy_conn))
+    setattr(fake_models_module, "import_all_models", lambda: None)
+    setattr(fake_base_module, "Base", _FakeBase)
+    setattr(fake_airflow_module, "configuration", fake_configuration_module)
+    setattr(fake_airflow_module, "models", fake_models_module)
+    setattr(fake_models_module, "base", fake_base_module)
+
+    monkeypatch.setitem(sys.modules, "airflow", fake_airflow_module)
+    monkeypatch.setitem(sys.modules, "airflow.configuration", 
fake_configuration_module)
+    monkeypatch.setitem(sys.modules, "airflow.models", fake_models_module)
+    monkeypatch.setitem(sys.modules, "airflow.models.base", fake_base_module)
+
+
+def _install_fake_external_db_manager_modules(
+    monkeypatch,
+    *,
+    managers: list[Any],
+    migration_check_result: bool,
+    session: _FakeSession,
+    migration_check_exception: Exception | None = None,
+) -> None:
+    class FakeRunDBManager:
+        def get_required_table_names(self) -> set[str]:
+            required_tables: set[str] = set()
+            for manager in managers:
+                required_tables.update(table.name for table in 
manager.metadata.tables.values())
+                required_tables.add(manager.version_table_name)
+            return required_tables
+
+        def check_migration(self, received_session) -> bool:
+            assert received_session is session
+            if migration_check_exception:
+                raise migration_check_exception
+            return migration_check_result
+
+    fake_airflow_module = types.ModuleType("airflow")
+    fake_settings_module = types.ModuleType("airflow.settings")
+    fake_utils_module = types.ModuleType("airflow.utils")
+    fake_db_manager_module = types.ModuleType("airflow.utils.db_manager")
+
+    setattr(fake_settings_module, "Session", lambda: session)
+    setattr(fake_db_manager_module, "RunDBManager", FakeRunDBManager)
+    setattr(fake_airflow_module, "settings", fake_settings_module)
+    setattr(fake_airflow_module, "utils", fake_utils_module)
+    setattr(fake_utils_module, "db_manager", fake_db_manager_module)
+
+    monkeypatch.setitem(sys.modules, "airflow", fake_airflow_module)
+    monkeypatch.setitem(sys.modules, "airflow.settings", fake_settings_module)
+    monkeypatch.setitem(sys.modules, "airflow.utils", fake_utils_module)
+    monkeypatch.setitem(sys.modules, "airflow.utils.db_manager", 
fake_db_manager_module)
+
+
+class TestSqliteDbHealthCheck:
+    def test_non_sqlite_db_is_not_checked(self):
+        assert (
+            pytest_plugin._get_sqlite_db_health_failure_reason(
+                "postgresql://localhost/airflow",
+                required_tables={"dag"},
+            )
+            is None
+        )
+
+    def test_missing_sqlite_db_file_fails_health_check(self, tmp_path):
+        missing_db_url = f"sqlite:///{tmp_path / 'missing.db'}"
+
+        reason = pytest_plugin._get_sqlite_db_health_failure_reason(
+            missing_db_url,
+            required_tables={"dag"},
+        )
+
+        assert reason == f"SQLite test DB file `{tmp_path / 'missing.db'}` 
does not exist."
+
+    def test_in_memory_sqlite_db_fails_health_check(self):
+        reason = pytest_plugin._get_sqlite_db_health_failure_reason(
+            "sqlite:///:memory:",
+            required_tables={"dag"},
+        )
+
+        assert (
+            reason == "SQLite test DB uses an in-memory URL, so it must be 
re-initialized for this test run."
+        )
+
+    def test_missing_required_tables_fail_health_check(self, tmp_path):
+        sqlite_db_url = f"sqlite:///{tmp_path / 'airflow.db'}"
+        _create_sqlite_db(sqlite_db_url, {"alembic_version"})
+
+        reason = pytest_plugin._get_sqlite_db_health_failure_reason(
+            sqlite_db_url,
+            required_tables={"alembic_version", "dag", "dag_run"},
+        )
+
+        assert reason == "SQLite test DB schema is missing expected tables: 
dag, dag_run."
+
+    def test_existing_required_tables_pass_health_check(self, tmp_path):
+        sqlite_db_url = f"sqlite:///{tmp_path / 'airflow.db'}"
+        _create_sqlite_db(sqlite_db_url, {"alembic_version", "dag", "dag_run"})
+
+        assert (
+            pytest_plugin._get_sqlite_db_health_failure_reason(
+                sqlite_db_url,
+                required_tables={"alembic_version", "dag", "dag_run"},
+            )
+            is None
+        )
+
+
+class TestAirflowDbHealthCheck:
+    def test_sqlite_db_health_check_includes_core_migration_health(self, 
monkeypatch):
+        _install_fake_airflow_metadata_modules(monkeypatch, 
sql_alchemy_conn="sqlite:////tmp/airflow.db")
+
+        def get_sqlite_db_health_failure_reason(
+            sql_alchemy_conn: str, *, required_tables: set[str]
+        ) -> str | None:
+            assert sql_alchemy_conn == "sqlite:////tmp/airflow.db"
+            assert required_tables == {"alembic_version", "dag"}
+            return None
+
+        monkeypatch.setattr(
+            pytest_plugin,
+            "_get_sqlite_db_health_failure_reason",
+            get_sqlite_db_health_failure_reason,
+        )
+        monkeypatch.setattr(
+            pytest_plugin,
+            "_get_core_migration_health_failure_reason",
+            lambda: "SQLite test DB migration revision does not match.",
+        )
+
+        assert (
+            pytest_plugin._get_airflow_db_health_failure_reason()
+            == "SQLite test DB migration revision does not match."
+        )
+
+    def test_sqlite_db_health_check_includes_external_db_manager_health(self, 
monkeypatch):
+        _install_fake_airflow_metadata_modules(monkeypatch, 
sql_alchemy_conn="sqlite:////tmp/airflow.db")
+        monkeypatch.setattr(pytest_plugin, 
"_get_sqlite_db_health_failure_reason", lambda *_, **__: None)
+        monkeypatch.setattr(pytest_plugin, 
"_get_core_migration_health_failure_reason", lambda: None)
+        monkeypatch.setattr(
+            pytest_plugin,
+            "_get_external_db_manager_health_failure_reason",
+            lambda sql_alchemy_conn: "SQLite test DB external DB manager 
migrations do not match.",
+        )
+
+        assert (
+            pytest_plugin._get_airflow_db_health_failure_reason()
+            == "SQLite test DB external DB manager migrations do not match."
+        )
+
+
+class TestCoreMigrationHealthCheck:
+    def test_matching_core_migration_heads_pass_health_check(self, 
monkeypatch):
+        _install_fake_airflow_db_module(
+            monkeypatch, db_heads={"current_revision"}, 
source_heads={"current_revision"}
+        )
+
+        assert pytest_plugin._get_core_migration_health_failure_reason() is 
None
+
+    def test_mismatched_core_migration_heads_fail_health_check(self, 
monkeypatch):
+        _install_fake_airflow_db_module(
+            monkeypatch, db_heads={"old_revision"}, 
source_heads={"current_revision"}
+        )
+
+        reason = pytest_plugin._get_core_migration_health_failure_reason()
+
+        assert (
+            reason == "SQLite test DB migration revision does not match the 
current Airflow migration head. "
+            "DB heads: old_revision; source heads: current_revision."
+        )
+
+    def test_core_migration_inspection_error_fails_health_check(self, 
monkeypatch):
+        _install_fake_airflow_db_module(
+            monkeypatch,
+            db_heads=set(),
+            source_heads={"current_revision"},
+            db_heads_exception=SQLAlchemyError("malformed alembic_version"),
+        )
+
+        assert (
+            pytest_plugin._get_core_migration_health_failure_reason()
+            == "Unable to inspect SQLite test DB core migration state: 
malformed alembic_version"
+        )
+
+
+class TestExternalDbManagerHealthCheck:
+    def test_missing_external_db_manager_table_fails_health_check(self, 
monkeypatch):
+        class FakeExternalDbManager:
+            metadata = types.SimpleNamespace(
+                tables={"external_table": _FakeTable("external_table")},
+            )
+            version_table_name = "alembic_version_external"
+
+        _install_fake_external_db_manager_modules(
+            monkeypatch,
+            managers=[FakeExternalDbManager],
+            migration_check_result=True,
+            session=_FakeSession(),
+        )
+
+        def get_sqlite_db_health_failure_reason(
+            sql_alchemy_conn: str, *, required_tables: set[str]
+        ) -> str | None:
+            assert sql_alchemy_conn == "sqlite:////tmp/airflow.db"
+            assert required_tables == {"alembic_version_external", 
"external_table"}
+            return "SQLite test DB schema is missing expected tables: 
external_table."
+
+        monkeypatch.setattr(
+            pytest_plugin,
+            "_get_sqlite_db_health_failure_reason",
+            get_sqlite_db_health_failure_reason,
+        )
+
+        assert (
+            
pytest_plugin._get_external_db_manager_health_failure_reason("sqlite:////tmp/airflow.db")
+            == "SQLite test DB schema is missing expected tables: 
external_table."
+        )
+
+    def test_external_db_manager_migration_mismatch_fails_health_check(self, 
monkeypatch):
+        class FakeExternalDbManager:
+            metadata = types.SimpleNamespace(
+                tables={"external_table": _FakeTable("external_table")},
+            )
+            version_table_name = "alembic_version_external"
+
+        session = _FakeSession()
+        _install_fake_external_db_manager_modules(
+            monkeypatch,
+            managers=[FakeExternalDbManager],
+            migration_check_result=False,
+            session=session,
+        )
+        monkeypatch.setattr(pytest_plugin, 
"_get_sqlite_db_health_failure_reason", lambda *_, **__: None)
+
+        assert (
+            
pytest_plugin._get_external_db_manager_health_failure_reason("sqlite:////tmp/airflow.db")
+            == "SQLite test DB external DB manager migrations do not match 
their current migration heads."
+        )
+        assert session.closed is True
+
+    def 
test_external_db_manager_migration_inspection_error_fails_health_check(self, 
monkeypatch):
+        class FakeExternalDbManager:
+            metadata = types.SimpleNamespace(
+                tables={"external_table": _FakeTable("external_table")},
+            )
+            version_table_name = "alembic_version_external"
+
+        session = _FakeSession()
+        _install_fake_external_db_manager_modules(
+            monkeypatch,
+            managers=[FakeExternalDbManager],
+            migration_check_result=True,
+            session=session,
+            migration_check_exception=SQLAlchemyError("malformed external 
version table"),
+        )
+        monkeypatch.setattr(pytest_plugin, 
"_get_sqlite_db_health_failure_reason", lambda *_, **__: None)
+
+        assert (
+            
pytest_plugin._get_external_db_manager_health_failure_reason("sqlite:////tmp/airflow.db")
+            == "Unable to inspect SQLite test DB external DB manager migration 
state: "
+            "malformed external version table"
+        )
+        assert session.closed is True
+
+
+class TestInitializeAirflowDb:
+    def test_skips_initialization_when_marker_exists_and_db_is_healthy(self, 
monkeypatch, tmp_path):
+        called = False
+
+        def initial_db_init():
+            nonlocal called
+            called = True
+
+        fake_db_module = types.ModuleType("tests_common.test_utils.db")
+        setattr(fake_db_module, "initial_db_init", initial_db_init)
+        monkeypatch.setitem(sys.modules, "tests_common.test_utils.db", 
fake_db_module)
+        monkeypatch.setattr(pytest_plugin, 
"_get_airflow_db_health_failure_reason", lambda: None)
+        tmp_path.joinpath(".airflow_db_initialised").touch()
+
+        pytest_plugin._initialize_airflow_db(force_db_init=False, 
airflow_home=tmp_path)
+
+        assert called is False
+
+    def test_initializes_when_marker_exists_but_db_health_check_fails(self, 
monkeypatch, tmp_path):
+        called = False
+
+        def initial_db_init():
+            nonlocal called
+            called = True
+
+        fake_db_module = types.ModuleType("tests_common.test_utils.db")
+        setattr(fake_db_module, "initial_db_init", initial_db_init)
+        monkeypatch.setitem(sys.modules, "tests_common.test_utils.db", 
fake_db_module)
+        monkeypatch.setattr(
+            pytest_plugin,
+            "_get_airflow_db_health_failure_reason",
+            lambda: "SQLite test DB file `/tmp/airflow.db` does not exist.",
+        )
+        tmp_path.joinpath(".airflow_db_initialised").touch()
+
+        pytest_plugin._initialize_airflow_db(force_db_init=False, 
airflow_home=tmp_path)
+
+        assert called is True
+
+    def 
test_sqlite_initialization_failure_includes_db_file_removal_guidance(self, 
monkeypatch, tmp_path):
+        sqlite_db_file = tmp_path / "airflow.db"
+
+        def initial_db_init():
+            raise SQLAlchemyError("reset failed")
+
+        fake_db_module = types.ModuleType("tests_common.test_utils.db")
+        setattr(fake_db_module, "initial_db_init", initial_db_init)
+        monkeypatch.setitem(sys.modules, "tests_common.test_utils.db", 
fake_db_module)
+        monkeypatch.setattr(
+            pytest_plugin,
+            "_get_airflow_db_health_failure_reason",
+            lambda: "SQLite test DB migration revision does not match.",
+        )
+        _install_fake_airflow_metadata_modules(
+            monkeypatch,
+            sql_alchemy_conn=f"sqlite:///{sqlite_db_file}",
+        )
+        tmp_path.joinpath(".airflow_db_initialised").touch()
+
+        with pytest.raises(RuntimeError) as exc_info:
+            pytest_plugin._initialize_airflow_db(force_db_init=False, 
airflow_home=tmp_path)
+
+        assert str(exc_info.value) == (
+            f"Unable to re-initialize the SQLite test DB. Remove 
`{sqlite_db_file}` and rerun pytest."
+        )
+        assert isinstance(exc_info.value.__cause__, SQLAlchemyError)

Reply via email to