blob: 341979243d876d1b662bc6c53a49641662c6b01b [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Regression tests for connector E2E module sharding."""
import io
import re
import unittest
from collections import Counter
from contextlib import redirect_stdout
from pathlib import Path
from update_modules_check import (
ALL_CONNECTORS_DEDICATED_SHARD_MODULES,
ALL_CONNECTORS_OPTIONAL_DEDICATED_SHARD_MODULES,
ALL_CONNECTORS_REQUIRED_DEDICATED_SHARD_MODULES,
build_sub_it_modules,
get_sub_it_modules,
get_sub_update_it_modules,
modules_to_json,
split_full_connector_it_modules,
)
class ConnectorItShardingTest(unittest.TestCase):
"""Verify connector sharding remains stable as modules change."""
@staticmethod
def parse_modules(modules):
return [module.lstrip(":") for module in modules.split(",") if module]
@staticmethod
def workflow_text():
return (
Path(__file__).resolve().parents[2] / ".github" / "workflows" / "backend.yml"
).read_text(encoding="utf-8")
def test_modules_to_json_preserves_exact_module_tokens(self) -> None:
self.assertEqual(
'["connector-kafka-e2e", "connector-iceberg-e2e"]',
modules_to_json(":connector-kafka-e2e,:connector-iceberg-e2e"),
)
self.assertEqual("[]", modules_to_json(""))
def test_module_outputs_use_json_empty_array(self) -> None:
workflow = self.workflow_text()
self.assertIn(
"ut-modules: ${{ steps.ut-modules.outputs.modules || '[]' }}", workflow
)
self.assertIn(
"it-modules: ${{ steps.it-modules.outputs.modules || '[]' }}", workflow
)
self.assertIn("needs.changes.outputs.ut-modules != '[]'", workflow)
self.assertIn("needs.changes.outputs.it-modules != '[]'", workflow)
def test_every_module_is_assigned_once(self) -> None:
modules = ["connector-a-e2e", "connector-b-e2e", "connector-c-e2e"]
shards = split_full_connector_it_modules(modules, 7)
assigned_modules = [module for shard in shards for module in shard]
self.assertCountEqual(modules, assigned_modules)
self.assertEqual(len(modules), len(assigned_modules))
def test_unknown_module_does_not_reshuffle_existing_modules(self) -> None:
modules = ["connector-a-e2e", "connector-b-e2e", "connector-c-e2e"]
original_shards = split_full_connector_it_modules(modules, 7)
shards_with_new_module = split_full_connector_it_modules(
modules + ["new-connector-e2e"], 7
)
for original_shard, new_shard in zip(original_shards, shards_with_new_module):
self.assertEqual(
original_shard,
[module for module in new_shard if module != "new-connector-e2e"],
)
def test_sharding_is_independent_of_module_order(self) -> None:
modules = ["connector-a-e2e", "connector-b-e2e", "connector-c-e2e"]
self.assertEqual(
split_full_connector_it_modules(modules, 7),
split_full_connector_it_modules(list(reversed(modules)), 7),
)
def test_historical_seed_assignments_are_preserved(self) -> None:
modules = [
"connector-file-hadoop-e2e",
"connector-cdc-mongodb-e2e",
"connector-clickhouse-e2e",
"connector-typesense-e2e",
"connector-file-ftp-e2e",
"connector-databend-e2e",
"connector-http-e2e",
]
self.assertEqual(
split_full_connector_it_modules(modules, 7),
[
["connector-file-hadoop-e2e"],
["connector-cdc-mongodb-e2e"],
["connector-clickhouse-e2e"],
["connector-typesense-e2e"],
["connector-file-ftp-e2e"],
["connector-databend-e2e"],
["connector-http-e2e"],
],
)
def test_full_and_updated_paths_apply_their_ownership_rules(self) -> None:
self.assertIn(
"connector-google-pubsub-e2e",
ALL_CONNECTORS_REQUIRED_DEDICATED_SHARD_MODULES,
)
connector_modules = [
"connector-normal-e2e",
*ALL_CONNECTORS_REQUIRED_DEDICATED_SHARD_MODULES,
*ALL_CONNECTORS_OPTIONAL_DEDICATED_SHARD_MODULES,
"connector-iceberg-hadoop3-e2e",
"connector-iceberg-s3-e2e",
]
updated_modules = connector_modules + [
"seatunnel-engine-k8s-e2e",
]
full_output = io.StringIO()
with redirect_stdout(full_output):
get_sub_it_modules("," + ",".join(connector_modules), 1, 0)
self.assertEqual(
full_output.getvalue(),
":connector-iceberg-hadoop3-e2e,"
":connector-iceberg-s3-e2e,"
":connector-normal-e2e\n",
)
updated_output = io.StringIO()
with redirect_stdout(updated_output):
get_sub_update_it_modules(
modules_to_json(":" + ",:".join(updated_modules)), 1, 0
)
self.assertEqual(
updated_output.getvalue(),
":connector-normal-e2e,"
":connector-jdbc-e2e,"
":connector-iceberg-hadoop3-e2e,"
":connector-iceberg-s3-e2e\n",
)
def test_regular_shards_keep_only_remaining_modules_once(self) -> None:
expected_modules = {
"connector-assert-e2e",
"connector-cdc-sqlserver-e2e",
"connector-http-e2e",
}
modules = ",".join(
["", *sorted(expected_modules), *ALL_CONNECTORS_DEDICATED_SHARD_MODULES]
)
shard_outputs = [build_sub_it_modules(modules, 7, shard) for shard in range(7)]
shard_modules = [self.parse_modules(output) for output in shard_outputs]
combined_counter = Counter(
module for output_modules in shard_modules for module in output_modules
)
self.assertEqual(expected_modules, set(combined_counter))
self.assertEqual(Counter(expected_modules), combined_counter)
self.assertTrue(
set(ALL_CONNECTORS_DEDICATED_SHARD_MODULES).isdisjoint(set(combined_counter))
)
def test_regular_shards_fail_fast_when_dedicated_modules_disappear(self) -> None:
modules = ",".join(
[
"",
"connector-assert-e2e",
*[
module
for module in ALL_CONNECTORS_REQUIRED_DEDICATED_SHARD_MODULES
if module != "connector-elasticsearch-e2e"
],
]
)
with self.assertRaisesRegex(ValueError, "connector-elasticsearch-e2e"):
build_sub_it_modules(modules, 7, 0)
def test_regular_shards_allow_optional_dedicated_modules_to_be_absent(
self,
) -> None:
modules = ",".join(
["", "connector-assert-e2e", *ALL_CONNECTORS_REQUIRED_DEDICATED_SHARD_MODULES]
)
shard_outputs = [build_sub_it_modules(modules, 7, shard) for shard in range(7)]
combined_modules = {
module
for output in shard_outputs
for module in self.parse_modules(output)
}
self.assertEqual({"connector-assert-e2e"}, combined_modules)
def test_workflow_keeps_dedicated_jobs_for_excluded_modules(self) -> None:
workflow_modules = set()
for modules in re.findall(
r"-pl\s+(:[A-Za-z0-9._-]+(?:,:[A-Za-z0-9._-]+)*)",
self.workflow_text(),
):
workflow_modules.update(
module.lstrip(":") for module in modules.split(",") if module
)
expected_workflow_modules = set(ALL_CONNECTORS_DEDICATED_SHARD_MODULES)
expected_workflow_modules.remove("connector-jdbc-e2e")
expected_workflow_modules.update(
{
"connector-jdbc-e2e-part-1",
"connector-jdbc-e2e-part-2",
"connector-jdbc-e2e-part-3",
"connector-jdbc-e2e-part-4",
"connector-jdbc-e2e-part-5",
"connector-jdbc-e2e-part-6",
"connector-jdbc-e2e-part-7",
"connector-jdbc-e2e-ddl",
}
)
self.assertFalse(
expected_workflow_modules - workflow_modules,
f"Missing dedicated workflow modules: {sorted(expected_workflow_modules - workflow_modules)}",
)
def test_dedicated_job_conditions_match_json_module_tokens(self) -> None:
workflow = self.workflow_text()
condition_modules = set(ALL_CONNECTORS_REQUIRED_DEDICATED_SHARD_MODULES) - {
"connector-jdbc-e2e"
}
condition_modules.update(
{"seatunnel-edge-agent-e2e", "seatunnel-engine-k8s-e2e"}
)
for module in sorted(condition_modules):
with self.subTest(module=module):
self.assertIn(
"contains(fromJSON(needs.changes.outputs.it-modules), "
f"'{module}')",
workflow,
)
def test_full_shard_rejects_non_positive_shard_count(self) -> None:
with self.assertRaisesRegex(
ValueError, "total shard count must be positive, got 0"
):
build_sub_it_modules("connector-normal-e2e", 0, 0)
def test_full_shard_rejects_out_of_range_index(self) -> None:
for current_num in (-1, 7):
with self.subTest(current_num=current_num):
with self.assertRaisesRegex(
ValueError,
f"shard index {current_num} out of range \\[0, 7\\)",
):
build_sub_it_modules("connector-normal-e2e", 7, current_num)
if __name__ == "__main__":
unittest.main()