|
| 1 | +# Copyright 2026 Google LLC. All Rights Reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +"""Tests for tfx.components.experimental.filter.executor.""" |
| 15 | + |
| 16 | +import os |
| 17 | +from typing import List |
| 18 | +import tensorflow as tf |
| 19 | +from tfx.components.experimental.filter import executor |
| 20 | + |
| 21 | +from tfx.types import standard_artifacts |
| 22 | +from tfx.types import artifact_utils |
| 23 | + |
| 24 | + |
| 25 | +def dummy_filter_fn(serialized_example: bytes) -> bool: |
| 26 | + """A simple filter function that parses the example and filters by age.""" |
| 27 | + example = tf.train.Example() |
| 28 | + example.ParseFromString(serialized_example) |
| 29 | + features = example.features.feature |
| 30 | + if 'age' in features: |
| 31 | + return features['age'].int64_list.value[0] > 18 |
| 32 | + return False |
| 33 | + |
| 34 | + |
| 35 | +class ExecutorTest(tf.test.TestCase): |
| 36 | + |
| 37 | + def setUp(self): |
| 38 | + super().setUp() |
| 39 | + self._output_data_dir = os.path.join( |
| 40 | + os.environ.get('TEST_UNDECLARED_OUTPUTS_DIR', self.get_temp_dir()), |
| 41 | + self._testMethodName) |
| 42 | + self._input_data_dir = os.path.join(self.get_temp_dir(), 'input') |
| 43 | + |
| 44 | + def _create_test_examples(self, examples_artifact: standard_artifacts.Examples, split_name: str, values: List[int]): |
| 45 | + """Creates a TFRecord file with test examples for the given split.""" |
| 46 | + split_dir = artifact_utils.get_split_uri([examples_artifact], split_name) |
| 47 | + tf.io.gfile.makedirs(split_dir) |
| 48 | + file_path = os.path.join(split_dir, 'data.tfrecord.gz') |
| 49 | + |
| 50 | + options = tf.io.TFRecordOptions(compression_type='GZIP') |
| 51 | + with tf.io.TFRecordWriter(file_path, options=options) as writer: |
| 52 | + for val in values: |
| 53 | + example = tf.train.Example() |
| 54 | + example.features.feature['age'].int64_list.value.append(val) |
| 55 | + writer.write(example.SerializeToString()) |
| 56 | + |
| 57 | + def testExecutor(self): |
| 58 | + # 1. Prepare input and output examples artifacts. |
| 59 | + examples_artifact = standard_artifacts.Examples() |
| 60 | + examples_artifact.uri = self._input_data_dir |
| 61 | + examples_artifact.split_names = artifact_utils.encode_split_names( |
| 62 | + ['train', 'eval']) |
| 63 | + |
| 64 | + # 2. Create input data: |
| 65 | + # train split has ages [10, 20, 30] -> filtered should have [20, 30] |
| 66 | + # eval split has ages [15, 25] -> filtered should have [25] |
| 67 | + self._create_test_examples(examples_artifact, 'train', [10, 20, 30]) |
| 68 | + self._create_test_examples(examples_artifact, 'eval', [15, 25]) |
| 69 | + |
| 70 | + filtered_examples_artifact = standard_artifacts.Examples() |
| 71 | + filtered_examples_artifact.uri = self._output_data_dir |
| 72 | + |
| 73 | + input_dict = {'examples': [examples_artifact]} |
| 74 | + output_dict = {'filtered_examples': [filtered_examples_artifact]} |
| 75 | + |
| 76 | + # Full python import path to the dummy_filter_fn |
| 77 | + filter_fn_path = ( |
| 78 | + 'tfx.components.experimental.filter.executor_test.dummy_filter_fn') |
| 79 | + |
| 80 | + exec_properties = {'filter_fn_path': filter_fn_path} |
| 81 | + |
| 82 | + # 3. Run the executor. |
| 83 | + filter_executor = executor.Executor() |
| 84 | + filter_executor.Do(input_dict, output_dict, exec_properties) |
| 85 | + |
| 86 | + # 4. Verify output splits. |
| 87 | + decoded_splits = artifact_utils.decode_split_names( |
| 88 | + filtered_examples_artifact.split_names) |
| 89 | + self.assertEqual(decoded_splits, ['train', 'eval']) |
| 90 | + |
| 91 | + # 5. Verify the content of the filtered train split. |
| 92 | + train_output_dir = artifact_utils.get_split_uri( |
| 93 | + [filtered_examples_artifact], 'train') |
| 94 | + train_output_files = tf.io.gfile.glob(os.path.join(train_output_dir, '*')) |
| 95 | + self.assertNotEmpty(train_output_files) |
| 96 | + |
| 97 | + train_ages = [] |
| 98 | + # Read the output TFRecords. Beam writes sharded files, so we read all matching files. |
| 99 | + for file_path in train_output_files: |
| 100 | + raw_dataset = tf.data.TFRecordDataset(file_path, compression_type='GZIP') |
| 101 | + for raw_record in raw_dataset: |
| 102 | + example = tf.train.Example() |
| 103 | + example.ParseFromString(raw_record.numpy()) |
| 104 | + train_ages.append(example.features.feature['age'].int64_list.value[0]) |
| 105 | + |
| 106 | + self.assertCountEqual(train_ages, [20, 30]) |
| 107 | + |
| 108 | + # 6. Verify the content of the filtered eval split. |
| 109 | + eval_output_dir = artifact_utils.get_split_uri( |
| 110 | + [filtered_examples_artifact], 'eval') |
| 111 | + eval_output_files = tf.io.gfile.glob(os.path.join(eval_output_dir, '*')) |
| 112 | + self.assertNotEmpty(eval_output_files) |
| 113 | + |
| 114 | + eval_ages = [] |
| 115 | + for file_path in eval_output_files: |
| 116 | + raw_dataset = tf.data.TFRecordDataset(file_path, compression_type='GZIP') |
| 117 | + for raw_record in raw_dataset: |
| 118 | + example = tf.train.Example() |
| 119 | + example.ParseFromString(raw_record.numpy()) |
| 120 | + eval_ages.append(example.features.feature['age'].int64_list.value[0]) |
| 121 | + |
| 122 | + self.assertCountEqual(eval_ages, [25]) |
| 123 | + |
| 124 | + |
| 125 | +if __name__ == '__main__': |
| 126 | + tf.test.main() |
0 commit comments