@@ -117,31 +117,39 @@ def _fn(row):
117117@beam .ptransform .ptransform_fn
118118def test_kafka_read (
119119 pcoll ,
120- format ,
121- topic ,
122- bootstrap_servers ,
123- auto_offset_reset_config ,
124- consumer_config ):
120+ topic : Optional [str ] = None ,
121+ format : Optional [str ] = None ,
122+ schema : Optional [Any ] = None ,
123+ bootstrap_servers : Optional [str ] = None ,
124+ auto_offset_reset_config : Optional [str ] = None ,
125+ consumer_config : Optional [Any ] = None ):
125126 """
126- This PTransform simulates the behavior of the ReadFromKafka transform
127- with the RAW format by simply using some fixed sample text data and
128- encode it to raw bytes.
127+ Mocks the ReadFromKafka transform for testing purposes.
128+
129+ This PTransform simulates the behavior of the ReadFromKafka transform by
130+ reading from predefined in-memory data based on the Kafka topic argument.
129131
130132 Args:
131133 pcoll: The input PCollection.
132- format: The format of the Kafka messages (e.g., 'RAW').
133134 topic: The name of Kafka topic to read from.
135+ format: The format of the Kafka messages (e.g., 'RAW').
136+ schema: The schema of the Kafka messages.
134137 bootstrap_servers: A list of Kafka bootstrap servers to connect to.
135- auto_offset_reset_config: A configuration for the auto offset reset
136- consumer_config: A dictionary containing additional consumer configurations
138+ auto_offset_reset_config: A configuration for the auto offset reset.
139+ consumer_config: A map for additional consumer configuration parameters.
137140
138141 Returns:
139- A PCollection containing the sample text data in bytes .
142+ A PCollection containing the sample data.
140143 """
141144
142- return (
143- pcoll | beam .Create (input_data .text_data ().split ('\n ' ))
144- | beam .Map (lambda element : beam .Row (payload = element .encode ('utf-8' ))))
145+ if topic == 'test-topic' :
146+ kafka_byte_messages = KAFKA_TOPICS ['test-topic' ]
147+ return (
148+ pcoll
149+ | beam .Create ([msg .decode ('utf-8' ) for msg in kafka_byte_messages ])
150+ | beam .Map (lambda element : beam .Row (payload = element .encode ('utf-8' ))))
151+
152+ return None
145153
146154
147155@beam .ptransform .ptransform_fn
@@ -155,17 +163,70 @@ def test_pubsub_read(
155163 attributes_map : Optional [str ] = None ,
156164 id_attribute : Optional [str ] = None ,
157165 timestamp_attribute : Optional [str ] = None ):
166+ """
167+ Mocks the ReadFromPubSub transform for testing purposes.
168+
169+ This PTransform simulates the behavior of the ReadFromPubSub transform by
170+ reading from predefined in-memory data based on the Pub/Sub topic argument.
171+ Args:
172+ pcoll: The input PCollection.
173+ topic: The name of Pub/Sub topic to read from.
174+ subscription: The name of Pub/Sub subscription to read from.
175+ format: The format of the Pub/Sub messages (e.g., 'JSON').
176+ schema: The schema of the Pub/Sub messages.
177+ attributes: A list of attributes to include in the output.
178+ attributes_map: A string representing a mapping of attributes.
179+ id_attribute: The attribute to use as the ID for the message.
180+ timestamp_attribute: The attribute to use as the timestamp for the message.
181+
182+ Returns:
183+ A PCollection containing the sample data.
184+ """
185+
186+ if topic == 'test-topic' :
187+ pubsub_messages = PUBSUB_TOPICS ['test-topic' ]
188+ return (
189+ pcoll
190+ | beam .Create ([json .loads (msg .data ) for msg in pubsub_messages ])
191+ | beam .Map (lambda element : beam .Row (** element )))
192+ elif topic == 'taxi-ride-topic' :
193+ pubsub_messages = PUBSUB_TOPICS ['taxi-ride-topic' ]
194+ schema = input_data .TaxiRideEventSchema
195+ return (
196+ pcoll
197+ | beam .Create ([json .loads (msg .data ) for msg in pubsub_messages ])
198+ |
199+ beam .Map (lambda element : beam .Row (** element )).with_output_types (schema ))
200+
201+ return None
202+
203+
204+ @beam .ptransform .ptransform_fn
205+ def test_run_inference_taxi_fare (pcoll , inference_tag , model_handler ):
206+ """
207+ This PTransform simulates the behavior of the RunInference transform.
208+
209+ Args:
210+ pcoll: The input PCollection.
211+ inference_tag: The tag to use for the returned inference.
212+ model_handler: A configuration for the respective ML model handler
213+
214+ Returns:
215+ A PCollection containing the enriched data.
216+ """
217+ def _fn (row ):
218+ input = row ._asdict ()
158219
159- pubsub_messages = input_data . pubsub_messages_data ()
220+ row = { inference_tag : PredictionResult ( input , 10.0 ), ** input }
160221
161- return (
162- pcoll
163- | beam . Create ([ json . loads ( msg . data ) for msg in pubsub_messages ] )
164- | beam .Map (lambda element : beam . Row ( ** element )) )
222+ return beam . Row ( ** row )
223+
224+ schema = _format_predicition_result_ouput ( pcoll , inference_tag )
225+ return pcoll | beam .Map (_fn ). with_output_types ( schema )
165226
166227
167228@beam .ptransform .ptransform_fn
168- def test_run_inference (pcoll , inference_tag , model_handler ):
229+ def test_run_inference_youtube_comments (pcoll , inference_tag , model_handler ):
169230 """
170231 This PTransform simulates the behavior of the RunInference transform.
171232
@@ -193,24 +254,28 @@ def _fn(row):
193254
194255 return beam .Row (** row )
195256
257+ schema = _format_predicition_result_ouput (pcoll , inference_tag )
258+ return pcoll | beam .Map (_fn ).with_output_types (schema )
259+
260+
261+ def _format_predicition_result_ouput (pcoll , inference_tag ):
196262 user_type = RowTypeConstraint .from_user_type (pcoll .element_type .user_type )
197263 user_schema_fields = [(name , type (typ ) if not isinstance (typ , type ) else typ )
198264 for (name ,
199265 typ ) in user_type ._fields ] if user_type else []
200266 inference_output_type = RowTypeConstraint .from_fields ([
201267 ('example' , Any ), ('inference' , Any ), ('model_id' , Optional [str ])
202268 ])
203- schema = RowTypeConstraint .from_fields (
269+ return RowTypeConstraint .from_fields (
204270 user_schema_fields + [(str (inference_tag ), inference_output_type )])
205271
206- return pcoll | beam .Map (_fn ).with_output_types (schema )
207-
208272
209273TEST_PROVIDERS = {
210274 'TestEnrichment' : test_enrichment ,
211275 'TestReadFromKafka' : test_kafka_read ,
212276 'TestReadFromPubSub' : test_pubsub_read ,
213- 'TestRunInference' : test_run_inference
277+ 'TestRunInferenceYouTubeComments' : test_run_inference_youtube_comments ,
278+ 'TestRunInferenceTaxiFare' : test_run_inference_taxi_fare ,
214279}
215280"""
216281Transforms not requiring inputs.
@@ -305,6 +370,11 @@ def _python_deps_involved(spec_filename):
305370 substr in spec_filename
306371 for substr in ['deps' , 'streaming_sentiment_analysis' ])
307372
373+ def _java_deps_involved (spec_filename ):
374+ return any (
375+ substr in spec_filename
376+ for substr in ['java_deps' , 'streaming_taxifare_prediction' ])
377+
308378 if _python_deps_involved (pipeline_spec_file ):
309379 test_yaml_example = pytest .mark .no_xdist (test_yaml_example )
310380 test_yaml_example = unittest .skipIf (
@@ -319,7 +389,7 @@ def _python_deps_involved(spec_filename):
319389 'Github actions environment issue.' )(
320390 test_yaml_example )
321391
322- if 'java_deps' in pipeline_spec_file :
392+ if _java_deps_involved ( pipeline_spec_file ) :
323393 test_yaml_example = pytest .mark .xlang_sql_expansion_service (
324394 test_yaml_example )
325395 test_yaml_example = unittest .skipIf (
@@ -500,6 +570,7 @@ def _kafka_test_preprocessor(
500570 for transform in pipeline .get ('transforms' , []):
501571 if transform .get ('type' , '' ) == 'ReadFromKafka' :
502572 transform ['type' ] = 'TestReadFromKafka'
573+ transform ['config' ]['topic' ] = 'test-topic'
503574
504575 return test_spec
505576
@@ -525,8 +596,7 @@ def _kafka_test_preprocessor(
525596 'test_oracle_to_bigquery_yaml' ,
526597 'test_mysql_to_bigquery_yaml' ,
527598 'test_spanner_to_bigquery_yaml' ,
528- 'test_streaming_sentiment_analysis_yaml' ,
529- 'test_enrich_spanner_with_bigquery_yaml'
599+ 'test_streaming_sentiment_analysis_yaml'
530600])
531601def _io_write_test_preprocessor (
532602 test_spec : dict , expected : List [str ], env : TestEnvironment ):
@@ -740,6 +810,7 @@ def _pubsub_io_read_test_preprocessor(
740810 for transform in pipeline .get ('transforms' , []):
741811 if transform .get ('type' , '' ) == 'ReadFromPubSub' :
742812 transform ['type' ] = 'TestReadFromPubSub'
813+ transform ['config' ]['topic' ] = 'test-topic'
743814
744815 return test_spec
745816
@@ -904,7 +975,90 @@ def _streaming_sentiment_analysis_test_preprocessor(
904975 if pipeline := test_spec .get ('pipeline' , None ):
905976 for transform in pipeline .get ('transforms' , []):
906977 if transform .get ('type' , '' ) == 'RunInference' :
907- transform ['type' ] = 'TestRunInference'
978+ transform ['type' ] = 'TestRunInferenceYouTubeComments'
979+
980+ return test_spec
981+
982+
983+ @YamlExamplesTestSuite .register_test_preprocessor (
984+ 'test_streaming_taxifare_prediction_yaml' )
985+ def _streaming_taxifare_prediction_test_preprocessor (
986+ test_spec : dict , expected : List [str ], env : TestEnvironment ):
987+ """
988+ Preprocessor for tests that involve the streaming taxi fare prediction
989+ example.
990+
991+ This preprocessor replaces several IO transforms and the RunInference
992+ transform. This allows the test to verify the pipeline's correctness
993+ without relying on external data sources and the model hosted on VertexAI.
994+ It also turns this non-linear pipeline into a linear pipeline by replacing
995+ the ReadFromKafka and WriteToKafka transforms with MapToFields and linking
996+ the two disconnected pipeline components together. The pipeline logic,
997+ however, remains the same and is still being tested accordingly.
998+
999+ Args:
1000+ test_spec: The dictionary representation of the YAML pipeline specification.
1001+ expected: A list of strings representing the expected output of the
1002+ pipeline.
1003+ env: The TestEnvironment object providing utilities for creating temporary
1004+ files.
1005+
1006+ Returns:
1007+ The modified test_spec dictionary with several involved IO transforms and
1008+ the RunInference transform replaced.
1009+ """
1010+
1011+ if pipeline := test_spec .get ('pipeline' , None ):
1012+ for transform in pipeline .get ('transforms' , []):
1013+ if transform .get ('type' , '' ) == 'ReadFromPubSub' :
1014+ transform ['type' ] = 'TestReadFromPubSub'
1015+ transform ['config' ]['topic' ] = 'taxi-ride-topic'
1016+
1017+ elif transform .get ('type' , '' ) == 'WriteToKafka' :
1018+ transform ['type' ] = 'MapToFields'
1019+ transform ['config' ] = {
1020+ k : v
1021+ for (k , v ) in transform .get ('config' , {}).items ()
1022+ if k .startswith ('__' )
1023+ }
1024+ transform ['config' ]['fields' ] = {
1025+ 'ride_id' : 'ride_id' ,
1026+ 'pickup_longitude' : 'pickup_longitude' ,
1027+ 'pickup_latitude' : 'pickup_latitude' ,
1028+ 'pickup_datetime' : 'pickup_datetime' ,
1029+ 'dropoff_longitude' : 'dropoff_longitude' ,
1030+ 'dropoff_latitude' : 'dropoff_latitude' ,
1031+ 'passenger_count' : 'passenger_count' ,
1032+ }
1033+
1034+ elif transform .get ('type' , '' ) == 'ReadFromKafka' :
1035+ transform ['type' ] = 'MapToFields'
1036+ transform ['config' ] = {
1037+ k : v
1038+ for (k , v ) in transform .get ('config' , {}).items ()
1039+ if k .startswith ('__' )
1040+ }
1041+ transform ['input' ] = 'WriteKafka'
1042+ transform ['config' ]['fields' ] = {
1043+ 'ride_id' : 'ride_id' ,
1044+ 'pickup_longitude' : 'pickup_longitude' ,
1045+ 'pickup_latitude' : 'pickup_latitude' ,
1046+ 'pickup_datetime' : 'pickup_datetime' ,
1047+ 'dropoff_longitude' : 'dropoff_longitude' ,
1048+ 'dropoff_latitude' : 'dropoff_latitude' ,
1049+ 'passenger_count' : 'passenger_count' ,
1050+ }
1051+
1052+ elif transform .get ('type' , '' ) == 'WriteToBigQuery' :
1053+ transform ['type' ] = 'LogForTesting'
1054+ transform ['config' ] = {
1055+ k : v
1056+ for (k , v ) in transform .get ('config' , {}).items ()
1057+ if (k .startswith ('__' ) or k == 'error_handling' )
1058+ }
1059+
1060+ elif transform .get ('type' , '' ) == 'RunInference' :
1061+ transform ['type' ] = 'TestRunInferenceTaxiFare'
9081062
9091063 return test_spec
9101064
@@ -915,6 +1069,15 @@ def _streaming_sentiment_analysis_test_preprocessor(
9151069 'youtube-comments.csv' : input_data .youtube_comments_csv ()
9161070}
9171071
1072+ KAFKA_TOPICS = {
1073+ 'test-topic' : input_data .kafka_messages_data (),
1074+ }
1075+
1076+ PUBSUB_TOPICS = {
1077+ 'test-topic' : input_data .pubsub_messages_data (),
1078+ 'taxi-ride-topic' : input_data .pubsub_taxi_ride_events_data ()
1079+ }
1080+
9181081INPUT_TABLES = {
9191082 ('shipment-test' , 'shipment' , 'shipments' ): input_data .shipments_data (),
9201083 ('orders-test' , 'order-database' , 'orders' ): input_data .
0 commit comments