@@ -361,11 +361,11 @@ def temp_sqlserver_database():
361361
362362class OracleTestContainer (DockerContainer ):
363363 """
364- OracleTestContainer is an updated version of OracleDBContainer that goes
365- ahead and sets the oracle password, waits for logs to establish that the
366- container is ready before calling get_exposed_port, and uses a more modern
367- oracle driver.
368- """
364+ OracleTestContainer is an updated version of OracleDBContainer that goes
365+ ahead and sets the oracle password, waits for logs to establish that the
366+ container is ready before calling get_exposed_port, and uses a more modern
367+ oracle driver.
368+ """
369369 def __init__ (self ):
370370 super ().__init__ ("gvenzl/oracle-xe:21-slim" )
371371 self .with_env ("ORACLE_PASSWORD" , "oracle" )
@@ -483,6 +483,22 @@ def temp_pubsub_emulator(project_id="apache-beam-testing"):
483483
484484
485485def replace_recursive (spec , vars ):
486+ """Recursively replaces string placeholders in a spec with values from vars.
487+
488+ Traverses a nested structure (dicts, lists, or other types). If a string
489+ is encountered and contains placeholders in the format '{key}', it attempts
490+ to replace them using the `vars` dictionary.
491+
492+ Args:
493+ spec: The (potentially nested) structure to process.
494+ vars: A dictionary of variable names to their replacement values.
495+
496+ Returns:
497+ The spec with placeholders replaced.
498+
499+ Raises:
500+ ValueError: If a string formatting error occurs.
501+ """
486502 if isinstance (spec , dict ):
487503 return {
488504 key : replace_recursive (value , vars )
@@ -503,6 +519,20 @@ def replace_recursive(spec, vars):
503519
504520
505521def transform_types (spec ):
522+ """Recursively extracts all transform types from a pipeline specification.
523+
524+ This generator function traverses a nested pipeline specification (likely
525+ parsed from YAML). It identifies and yields the 'type' string for each
526+ transform defined within the specification, including those within
527+ 'composite' or 'chain' structures.
528+
529+ Args:
530+ spec (dict): A dictionary representing a pipeline or transform
531+ specification.
532+
533+ Yields:
534+ str: The 'type' of each transform found in the specification.
535+ """
506536 if spec .get ('type' , None ) in (None , 'composite' , 'chain' ):
507537 if 'source' in spec :
508538 yield from transform_types (spec ['source' ])
@@ -515,8 +545,30 @@ def transform_types(spec):
515545
516546
517547def provider_sets (spec , require_available = False ):
518- """For transforms that are vended by multiple providers, yields all possible
519- combinations of providers to use.
548+ """
549+ Generates all relevant combinations of providers for a given pipeline spec.
550+
551+ This function analyzes a pipeline specification to identify transforms that
552+ can be implemented by multiple underlying providers (e.g., a generic
553+ transform vs. a SQL-backed one). It then yields different "provider sets,"
554+ each representing a unique combination of choices for these multi-provider
555+ transforms.
556+
557+ If no transforms have multiple available providers, it yields a single
558+ provider set using the standard defaults.
559+
560+ Args:
561+ spec (dict): The pipeline specification, typically loaded from YAML.
562+ require_available (bool): If True, raises an error if a provider
563+ needed for a transform is not available. If False (default),
564+ unavailable providers are skipped, potentially reducing the number
565+ of yielded combinations.
566+
567+ Yields:
568+ tuple: A tuple where the first element is a string suffix uniquely
569+ identifying the provider combination (e.g., "MyTransform_SqlProvider_0"),
570+ and the second element is a dictionary mapping transform types to a list
571+ containing the selected provider(s) for that combination.
520572 """
521573 try :
522574 for p in spec ['pipelines' ]:
@@ -566,6 +618,39 @@ def filter_to_available(t, providers):
566618
567619
568620def create_test_methods (spec ):
621+ """Dynamically creates test methods based on a YAML specification.
622+
623+ This function takes a YAML specification (`spec`) which defines pipelines,
624+ fixtures, and potentially options. It iterates through different
625+ combinations of "providers" (which determine how YAML transforms are
626+ implemented, e.g., using Python or SQL).
627+
628+ For each combination of providers:
629+ 1. It constructs a unique test method name (e.g., `test_only`).
630+ 2. It defines a test method that:
631+ a. Sets up any specified fixtures, making their values available as
632+ variables.
633+ b. Mocks the standard YAML providers to use the current combination
634+ of providers for this test run.
635+ c. For each pipeline defined in the `spec`:
636+ i. Creates a `beam.Pipeline` instance with specified options.
637+ ii. Expands the YAML pipeline definition using
638+ `yaml_transform.expand_pipeline`, substituting any fixture
639+ variables.
640+ iii. Runs the Beam pipeline.
641+
642+ The function yields tuples of (test_method_name, test_method_function),
643+ which can then be used to populate a `unittest.TestCase` class.
644+
645+ Args:
646+ spec (dict): A dictionary parsed from a YAML test specification file.
647+ It's expected to have keys like 'fixtures' (optional) and 'pipelines'.
648+
649+ Yields:
650+ tuple: A tuple containing:
651+ - str: The generated name for the test method (e.g., "test_only").
652+ - function: The dynamically generated test method.
653+ """
569654 for suffix , providers in provider_sets (spec ):
570655
571656 def test (self , providers = providers ): # default arg to capture loop value
@@ -593,6 +678,23 @@ def test(self, providers=providers): # default arg to capture loop value
593678
594679
595680def parse_test_files (filepattern ):
681+ """Parses YAML test files and dynamically creates test cases.
682+
683+ This function iterates through all files matching the given glob pattern.
684+ For each YAML file found, it:
685+ 1. Reads the file content.
686+ 2. Determines a test suite name based on the file name.
687+ 3. Calls `create_test_methods` to generate test methods from the
688+ YAML specification.
689+ 4. Dynamically creates a new TestCase class (inheriting from
690+ `unittest.TestCase`) and populates it with the generated test methods.
691+ 5. Adds this newly created TestCase class to the global scope, making it
692+ discoverable by the unittest framework.
693+
694+ Args:
695+ filepattern (str): A glob pattern specifying the YAML test files to parse.
696+ For example, 'path/to/tests/*.yaml'.
697+ """
596698 for path in glob .glob (filepattern ):
597699 with open (path ) as fin :
598700 suite_name = os .path .splitext (os .path .basename (path ))[0 ].title ().replace (
0 commit comments