11import tempfile
22import uuid
3+ from dataclasses import asdict
34from pathlib import Path
45from typing import Any , Callable , Literal
56
67import orjson
8+ from typing_extensions import Protocol , runtime_checkable
79
8- from airbyte_cdk import Connector
910from airbyte_cdk .models import (
11+ ConfiguredAirbyteCatalog ,
1012 Status ,
1113)
12- from airbyte_cdk .sources .abstract_source import AbstractSource
13- from airbyte_cdk .sources .declarative .declarative_source import DeclarativeSource
1414from airbyte_cdk .test import entrypoint_wrapper
1515from airbyte_cdk .test .declarative .models import (
1616 ConnectorTestScenario ,
1717)
1818
1919
20+ @runtime_checkable
21+ class IConnector (Protocol ):
22+ """A connector that can be run in a test scenario."""
23+
24+ def launch (self , args : list [str ] | None ) -> None :
25+ """Launch the connector with the given arguments."""
26+ ...
27+
28+
2029def run_test_job (
21- connector : Connector | type [Connector ] | Callable [[], Connector ],
30+ connector : IConnector | type [IConnector ] | Callable [[], IConnector ],
2231 verb : Literal ["read" , "check" , "discover" ],
2332 test_instance : ConnectorTestScenario ,
2433 * ,
25- catalog : dict [str , Any ] | None = None ,
34+ catalog : ConfiguredAirbyteCatalog | dict [str , Any ] | None = None ,
2635) -> entrypoint_wrapper .EntrypointOutput :
2736 """Run a test job from provided CLI args and return the result."""
2837 if not connector :
2938 raise ValueError ("Connector is required" )
3039
31- connector_obj : Connector
32- if isinstance (connector , type ):
40+ if catalog and isinstance (catalog , ConfiguredAirbyteCatalog ):
41+ # Convert the catalog to a dict if it's already a ConfiguredAirbyteCatalog.
42+ catalog = asdict (catalog )
43+
44+ connector_obj : IConnector
45+ if isinstance (connector , type ) or callable (connector ):
46+ # If the connector is a class or a factory lambda, instantiate it.
3347 connector_obj = connector ()
34- elif isinstance (connector , Connector ):
35- connector_obj = connector
36- elif isinstance (connector , DeclarativeSource | AbstractSource ):
48+ elif isinstance (connector , IConnector ):
3749 connector_obj = connector
38- elif isinstance (connector , Callable ):
39- try :
40- connector_obj = connector ()
41- except Exception as ex :
42- if not test_instance .expect_exception :
43- raise
44-
45- return entrypoint_wrapper .EntrypointOutput (
46- messages = [],
47- uncaught_exception = ex ,
48- )
4950 else :
50- raise ValueError (f"Invalid source type: { type (connector )} " )
51+ raise ValueError (
52+ f"Invalid connector input: { type (connector )} " ,
53+ )
5154
5255 args : list [str ] = [verb ]
5356 if test_instance .config_path :
@@ -82,34 +85,40 @@ def run_test_job(
8285 # Because it *also* can fail, we have ot redundantly wrap it in a try/except block.
8386
8487 result : entrypoint_wrapper .EntrypointOutput = entrypoint_wrapper ._run_command ( # noqa: SLF001 # Non-public API
85- source = connector_obj ,
88+ source = connector_obj , # type: ignore [reportArgumentType]
8689 args = args ,
8790 expecting_exception = test_instance .expect_exception ,
8891 )
8992 if result .errors and not test_instance .expect_exception :
9093 raise AssertionError (
9194 "\n \n " .join (
92- [str (err .trace .error ).replace ("\\ n" , "\n " ) for err in result .errors ],
95+ [str (err .trace .error ).replace ("\\ n" , "\n " ) for err in result .errors if err . trace ],
9396 )
9497 )
9598
9699 if verb == "check" :
97100 # Check is expected to fail gracefully without an exception.
98101 # Instead, we assert that we have a CONNECTION_STATUS message with
99102 # a failure status.
100- assert not result .errors , "Expected no errors from check. Got:\n " + "\n " .join (
101- [ str (error ) for error in result .errors ]
102- )
103+ assert not result .errors , "Expected no errors from check. Got:\n " + "\n " .join ([
104+ str (error ) for error in result .errors
105+ ] )
103106 assert len (result .connection_status_messages ) == 1 , (
104107 "Expected exactly one CONNECTION_STATUS message. Got "
105108 f"{ len (result .connection_status_messages )} :\n "
106109 + "\n " .join ([str (msg ) for msg in result .connection_status_messages ])
107110 )
108111 if test_instance .expect_exception :
109- assert result .connection_status_messages [0 ].connectionStatus .status == Status .FAILED , (
112+ conn_status = result .connection_status_messages [0 ].connectionStatus
113+ assert conn_status , (
114+ "Expected CONNECTION_STATUS message to be present. Got: \n "
115+ + "\n " .join ([str (msg ) for msg in result .connection_status_messages ])
116+ )
117+ assert conn_status .status == Status .FAILED , (
110118 "Expected CONNECTION_STATUS message to be FAILED. Got: \n "
111119 + "\n " .join ([str (msg ) for msg in result .connection_status_messages ])
112120 )
121+
113122 return result
114123
115124 # For all other verbs, we assert check that an exception is raised (or not).
@@ -121,7 +130,14 @@ def run_test_job(
121130 if result .errors :
122131 raise AssertionError (
123132 "\n \n " .join (
124- [str (err .trace .error ).replace ("\\ n" , "\n " ) for err in result .errors ],
133+ [
134+ str (err .trace .error ).replace (
135+ "\\ n" ,
136+ "\n " ,
137+ )
138+ for err in result .errors
139+ if err .trace
140+ ],
125141 )
126142 )
127143
0 commit comments