2323import itertools
2424import logging
2525import os
26+ import random
2627import sqlite3
28+ import string
2729import unittest
2830import uuid
2931
3335import pytds
3436import sqlalchemy
3537import yaml
38+ from google .cloud import pubsub_v1
39+ from testcontainers .core .container import DockerContainer
40+ from testcontainers .core .waiting_utils import wait_for_logs
41+ from testcontainers .google import PubSubContainer
42+ from testcontainers .kafka import KafkaContainer
3643from testcontainers .mssql import SqlServerContainer
3744from testcontainers .mysql import MySqlContainer
3845from testcontainers .postgres import PostgresContainer
@@ -351,6 +358,129 @@ def temp_sqlserver_database():
351358 raise err
352359
353360
361+ class OracleTestContainer (DockerContainer ):
362+ """
363+ OracleTestContainer is an updated version of OracleDBContainer that goes
364+ ahead and sets the oracle password, waits for logs to establish that the
365+ container is ready before calling get_exposed_port, and uses a more modern
366+ oracle driver.
367+ """
368+ def __init__ (self ):
369+ super ().__init__ ("gvenzl/oracle-xe:21-slim" )
370+ self .with_env ("ORACLE_PASSWORD" , "oracle" )
371+ self .with_exposed_ports (1521 )
372+
373+ def start (self ):
374+ super ().start ()
375+ wait_for_logs (self , "DATABASE IS READY TO USE!" , timeout = 300 )
376+ return self
377+
378+ def get_connection_url (self ):
379+ port = self .get_exposed_port (1521 )
380+ return \
381+ f"oracle+oracledb://system:oracle@localhost:{ port } /?service_name=XEPDB1"
382+
383+
384+ @contextlib .contextmanager
385+ def temp_oracle_database ():
386+ """Context manager to provide a temporary Oracle database for testing.
387+
388+ This function utilizes the 'testcontainers' library to spin up an
389+ Oracle Database instance within a Docker container. It then connects
390+ to this temporary database using 'oracledb', creates a predefined
391+
392+ NOTE: A custom OracleTestContainer class was created due to the current
393+ version (OracleDBContainer) that calls get_exposed_port too soon causing the
394+ service to hang until timeout.
395+
396+ Yields:
397+ str: A JDBC connection string for the temporary Oracle database.
398+ Example format:
399+ "jdbc:oracle:thin:system/oracle@localhost:{port}/XEPDB1"
400+
401+ Raises:
402+ oracledb.Error: If there's an error connecting to or interacting with
403+ the Oracle database during setup.
404+ Exception: Any other exception encountered during the setup process.
405+ """
406+ with OracleTestContainer () as oracle :
407+ engine = sqlalchemy .create_engine (oracle .get_connection_url ())
408+ with engine .connect () as connection :
409+ connection .execute (
410+ sqlalchemy .text (
411+ """
412+ CREATE TABLE tmp_table (
413+ value NUMBER PRIMARY KEY,
414+ rank NUMBER
415+ )
416+ """ ))
417+ connection .commit ()
418+ port = oracle .get_exposed_port (1521 )
419+ yield f"jdbc:oracle:thin:system/oracle@localhost:{ port } /XEPDB1"
420+
421+
422+ @contextlib .contextmanager
423+ def temp_kafka_server ():
424+ """Context manager to provide a temporary Kafka server for testing.
425+
426+ This function utilizes the 'testcontainers' library to spin up a Kafka
427+ instance within a Docker container. It then yields the bootstrap server
428+ string, which can be used by Kafka clients to connect to this temporary
429+ server.
430+
431+ The Docker container and the Kafka instance are automatically managed
432+ and torn down when the context manager exits.
433+
434+ Yields:
435+ str: The bootstrap server string for the temporary Kafka instance.
436+ Example format: "localhost:XXXXX" or "PLAINTEXT://localhost:XXXXX"
437+
438+ Raises:
439+ Exception: If there's an error starting the Kafka container or
440+ interacting with the temporary Kafka server.
441+ """
442+ try :
443+ with KafkaContainer () as kafka_container :
444+ yield kafka_container .get_bootstrap_server ()
445+ except Exception as err :
446+ logging .error ("Error interacting with temporary Kakfa Server: %s" , err )
447+ raise err
448+
449+
450+ @contextlib .contextmanager
451+ def temp_pubsub_emulator (project_id = "apache-beam-testing" ):
452+ """
453+ Context manager to provide a temporary Pub/Sub emulator for testing.
454+
455+ This function uses 'testcontainers' to spin up a Google Cloud SDK
456+ container running the Pub/Sub emulator. It yields the emulator host
457+ string (e.g., "localhost:xxxxx") that can be used to configure Pub/Sub
458+ clients.
459+
460+ The Docker container is automatically managed and torn down when the
461+ context manager exits.
462+
463+ Args:
464+ project_id (str): The GCP project ID to be used by the emulator.
465+ This doesn't need to be a real project for the emulator.
466+
467+ Yields:
468+ str: The host and port for the Pub/Sub emulator (e.g., "localhost:xxxx").
469+ This will be the address to point your Pub/Sub client to.
470+
471+ Raises:
472+ Exception: If the container fails to start or the emulator isn't ready.
473+ """
474+ with PubSubContainer (project = project_id ) as pubsub_container :
475+ publisher = pubsub_v1 .PublisherClient ()
476+ random_front_charactor = random .choice (string .ascii_lowercase )
477+ topic_id = f"{ random_front_charactor } { uuid .uuid4 ().hex [:8 ]} "
478+ topic_name_to_create = \
479+ f"projects/{ pubsub_container .project } /topics/{ topic_id } "
480+ created_topic_object = publisher .create_topic (name = topic_name_to_create )
481+ yield created_topic_object .name
482+
483+
354484def replace_recursive (spec , vars ):
355485 if isinstance (spec , dict ):
356486 return {
@@ -359,7 +489,10 @@ def replace_recursive(spec, vars):
359489 }
360490 elif isinstance (spec , list ):
361491 return [replace_recursive (value , vars ) for value in spec ]
362- elif isinstance (spec , str ) and '{' in spec and '{\n ' not in spec :
492+ # TODO(https://github.com/apache/beam/issues/35067): Consider checking for
493+ # callable in the if branch above instead of checking lambda here.
494+ elif isinstance (
495+ spec , str ) and '{' in spec and '{\n ' not in spec and 'lambda' not in spec :
363496 try :
364497 return spec .format (** vars )
365498 except Exception as exn :
0 commit comments