Files
wehub-resource-sync bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:35:45 +08:00

399 lines
16 KiB
Python

# Copyright 2025 Collate
# Licensed under the Collate Community License, Version 1.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# https://github.com/open-metadata/OpenMetadata/blob/main/ingestion/LICENSE
# 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.
"""Unit tests for the hierarchical (tree) ProgressRegistry."""
import threading
from unittest.mock import patch
from metadata.ingestion.progress.registry import ProgressRegistry
def _by_label(snapshot):
return {child.label: child for child in snapshot.children}
class TestTreeProgress:
def test_empty_registry_snapshots_none(self):
assert ProgressRegistry().snapshot() is None
def test_root_counts_databases_as_bare_when_expected_unknown(self):
reg = ProgressRegistry()
reg.open([], "Database", None)
snap = reg.snapshot()
assert snap.child_type == "Database"
assert snap.expected is None
assert snap.processed == 0
assert snap.active is True
def test_leaf_advance_increments_processed_against_expected(self):
reg = ProgressRegistry()
reg.open(["xyz"], "DatabaseSchema", 10)
reg.open(["xyz", "abc"], "Table", 100)
for _ in range(20):
reg.advance(["xyz", "abc"])
abc = _by_label(_by_label(reg.snapshot())["xyz"])["abc"]
assert abc.processed == 20
assert abc.expected == 100
assert abc.active is True
def test_container_completion_is_derived_from_children(self):
reg = ProgressRegistry()
reg.open([], "Database", 1)
reg.open(["xyz"], "DatabaseSchema", 2)
reg.open(["xyz", "abc"], "Table", 1)
reg.open(["xyz", "def"], "Table", 1)
reg.advance(["xyz", "abc"]) # only one of two schemas complete
xyz = _by_label(reg.snapshot())["xyz"]
assert xyz.processed == 1
assert xyz.expected == 2
assert xyz.active is True
def test_completed_subtree_is_pruned_and_root_completes(self):
reg = ProgressRegistry()
reg.open([], "Database", 1)
reg.open(["xyz"], "DatabaseSchema", 1)
reg.open(["xyz", "abc"], "Table", 1)
reg.advance(["xyz", "abc"]) # schema done -> db done -> root done
snap = reg.snapshot()
assert snap.processed == 1
assert snap.expected == 1
assert snap.active is False
assert snap.children == () # completed db pruned
def test_empty_schema_auto_completes_and_vanishes(self):
reg = ProgressRegistry()
reg.open(["xyz"], "DatabaseSchema", 2)
reg.open(["xyz", "empty"], "Table", 0) # zero-table schema -> complete
reg.open(["xyz", "live"], "Table", 5) # in-flight keeps xyz visible
reg.advance(["xyz", "live"])
xyz = _by_label(reg.snapshot())["xyz"]
schemas = _by_label(xyz)
assert "empty" not in schemas # completed empty schema pruned
assert "live" in schemas # active schema still shown
assert xyz.processed == 1 # empty counted as one complete child
def test_unknown_expected_advance_renders_bare_count(self):
reg = ProgressRegistry()
reg.advance(["topic"]) # advanced without a prior open
reg.advance(["topic"])
node = _by_label(reg.snapshot())["topic"]
assert node.processed == 2
assert node.expected is None
assert node.active is True
def test_over_count_is_pruned_and_no_visible_node_exceeds_expected(self):
reg = ProgressRegistry()
reg.open(["xyz"], "DatabaseSchema", 2)
reg.open(["xyz", "abc"], "Table", 2)
reg.open(["xyz", "live"], "Table", 9)
for _ in range(5): # 5 advances against expected 2 -> over-counted
reg.advance(["xyz", "abc"])
reg.advance(["xyz", "live"])
xyz = _by_label(reg.snapshot())["xyz"]
assert "abc" not in _by_label(xyz) # over-counted -> complete -> pruned
assert xyz.processed == 1 # abc counted as one complete child
def _no_overrun(node):
assert node.expected is None or node.processed <= node.expected
for child in node.children:
_no_overrun(child)
_no_overrun(reg.snapshot())
def test_multi_leaf_type_schema_sums_expected(self):
reg = ProgressRegistry()
reg.open(["xyz", "abc"], "Table", 100)
reg.open(["xyz", "abc"], "StoredProcedure", 5)
for _ in range(7):
reg.advance(["xyz", "abc"])
abc = _by_label(_by_label(reg.snapshot())["xyz"])["abc"]
assert abc.expected == 105
assert abc.processed == 7
assert abc.child_type == "Table" # first opener is the label
def test_active_leaf_cap_overflows(self):
reg = ProgressRegistry(active_leaf_cap=2)
reg.open(["db"], "DatabaseSchema", 50)
for i in range(5):
reg.open(["db", f"s{i}"], "Table", 10)
reg.advance(["db", f"s{i}"]) # in-flight, not complete
db = _by_label(reg.snapshot())["db"]
assert len(db.children) == 2
assert db.overflow == 3
def test_snapshot_is_detached(self):
reg = ProgressRegistry()
reg.open(["xyz", "abc"], "Table", 100)
reg.advance(["xyz", "abc"])
snap = reg.snapshot()
reg.advance(["xyz", "abc"])
abc = _by_label(_by_label(snap)["xyz"])["abc"]
assert abc.processed == 1 # frozen at snapshot time
def test_real_thread_concurrency_converges(self):
reg = ProgressRegistry()
reg.open([], "Database", 1)
reg.open(["db"], "DatabaseSchema", 8)
for i in range(8):
reg.open(["db", f"s{i}"], "Table", 10)
def work(schema):
for _ in range(10):
reg.advance(["db", schema])
threads = [threading.Thread(target=work, args=(f"s{i}",)) for i in range(8)]
for t in threads:
t.start()
for t in threads:
t.join()
snap = reg.snapshot()
assert snap.processed == 1 # the one database completed
assert snap.active is False
class TestRegistryPrimitives:
def test_advance_tracks_processed_per_child_type(self):
reg = ProgressRegistry()
reg.open(["db", "s1"], "Table", 10)
reg.open(["db", "s1"], "StoredProcedure", 4)
for _ in range(3):
reg.advance(["db", "s1"], "Table")
reg.advance(["db", "s1"], "StoredProcedure")
s1 = _by_label(_by_label(reg.snapshot())["db"])["s1"]
assert s1.processed_by_type == {"Table": 3, "StoredProcedure": 1}
assert s1.expected_by_type == {"Table": 10, "StoredProcedure": 4}
assert s1.processed == 4 # combined, unchanged generic semantics
def test_advance_without_child_type_uses_node_child_type(self):
reg = ProgressRegistry()
reg.open(["db", "s1"], "Table", 5)
reg.advance(["db", "s1"])
s1 = _by_label(_by_label(reg.snapshot())["db"])["s1"]
assert s1.processed_by_type == {"Table": 1}
def test_completed_at_depth_counts_complete_nodes_including_pruned(self):
reg = ProgressRegistry()
reg.open([], "Database", 2)
for db in ("d1", "d2"):
reg.open([db], "DatabaseSchema", 1)
# d1.s1 complete (1/1), d2.s1 in-flight (0/1)
reg.open(["d1", "s1"], "Table", 1)
reg.advance(["d1", "s1"], "Table")
reg.open(["d2", "s1"], "Table", 3)
reg.advance(["d2", "s1"], "Table")
assert reg.completed_at_depth(1) == 1 # d1 complete, d2 not
assert reg.completed_at_depth(2) == 1 # d1.s1 complete, d2.s1 not
def test_snapshot_cap_override_widens_active_children(self):
reg = ProgressRegistry(active_leaf_cap=1)
reg.open(["db"], "DatabaseSchema", 9)
for i in range(3):
reg.open(["db", f"s{i}"], "Table", 5)
reg.advance(["db", f"s{i}"], "Table")
db_default = _by_label(reg.snapshot())["db"]
db_wide = _by_label(reg.snapshot(active_leaf_cap=10))["db"]
assert len(db_default.children) == 1 # capped
assert len(db_wide.children) == 3 # override lifts the cap
def test_completed_snapshots_at_depth_returns_finished_nodes_with_detail(self):
reg = ProgressRegistry()
reg.open([], "Database", 1)
reg.open(["d1"], "DatabaseSchema", 2)
reg.open(["d1", "orders"], "Table", 2)
reg.open(["d1", "orders"], "StoredProcedure", 1)
reg.advance(["d1", "orders"], "Table")
reg.advance(["d1", "orders"], "Table")
reg.advance(["d1", "orders"], "StoredProcedure")
reg.open(["d1", "public"], "Table", 5) # still in-flight
reg.advance(["d1", "public"], "Table")
completed = reg.completed_snapshots_at_depth(2)
assert len(completed) == 1 # only the finished schema
ancestors, snap = completed[0]
assert ancestors == ("d1",)
assert snap.label == "orders"
assert snap.active is False
assert snap.processed_by_type == {"Table": 2, "StoredProcedure": 1}
assert snap.expected_by_type == {"Table": 2, "StoredProcedure": 1}
def test_completed_snapshots_at_depth_caps_to_last_limit(self):
reg = ProgressRegistry()
for i in range(5):
reg.open(["d1", f"s{i}"], "Table", 1)
reg.advance(["d1", f"s{i}"], "Table") # all complete
completed = reg.completed_snapshots_at_depth(2, limit=2)
assert [snap.label for _, snap in completed] == ["s3", "s4"] # last two
class TestOpenKeepsKnownExpected:
def test_lazy_open_does_not_clobber_known_expected(self):
registry = ProgressRegistry()
registry.open(["db", "s"], "Table", 5)
registry.open(["db", "s"], "Table", None)
snapshot = registry.snapshot()
schema = snapshot.children[0].children[0]
assert schema.expected == 5
class TestAssetCounterAndClose:
def test_advance_increments_assets_ingested(self):
registry = ProgressRegistry()
registry.advance(["db", "s"], "Table")
registry.advance(["db", "s"], "Table")
assert registry.assets_ingested() == 2
def test_assets_ingested_is_monotonic_across_close(self):
registry = ProgressRegistry()
registry.advance(["db", "s1"], "Table")
registry.advance(["db", "s1"], "Table")
registry.close(["db", "s1"])
assert registry.assets_ingested() == 2 # survives the prune
registry.advance(["db", "s2"], "Table")
assert registry.assets_ingested() == 3
def test_close_removes_node_from_parent(self):
registry = ProgressRegistry()
registry.open(["db", "s1"], "Table", 5)
registry.advance(["db", "s1"], "Table")
registry.open(["db", "s2"], "Table", 5)
registry.advance(["db", "s2"], "Table")
registry.close(["db", "s1"])
snapshot = registry.snapshot()
database = snapshot.children[0]
labels = {child.label for child in database.children}
assert "s1" not in labels
assert "s2" in labels
def test_close_absent_or_empty_path_is_noop(self):
registry = ProgressRegistry()
registry.close([]) # empty: no-op
registry.close(["ghost"]) # absent: no-op
assert registry.snapshot() is None
class TestGlobalCounters:
def test_global_counters_empty_by_default(self):
reg = ProgressRegistry()
assert reg.global_counters() == []
def test_set_total_then_track(self):
reg = ProgressRegistry()
reg.set_total("Database", 4)
assert reg.global_counters() == [("Database", 0, 4)]
reg.track("Database")
reg.track("Database")
assert reg.global_counters() == [("Database", 2, 4)]
def test_set_total_with_unknown_total(self):
reg = ProgressRegistry()
reg.set_total("Workspaces", None)
reg.track("Workspaces")
assert reg.global_counters() == [("Workspaces", 1, None)]
def test_track_unknown_type_is_noop(self):
reg = ProgressRegistry()
reg.track("Nope")
assert reg.global_counters() == []
def test_track_does_not_touch_asset_counter(self):
reg = ProgressRegistry()
reg.advance([], "Table")
reg.set_total("Database", 3)
reg.track("Database")
assert reg.assets_ingested() == 1
assert reg.global_counters() == [("Database", 1, 3)]
def test_seed_scope_total_sums_and_is_reconcilable(self):
reg = ProgressRegistry()
reg.seed_scope_total("DatabaseSchema", "db1", 10)
reg.seed_scope_total("DatabaseSchema", "db2", 35)
assert reg.global_counters() == [("DatabaseSchema", 0, 45)]
assert reg.is_reconcilable("DatabaseSchema") is True
assert reg.is_reconcilable("Database") is False
def test_reconcile_scope_total_applies_delta(self):
reg = ProgressRegistry()
reg.seed_scope_total("DatabaseSchema", "db1", 10)
reg.seed_scope_total("DatabaseSchema", "db2", 35)
reg.reconcile_scope_total("DatabaseSchema", "db1", 12)
assert reg.global_counters() == [("DatabaseSchema", 0, 47)]
def test_reconcile_unknown_type_is_noop(self):
reg = ProgressRegistry()
reg.reconcile_scope_total("DatabaseSchema", "db1", 5)
assert reg.global_counters() == []
def test_total_never_below_done(self):
reg = ProgressRegistry()
reg.seed_scope_total("DatabaseSchema", "db1", 1)
reg.track("DatabaseSchema")
reg.track("DatabaseSchema")
assert reg.global_counters() == [("DatabaseSchema", 2, 2)]
def test_set_reconcilable_creates_counter(self):
reg = ProgressRegistry()
reg.set_reconcilable("DatabaseSchema")
assert reg.is_reconcilable("DatabaseSchema") is True
reg.reconcile_scope_total("DatabaseSchema", "db1", 7)
assert reg.global_counters() == [("DatabaseSchema", 0, 7)]
class TestElapsedSeconds:
def test_none_before_first_open(self):
reg = ProgressRegistry()
assert reg.elapsed_seconds() is None
def test_positive_after_first_open(self):
reg = ProgressRegistry()
with patch("metadata.ingestion.progress.registry.time.monotonic") as clock:
clock.return_value = 100.0
reg.open([], "Database", None)
clock.return_value = 130.0
assert reg.elapsed_seconds() == 30.0
def test_marker_set_on_first_open_only(self):
reg = ProgressRegistry()
with patch("metadata.ingestion.progress.registry.time.monotonic") as clock:
clock.return_value = 100.0
reg.open([], "Database", None)
clock.return_value = 200.0
reg.open(["db"], "DatabaseSchema", None)
clock.return_value = 250.0
assert reg.elapsed_seconds() == 150.0
class TestGlobalCounterClockAndBulk:
def test_track_accepts_bulk_count(self):
registry = ProgressRegistry()
registry.set_total("Queries", 100)
registry.track("Queries", 40)
registry.track("Queries", 2)
assert registry.global_counters() == [("Queries", 42, 100)]
def test_track_defaults_to_one(self):
registry = ProgressRegistry()
registry.set_total("Workspace", None)
registry.track("Workspace")
assert registry.global_counters() == [("Workspace", 1, None)]
def test_clock_starts_on_counter_activity_without_open(self):
registry = ProgressRegistry()
assert registry.elapsed_seconds() is None
registry.seed_scope_total("Queries", "run", 50)
assert registry.elapsed_seconds() is not None
def test_seed_then_reconcile_settles_total_to_done(self):
registry = ProgressRegistry()
registry.seed_scope_total("Queries", "run", 100)
registry.track("Queries", 40)
registry.reconcile_scope_total("Queries", "run", 40)
assert registry.global_counters() == [("Queries", 40, 40)]