Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/trigger_files/beam_PostCommit_Python_Versions.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to run",
"revision": 5
"revision": 6
}
7 changes: 3 additions & 4 deletions sdks/python/apache_beam/dataframe/io_it_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,12 @@
_LOGGER = logging.getLogger(__name__)

try:
from google.api_core.exceptions import GoogleAPICallError
from apitools.base.py.exceptions import HttpError
except ImportError:
GoogleAPICallError = None
HttpError = None


@unittest.skipIf(
GoogleAPICallError is None, 'GCP dependencies are not installed')
@unittest.skipIf(HttpError is None, 'GCP dependencies are not installed')
class ReadUsingReadGbqTests(unittest.TestCase):
@pytest.mark.it_postcommit
def test_ReadGbq(self):
Expand Down
11 changes: 3 additions & 8 deletions sdks/python/apache_beam/dataframe/io_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@
from apache_beam.testing.util import equal_to

try:
from google.api_core.exceptions import GoogleAPICallError
from apitools.base.py.exceptions import HttpError
except ImportError:
GoogleAPICallError = None
HttpError = None

# Get major, minor version
PD_VERSION = tuple(map(int, pd.__version__.split('.')[0:2]))
Expand Down Expand Up @@ -492,15 +492,10 @@ def test_double_write(self):
set(self.read_all_lines(output + 'out2.csv*')))


@unittest.skipIf(
GoogleAPICallError is None, 'GCP dependencies are not installed')
@unittest.skipIf(HttpError is None, 'GCP dependencies are not installed')
class ReadGbqTransformTests(unittest.TestCase):
@mock.patch.object(BigQueryWrapper, 'get_table')
def test_bad_schema_public_api_direct_read(self, get_table):
try:
bigquery.TableFieldSchema
except AttributeError:
raise ValueError('Please install GCP Dependencies.')
fields = [
bigquery.TableFieldSchema(name='stn', type='DOUBLE', mode="NULLABLE"),
bigquery.TableFieldSchema(name='temp', type='FLOAT64', mode="REPEATED"),
Expand Down
5 changes: 5 additions & 0 deletions sdks/python/apache_beam/io/requestresponse_it_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from apache_beam.coders import coders
from apache_beam.options.pipeline_options import PipelineOptions
from apache_beam.testing.test_pipeline import TestPipeline
from apache_beam.testing.test_utils import require_linux_docker

# pylint: disable=ungrouped-imports
try:
Expand Down Expand Up @@ -206,6 +207,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):

@pytest.mark.uses_testcontainer
class TestRedisCache(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
require_linux_docker()

def setUp(self) -> None:
self.retries = 5
self._start_container()
Expand Down
25 changes: 25 additions & 0 deletions sdks/python/apache_beam/testing/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import struct
import sys
import tempfile
from unittest import SkipTest

import numpy as np

Expand Down Expand Up @@ -186,6 +187,30 @@ def block_imports(*packages):
sys.modules.update(hidden_modules)


def require_linux_docker():
"""Raises ``unittest.SkipTest`` unless Docker can run Linux containers.

Intended for tests that start containers via testcontainers, e.g. from
``setUpClass``. Such tests need a reachable Docker daemon running Linux
containers. Some environments have no daemon at all (e.g. GitHub macOS
runners), while others only run Windows containers (e.g. GitHub Windows
runners).
"""
try:
import docker
client = docker.from_env()
try:
os_type = client.info().get('OSType')
finally:
client.close()
except Exception as e: # pylint: disable=broad-except
raise SkipTest(f'Docker is not available: {e}')
if os_type != 'linux':
raise SkipTest(
f'Docker daemon runs {os_type} containers; '
'Linux containers are required.')


@retry.with_exponential_backoff(
num_retries=3, retry_filter=retry.retry_on_beam_io_error_filter)
def delete_files(file_paths):
Expand Down
37 changes: 37 additions & 0 deletions sdks/python/apache_beam/testing/test_utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,43 @@ def test_block_imports_only_blocks_given_packages(self):
self.assertEqual(attempted_imports, [])


class RequireLinuxDockerTest(unittest.TestCase):
def _fake_docker(self, os_type=None, error=None):
fake_docker = mock.MagicMock()
client = fake_docker.from_env.return_value
if error:
client.info.side_effect = error
else:
client.info.return_value = {'OSType': os_type}
return fake_docker, client

def test_linux_docker_does_not_skip(self):
fake_docker, client = self._fake_docker(os_type='linux')
with mock.patch.dict(sys.modules, {'docker': fake_docker}):
utils.require_linux_docker()
client.close.assert_called_once()

def test_windows_docker_skips(self):
fake_docker, client = self._fake_docker(os_type='windows')
with mock.patch.dict(sys.modules, {'docker': fake_docker}):
with self.assertRaisesRegex(unittest.SkipTest, 'windows containers'):
utils.require_linux_docker()
client.close.assert_called_once()

def test_unreachable_docker_skips(self):
fake_docker, client = self._fake_docker(
error=ConnectionError('no such file'))
with mock.patch.dict(sys.modules, {'docker': fake_docker}):
with self.assertRaisesRegex(unittest.SkipTest, 'Docker is not available'):
utils.require_linux_docker()
client.close.assert_called_once()

def test_missing_docker_sdk_skips(self):
with utils.block_imports('docker'):
with self.assertRaisesRegex(unittest.SkipTest, 'Docker is not available'):
utils.require_linux_docker()


class LCGeneratorTest(unittest.TestCase):
Generator = utils.LCGenerator

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import apache_beam as beam
from apache_beam.coders import coders
from apache_beam.testing.test_pipeline import TestPipeline
from apache_beam.testing.test_utils import require_linux_docker
from apache_beam.testing.util import assert_that
from apache_beam.testing.util import equal_to

Expand Down Expand Up @@ -589,6 +590,8 @@ def setUpClass(cls):
# Skip setup for the base class.
raise unittest.SkipTest("Base class - no db_adapter defined")

require_linux_docker()

# Type hint data from subclasses.
cls._db_adapter: DatabaseTypeAdapter

Expand Down
38 changes: 29 additions & 9 deletions sdks/python/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
"""Pytest configuration and custom hooks."""

import gc
import math
import os
import sys
import threading
import time
from types import SimpleNamespace

import pytest

Expand Down Expand Up @@ -150,6 +150,32 @@ def enhance_mock_stability():
time.sleep(0.05)


def _configure_testcontainers_timeouts():
"""Applies TC_* environment settings to testcontainers' global config.

testcontainers 4.x computes its wait timeout as ``max_tries * sleep_time``
from ``testcontainers_config``. It reads ``TC_MAX_TRIES`` and
``TC_POOLING_INTERVAL`` itself, but not ``TC_TIMEOUT`` or ``TC_SLEEP_TIME``,
which Beam's CI workflows set. Translate them here so that the effective
timeout is at least ``TC_TIMEOUT`` seconds.
"""
from testcontainers.core.config import testcontainers_config

sleep_time = float(
os.getenv("TC_SLEEP_TIME") or os.getenv("TC_POOLING_INTERVAL") or "1")
timeout = float(os.getenv("TC_TIMEOUT", "120"))
max_tries = int(os.getenv("TC_MAX_TRIES", "0"))

testcontainers_config.sleep_time = sleep_time
testcontainers_config.max_tries = max(
max_tries, math.ceil(timeout / sleep_time))
print(
"Set testcontainers max_tries=%d, sleep_time=%s (timeout=%ss)" % (
testcontainers_config.max_tries,
testcontainers_config.sleep_time,
testcontainers_config.timeout))


def pytest_configure(config):
"""Saves options added in pytest_addoption for later use.
This is necessary since pytest-xdist workers do not have the
Expand All @@ -159,14 +185,8 @@ def pytest_configure(config):
# for the entire test session.
print("\n--- Applying global testcontainers timeout configuration ---")
try:
from testcontainers.core import waiting_utils
waiting_utils.config = SimpleNamespace(
timeout=int(os.getenv("TC_TIMEOUT", "120")),
max_tries=int(os.getenv("TC_MAX_TRIES", "120")),
sleep_time=float(os.getenv("TC_SLEEP_TIME", "1")),
)
print("Successfully set waiting utils config")
except ModuleNotFoundError:
_configure_testcontainers_timeouts()
except ImportError:
print("The testcontainers library is not installed.")

TestPipeline.pytest_test_pipeline_options = config.getoption(
Expand Down
Loading