"""Unit tests for the Chartmetric share-table retry helper.""" from unittest.mock import Mock import pytest from snowflake.connector.errors import ProgrammingError from snowflake_connector.etl_connector import SnowflakeSQLExecutor from feed_ingestion.common import chartmetric_share_retry from feed_ingestion.common.chartmetric_share_retry import ( ChartmetricShareRetryMixin) @pytest.fixture(autouse=True) def _no_sleep(monkeypatch): """Skip real backoff sleeps so tests run instantly.""" monkeypatch.setattr(chartmetric_share_retry.time, 'sleep', lambda _: None) def _share_miss_error(): """Build a transient 'share table missing' ProgrammingError.""" return ProgrammingError( msg=( '002003 (42S02): 01c58af9: SQL compilation error:\n' 'Object CHARTMETRIC.RAW_DATA.ITUNES does not exist ' 'or not authorized.'), errno=2003) def _own_table_miss_error(): """Build a non-share 'table missing' ProgrammingError (a real bug).""" return ProgrammingError( msg=( '002003 (42S02): 01c58af9: SQL compilation error:\n' 'Object FACTS_DB.PUBLIC.STAGING_FACT_CHARTMETRIC_CHARTS ' 'does not exist or not authorized.'), errno=2003) class _Executor(ChartmetricShareRetryMixin, SnowflakeSQLExecutor): """Executor stub mirroring how flow executors mix in the retry. The base `get_cursor()` closes the connection on any error, so a closed connection between attempts is the realistic state; `get_connection()` tracks how many times the retry reopens it. """ def __init__(self): self.get_connection_count = 0 self.snowflake_conn = self.get_connection() def get_connection(self): self.get_connection_count += 1 conn = Mock() conn.is_closed.return_value = True return conn class TestIsTransientShareMiss(object): """Tests for the transient-error predicate.""" def test_true_for_missing_share_table(self): assert chartmetric_share_retry.is_transient_share_miss( _share_miss_error()) is True def test_false_for_missing_own_table(self): assert chartmetric_share_retry.is_transient_share_miss( _own_table_miss_error()) is False def test_false_for_other_errno(self): error = ProgrammingError( msg='001003 (42601): SQL compilation error: syntax error', errno=1003) assert chartmetric_share_retry.is_transient_share_miss(error) is False def test_false_for_non_programming_error(self): assert chartmetric_share_retry.is_transient_share_miss( ValueError('boom')) is False class TestRetryOnMissingShareTable(object): """Tests for the retry behaviour of the executor mixin.""" def test_retries_then_succeeds(self): executor = _Executor() connections_before = executor.get_connection_count base = Mock( side_effect=[_share_miss_error(), _share_miss_error(), 'ok']) with pytest.MonkeyPatch.context() as mp: mp.setattr(SnowflakeSQLExecutor, 'execute', base) result = executor.execute('SELECT 1') assert result == 'ok' assert base.call_count == 3 # Reconnected once per retry (two retries before success). assert executor.get_connection_count - connections_before == 2 def test_permanent_share_outage_gives_up_and_raises(self, caplog): executor = _Executor() base = Mock(side_effect=_share_miss_error()) with pytest.MonkeyPatch.context() as mp: mp.setattr(SnowflakeSQLExecutor, 'execute', base) with pytest.raises(ProgrammingError): executor.execute('SELECT 1') assert base.call_count == chartmetric_share_retry.MAX_ATTEMPTS assert 'giving up' in caplog.text def test_non_transient_error_is_not_retried(self): executor = _Executor() base = Mock(side_effect=_own_table_miss_error()) with pytest.MonkeyPatch.context() as mp: mp.setattr(SnowflakeSQLExecutor, 'execute', base) with pytest.raises(ProgrammingError): executor.execute('SELECT 1') assert base.call_count == 1 def test_fetchall_and_fetchone_are_wrapped(self): executor = _Executor() fetchall = Mock(side_effect=[_share_miss_error(), ['row']]) fetchone = Mock(side_effect=[_share_miss_error(), 'row']) with pytest.MonkeyPatch.context() as mp: mp.setattr(SnowflakeSQLExecutor, 'fetchall', fetchall) mp.setattr(SnowflakeSQLExecutor, 'fetchone', fetchone) all_result = executor.fetchall('SELECT 1') one_result = executor.fetchone('SELECT 1') assert all_result == ['row'] assert one_result == 'row' assert fetchall.call_count == 2 assert fetchone.call_count == 2