mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 11:43:26 +08:00
288 lines
10 KiB
Python
288 lines
10 KiB
Python
"""Tests for ``assemble_dc`` pure helpers — independent of disk fixtures.
|
|
|
|
Complements ``test_assemble_dc_gateway_direct.py`` (binding chain) by
|
|
hitting the small, mechanical helpers that bridge raw DC rows and the
|
|
assembled tree:
|
|
|
|
- ``_clean`` NOT_SET sentinel collapse
|
|
- ``_harvest_str`` unescape + quote-strip + UNSET_VALUE
|
|
- ``_ts`` ISO-8601 → datetime; NOT_SET / non-string → None
|
|
- ``_index_unique`` PK dedup with first-write-wins + collision record
|
|
- ``_groupby`` group rows by FK, dropping NOT_SET keys
|
|
- ``_extract_trace_id`` primary col with HTML-escaped fallback
|
|
- ``_tier`` ACTION < TOPIC < GUARDRAIL < other
|
|
- ``_window_contains`` half-open / closed timestamp interval semantics
|
|
- ``_load`` disk read with malformed-json tolerance
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import unittest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
|
|
from . import _bootstrap # noqa: F401 — sys.path setup
|
|
|
|
import assemble_dc # type: ignore
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _clean — NOT_SET → None
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class CleanTests(unittest.TestCase):
|
|
|
|
def test_not_set_token_returns_none(self):
|
|
self.assertIsNone(assemble_dc._clean("NOT_SET"))
|
|
|
|
def test_empty_string_returns_none(self):
|
|
self.assertIsNone(assemble_dc._clean(""))
|
|
|
|
def test_actual_value_passes_through(self):
|
|
self.assertEqual(assemble_dc._clean("real value"), "real value")
|
|
self.assertEqual(assemble_dc._clean(42), 42)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _harvest_str — unescape + quote-strip + UNSET_VALUE collapse
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class HarvestStrTests(unittest.TestCase):
|
|
|
|
def test_none_returns_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str(None))
|
|
|
|
def test_unescapes_html_entities(self):
|
|
self.assertEqual(
|
|
assemble_dc._harvest_str(""0Xx000""),
|
|
"0Xx000",
|
|
)
|
|
|
|
def test_strips_outer_double_quotes(self):
|
|
self.assertEqual(assemble_dc._harvest_str('"value"'), "value")
|
|
|
|
def test_unset_value_collapses_to_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str("UNSET_VALUE"))
|
|
|
|
def test_not_set_collapses_to_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str("NOT_SET"))
|
|
|
|
def test_empty_string_collapses_to_none(self):
|
|
self.assertIsNone(assemble_dc._harvest_str(""))
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _ts — ISO-8601 timestamp parsing
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class TsTests(unittest.TestCase):
|
|
|
|
def test_parses_iso_z_timestamp(self):
|
|
out = assemble_dc._ts("2026-04-22T10:00:00Z")
|
|
self.assertEqual(out, datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc))
|
|
|
|
def test_parses_iso_offset_timestamp(self):
|
|
out = assemble_dc._ts("2026-04-22T10:00:00+00:00")
|
|
self.assertEqual(out, datetime(2026, 4, 22, 10, 0, 0, tzinfo=timezone.utc))
|
|
|
|
def test_returns_none_for_non_string(self):
|
|
self.assertIsNone(assemble_dc._ts(None))
|
|
self.assertIsNone(assemble_dc._ts(12345))
|
|
|
|
def test_returns_none_for_not_set(self):
|
|
self.assertIsNone(assemble_dc._ts("NOT_SET"))
|
|
self.assertIsNone(assemble_dc._ts(""))
|
|
|
|
def test_returns_none_for_unparseable(self):
|
|
self.assertIsNone(assemble_dc._ts("not-a-timestamp"))
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _index_unique
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class IndexUniqueTests(unittest.TestCase):
|
|
|
|
def test_builds_dict_keyed_by_field(self):
|
|
rows = [{"id": "a", "v": 1}, {"id": "b", "v": 2}]
|
|
out = assemble_dc._index_unique(rows, "id", [], dmo_label="t")
|
|
self.assertEqual(set(out.keys()), {"a", "b"})
|
|
|
|
def test_first_write_wins_on_collision(self):
|
|
rows = [{"id": "a", "v": 1}, {"id": "a", "v": 2}]
|
|
collisions: list[dict] = []
|
|
out = assemble_dc._index_unique(rows, "id", collisions, dmo_label="dmo1")
|
|
self.assertEqual(out["a"]["v"], 1)
|
|
self.assertEqual(len(collisions), 1)
|
|
self.assertEqual(collisions[0]["dmo"], "dmo1")
|
|
self.assertEqual(collisions[0]["key"], "a")
|
|
|
|
def test_skips_NOT_SET_keys(self):
|
|
rows = [{"id": "NOT_SET", "v": 1}, {"id": "", "v": 2}, {"id": "a", "v": 3}]
|
|
out = assemble_dc._index_unique(rows, "id", [], dmo_label="t")
|
|
self.assertEqual(set(out.keys()), {"a"})
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _groupby
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class GroupbyTests(unittest.TestCase):
|
|
|
|
def test_groups_rows_by_key(self):
|
|
rows = [{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "a", "v": 3}]
|
|
out = assemble_dc._groupby(rows, "k")
|
|
self.assertEqual([r["v"] for r in out["a"]], [1, 3])
|
|
self.assertEqual([r["v"] for r in out["b"]], [2])
|
|
|
|
def test_skips_NOT_SET_keys(self):
|
|
rows = [{"k": "a", "v": 1}, {"k": "NOT_SET", "v": 2}]
|
|
out = assemble_dc._groupby(rows, "k")
|
|
self.assertNotIn("NOT_SET", out)
|
|
self.assertIn("a", out)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _extract_trace_id
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class ExtractTraceIdTests(unittest.TestCase):
|
|
|
|
def test_uses_primary_column_when_populated(self):
|
|
out = assemble_dc._extract_trace_id({"ssot__TelemetryTraceId__c": "abc"})
|
|
self.assertEqual(out, "abc")
|
|
|
|
def test_falls_back_to_html_escaped_attribute_text(self):
|
|
out = assemble_dc._extract_trace_id({
|
|
"ssot__TelemetryTraceId__c": "",
|
|
"ssot__AttributeText__c": '"internalTraceId":"deadbeef"',
|
|
})
|
|
self.assertEqual(out, "deadbeef")
|
|
|
|
def test_returns_none_when_no_trace_id_anywhere(self):
|
|
out = assemble_dc._extract_trace_id({
|
|
"ssot__TelemetryTraceId__c": "",
|
|
"ssot__AttributeText__c": "no trace here",
|
|
})
|
|
self.assertIsNone(out)
|
|
|
|
def test_returns_none_for_NOT_SET(self):
|
|
out = assemble_dc._extract_trace_id({
|
|
"ssot__TelemetryTraceId__c": "NOT_SET",
|
|
"ssot__AttributeText__c": "",
|
|
})
|
|
self.assertIsNone(out)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _tier — ACTION < TOPIC < GUARDRAIL < other
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class TierTests(unittest.TestCase):
|
|
|
|
def test_action_step_is_lowest_tier(self):
|
|
self.assertLess(
|
|
assemble_dc._tier("ACTION_STEP"),
|
|
assemble_dc._tier("TOPIC_STEP"),
|
|
)
|
|
|
|
def test_topic_step_outranks_guardrails(self):
|
|
self.assertLess(
|
|
assemble_dc._tier("TOPIC_STEP"),
|
|
assemble_dc._tier("TRUST_GUARDRAILS_STEP"),
|
|
)
|
|
|
|
def test_unknown_step_type_falls_to_end(self):
|
|
self.assertEqual(
|
|
assemble_dc._tier("MYSTERY"),
|
|
len(assemble_dc._TIER_ORDER),
|
|
)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _window_contains
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class WindowContainsTests(unittest.TestCase):
|
|
|
|
def _ts(self, h, m, s):
|
|
return datetime(2026, 4, 22, h, m, s, tzinfo=timezone.utc)
|
|
|
|
def test_in_range_inclusive_of_start_and_end(self):
|
|
gw = self._ts(10, 0, 5)
|
|
self.assertTrue(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(10, 0, 10))
|
|
)
|
|
|
|
def test_open_ended_when_end_is_none(self):
|
|
gw = self._ts(11, 0, 0)
|
|
self.assertTrue(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), None)
|
|
)
|
|
|
|
def test_returns_false_when_start_is_none(self):
|
|
gw = self._ts(10, 0, 0)
|
|
self.assertFalse(
|
|
assemble_dc._window_contains(gw, None, self._ts(11, 0, 0))
|
|
)
|
|
|
|
def test_returns_false_when_before_start(self):
|
|
gw = self._ts(9, 0, 0)
|
|
self.assertFalse(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(11, 0, 0))
|
|
)
|
|
|
|
def test_returns_false_when_after_end(self):
|
|
gw = self._ts(12, 0, 0)
|
|
self.assertFalse(
|
|
assemble_dc._window_contains(gw, self._ts(10, 0, 0), self._ts(11, 0, 0))
|
|
)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# _load — disk read with tolerance
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class LoadTests(unittest.TestCase):
|
|
|
|
def test_returns_empty_list_when_file_missing(self):
|
|
with TemporaryDirectory() as t:
|
|
warnings: list[str] = []
|
|
self.assertEqual(
|
|
assemble_dc._load(Path(t), "missing", warnings), []
|
|
)
|
|
# Missing file is normal — no warning recorded.
|
|
self.assertEqual(warnings, [])
|
|
|
|
def test_returns_loaded_rows_when_file_valid(self):
|
|
with TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
(tmp / "dc.sessions.json").write_text(json.dumps([{"a": 1}, {"a": 2}]))
|
|
warnings: list[str] = []
|
|
out = assemble_dc._load(tmp, "sessions", warnings)
|
|
self.assertEqual(out, [{"a": 1}, {"a": 2}])
|
|
self.assertEqual(warnings, [])
|
|
|
|
def test_returns_empty_list_and_records_warning_on_malformed_json(self):
|
|
with TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
(tmp / "dc.broken.json").write_text("<<<not json>>>")
|
|
warnings: list[str] = []
|
|
out = assemble_dc._load(tmp, "broken", warnings)
|
|
self.assertEqual(out, [])
|
|
self.assertEqual(warnings, ["broken"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|