From ae8b3ff57dbd754ec150dcd51a9fca0b293c5791 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 28 Feb 2026 21:38:08 -0500 Subject: [PATCH 1/4] =?UTF-8?q?ncdb:=20UCIS=20compliance=20=E2=80=94=20pha?= =?UTF-8?q?ses=201-5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Scope goal and source type - Add PRESENCE_GOAL and PRESENCE_SOURCE_TYPE to scope_tree.bin - Scope goal (coverage percentage target) now round-trips - Source type (SourceT enum) now round-trips Phase 2: Per-coveritem flags - New coveritem_flags.bin ZIP member for sparse flag storage - Delta-encoded (coveritem_index, flags) pairs - Supports exclusion flags (EXCLUDE_PRAGMA/FILE/INST/AUTO) and type-qualified flags (IS_BR_ELSE, IS_FSM_RESET, etc.) Phase 3: Attribute system redesign - attrs.bin v2 format with sections for scopes, coveritems, history nodes, and global (DB-level) attributes - MemCoverIndex gains setAttribute/getAttribute/getAttributes - MemHistoryNode gains setAttribute/getAttribute/getAttributes - Backward compatible: v1 JSON still readable Phase 4-5: Properties and tags (already working) - EXPR_TERMS, DU_SIGNATURE round-trip via existing properties.json - History hierarchy and scope tags already supported Test: 14 new compliance tests in test_ucis_compliance.py Full suite: 1002 passed, 0 failed --- src/ucis/mem/mem_cover_index.py | 20 +- src/ucis/mem/mem_history_node.py | 20 +- src/ucis/ncdb/_accel/_ncdb_accel.o | Bin 38008 -> 33640 bytes src/ucis/ncdb/attrs.py | 129 +++++++++++- src/ucis/ncdb/constants.py | 3 + src/ucis/ncdb/coveritem_flags.py | 86 ++++++++ src/ucis/ncdb/ncdb_reader.py | 10 +- src/ucis/ncdb/ncdb_writer.py | 8 +- src/ucis/ncdb/scope_tree.py | 24 +++ src/ucis/sqlite/sqlite_scope.py | 46 ++++- tests/unit/ncdb/test_attrs.py | 10 +- tests/unit/ncdb/test_ucis_compliance.py | 255 ++++++++++++++++++++++++ 12 files changed, 578 insertions(+), 33 deletions(-) create mode 100644 src/ucis/ncdb/coveritem_flags.py create mode 100644 tests/unit/ncdb/test_ucis_compliance.py diff --git a/src/ucis/mem/mem_cover_index.py b/src/ucis/mem/mem_cover_index.py index eeb9baf..6e957e9 100644 --- a/src/ucis/mem/mem_cover_index.py +++ b/src/ucis/mem/mem_cover_index.py @@ -42,5 +42,21 @@ def setCoverFlags(self, flags: int): """Set cover flags.""" if self.data: self.data.flags = flags - - \ No newline at end of file + + def setAttribute(self, key: str, value: str): + """Set a user-defined attribute on this coveritem.""" + if not hasattr(self, '_attributes'): + self._attributes = {} + self._attributes[key] = value + + def getAttribute(self, key: str): + """Get a user-defined attribute by key.""" + if not hasattr(self, '_attributes'): + return None + return self._attributes.get(key) + + def getAttributes(self): + """Get all user-defined attributes as a dict.""" + if not hasattr(self, '_attributes'): + return {} + return dict(self._attributes) diff --git a/src/ucis/mem/mem_history_node.py b/src/ucis/mem/mem_history_node.py index bb2452a..316b242 100644 --- a/src/ucis/mem/mem_history_node.py +++ b/src/ucis/mem/mem_history_node.py @@ -253,5 +253,21 @@ def setStringProperty(self, coverindex: int, property, value: str): } if property in _map: setattr(self, _map[property], value) - - \ No newline at end of file + + def setAttribute(self, key: str, value: str): + """Set a user-defined attribute on this history node.""" + if not hasattr(self, '_attributes'): + self._attributes = {} + self._attributes[key] = value + + def getAttribute(self, key: str): + """Get a user-defined attribute by key.""" + if not hasattr(self, '_attributes'): + return None + return self._attributes.get(key) + + def getAttributes(self): + """Get all user-defined attributes as a dict.""" + if not hasattr(self, '_attributes'): + return {} + return dict(self._attributes) diff --git a/src/ucis/ncdb/_accel/_ncdb_accel.o b/src/ucis/ncdb/_accel/_ncdb_accel.o index 51ffa7e1c679dc315b30fe007e06d7595e2d95be..301c7e518b1bdd4cc19f9347664974ce92fe7268 100644 GIT binary patch literal 33640 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 Hc=`VW?-qxx literal 38008 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 diff --git a/src/ucis/ncdb/attrs.py b/src/ucis/ncdb/attrs.py index bb50238..1257b6d 100644 --- a/src/ucis/ncdb/attrs.py +++ b/src/ucis/ncdb/attrs.py @@ -1,44 +1,100 @@ """ -attrs.json — user-defined attribute serialization. +attrs.bin — user-defined attribute serialization. -Format: JSON object +Format v1 (legacy): JSON object {"version": 1, "entries": [{"idx": , "attrs": {: }}, ...]} -Only scopes that have at least one attribute are included (sparse). +Format v2 (current): JSON object with sections for scopes, coveritems, +history nodes, and global attrs. + {"version": 2, + "scopes": [{"idx": , "attrs": {: }}, ...], + "coveritems": [{"scope_idx": , "ci_idx": , "attrs": {...}}, ...], + "history": [{"idx": , "attrs": {...}}, ...], + "global": {: }} """ import json from .dfs_util import dfs_scope_list +from ucis.history_node_kind import HistoryNodeKind -_VERSION = 1 +_VERSION = 2 +_COVER_ALL = 0xFFFFFFFF class AttrsWriter: - """Serialize user-defined scope attributes to attrs.json bytes.""" + """Serialize user-defined attributes to attrs.bin bytes.""" def serialize(self, db) -> bytes: scopes = dfs_scope_list(db) - entries = [] + scope_entries = [] for idx, scope in enumerate(scopes): if not hasattr(scope, 'getAttributes'): continue attrs = scope.getAttributes() if attrs: - entries.append({"idx": idx, "attrs": attrs}) - payload = {"version": _VERSION, "entries": entries} + scope_entries.append({"idx": idx, "attrs": attrs}) + + ci_entries = [] + for idx, scope in enumerate(scopes): + try: + items = list(scope.coverItems(_COVER_ALL)) + except Exception: + continue + for ci_idx, ci in enumerate(items): + if not hasattr(ci, 'getAttributes'): + continue + attrs = ci.getAttributes() + if attrs: + ci_entries.append({ + "scope_idx": idx, "ci_idx": ci_idx, "attrs": attrs + }) + + hist_entries = [] + for kind in (HistoryNodeKind.TEST, HistoryNodeKind.MERGE): + try: + nodes = list(db.historyNodes(kind)) + except Exception: + continue + for hi, node in enumerate(nodes): + if not hasattr(node, 'getAttributes'): + continue + attrs = node.getAttributes() + if attrs: + hist_entries.append({ + "idx": hi, "kind": kind.name, "attrs": attrs + }) + + global_attrs = {} + if hasattr(db, 'getAttributes'): + global_attrs = db.getAttributes() + + payload = { + "version": _VERSION, + "scopes": scope_entries, + "coveritems": ci_entries, + "history": hist_entries, + "global": global_attrs, + } return json.dumps(payload, separators=(',', ':')).encode() class AttrsReader: - """Deserialize attrs.json bytes and apply attributes to scope tree.""" + """Deserialize attrs.bin bytes and apply attributes.""" def deserialize(self, data: bytes, db) -> None: if not data: return payload = json.loads(data.decode()) - if payload.get("version") != _VERSION: - raise ValueError(f"Unsupported attrs.json version: {payload.get('version')}") + version = payload.get("version", 1) + + if version == 1: + self._deserialize_v1(payload, db) + elif version == 2: + self._deserialize_v2(payload, db) + + def _deserialize_v1(self, payload, db): + """Legacy v1: scope attrs only.""" entries = payload.get("entries", []) if not entries: return @@ -50,3 +106,54 @@ def deserialize(self, data: bytes, db) -> None: for key, val in entry.get("attrs", {}).items(): if hasattr(scope, 'setAttribute'): scope.setAttribute(key, val) + + def _deserialize_v2(self, payload, db): + """V2: scopes + coveritems + history + global.""" + scopes = dfs_scope_list(db) + + for entry in payload.get("scopes", []): + idx = entry["idx"] + if idx < len(scopes): + scope = scopes[idx] + for key, val in entry.get("attrs", {}).items(): + if hasattr(scope, 'setAttribute'): + scope.setAttribute(key, val) + + for entry in payload.get("coveritems", []): + scope_idx = entry["scope_idx"] + ci_idx = entry["ci_idx"] + if scope_idx < len(scopes): + scope = scopes[scope_idx] + try: + items = list(scope.coverItems(_COVER_ALL)) + if ci_idx < len(items): + ci = items[ci_idx] + for key, val in entry.get("attrs", {}).items(): + if hasattr(ci, 'setAttribute'): + ci.setAttribute(key, val) + except Exception: + pass + + hist_nodes = {} + for kind in (HistoryNodeKind.TEST, HistoryNodeKind.MERGE): + try: + hist_nodes[kind.name] = list(db.historyNodes(kind)) + except Exception: + pass + for entry in payload.get("history", []): + kind_name = entry.get("kind", "TEST") + idx = entry["idx"] + nodes = hist_nodes.get(kind_name, []) + if idx < len(nodes): + node = nodes[idx] + for key, val in entry.get("attrs", {}).items(): + if hasattr(node, 'setAttribute'): + node.setAttribute(key, val) + + for key, val in payload.get("global", {}).items(): + if hasattr(db, 'setAttribute'): + db.setAttribute(key, val) + + def apply(self, db, data: bytes) -> None: + """Alias for deserialize (matches other readers' API).""" + self.deserialize(data, db) diff --git a/src/ucis/ncdb/constants.py b/src/ucis/ncdb/constants.py index db67fee..faf0fb3 100644 --- a/src/ucis/ncdb/constants.py +++ b/src/ucis/ncdb/constants.py @@ -49,6 +49,8 @@ PRESENCE_WEIGHT = 0x04 # has non-default weight (≠1) PRESENCE_AT_LEAST = 0x08 # coveritem at_least override at scope level PRESENCE_CVG_OPTS = 0x10 # has covergroup options +PRESENCE_GOAL = 0x20 # has non-default scope goal (≠-1) +PRESENCE_SOURCE_TYPE = 0x40 # has explicit SourceT enum # ── counts.bin encoding modes ───────────────────────────────────────────── @@ -104,3 +106,4 @@ ScopeTypeT.COVER: CoverTypeT.COVERBIN, ScopeTypeT.ASSERT: CoverTypeT.ASSERTBIN, } +MEMBER_COVERITEM_FLAGS = "coveritem_flags.bin" diff --git a/src/ucis/ncdb/coveritem_flags.py b/src/ucis/ncdb/coveritem_flags.py new file mode 100644 index 0000000..1995c1a --- /dev/null +++ b/src/ucis/ncdb/coveritem_flags.py @@ -0,0 +1,86 @@ +""" +coveritem_flags.bin — per-coveritem flag serialization. + +Stores non-zero coveritem flags (exclusion, type-qualified) as sparse +delta-encoded (coveritem_dfs_index, flags) pairs. + +Format: + version: varint (1) + num_entries: varint + per entry: + delta_idx: varint (coveritem DFS index delta from previous) + flags: varint (ucisFlagsT value) +""" + +from ucis.cover_type_t import CoverTypeT +from ucis.scope_type_t import ScopeTypeT + +from .varint import encode_varint, decode_varint +from .dfs_util import dfs_scope_list + +_VERSION = 1 +_COVER_ALL = 0xFFFFFFFF + + +class CoveritemFlagsWriter: + """Serialize non-zero coveritem flags to binary bytes.""" + + def serialize(self, db) -> bytes: + entries = [] + global_ci_idx = 0 + scopes = dfs_scope_list(db) + for scope in scopes: + for ci in scope.coverItems(_COVER_ALL): + flags = ci.getCoverFlags() + if flags != 0: + entries.append((global_ci_idx, flags)) + global_ci_idx += 1 + + if not entries: + return b"" + + buf = bytearray() + buf.extend(encode_varint(_VERSION)) + buf.extend(encode_varint(len(entries))) + prev_idx = 0 + for ci_idx, flags in entries: + buf.extend(encode_varint(ci_idx - prev_idx)) + buf.extend(encode_varint(flags)) + prev_idx = ci_idx + return bytes(buf) + + +class CoveritemFlagsReader: + """Deserialize coveritem_flags.bin and apply to scope tree.""" + + def deserialize(self, data: bytes, db) -> None: + if not data: + return + + offset = 0 + version, offset = decode_varint(data, offset) + if version != _VERSION: + return + + num_entries, offset = decode_varint(data, offset) + if num_entries == 0: + return + + entries = [] + prev_idx = 0 + for _ in range(num_entries): + delta, offset = decode_varint(data, offset) + flags, offset = decode_varint(data, offset) + ci_idx = prev_idx + delta + entries.append((ci_idx, flags)) + prev_idx = ci_idx + + scopes = dfs_scope_list(db) + global_ci_idx = 0 + entry_pos = 0 + for scope in scopes: + for ci in scope.coverItems(_COVER_ALL): + if entry_pos < len(entries) and entries[entry_pos][0] == global_ci_idx: + ci.setCoverFlags(entries[entry_pos][1]) + entry_pos += 1 + global_ci_idx += 1 diff --git a/src/ucis/ncdb/ncdb_reader.py b/src/ucis/ncdb/ncdb_reader.py index 83d8873..b72a7a9 100644 --- a/src/ucis/ncdb/ncdb_reader.py +++ b/src/ucis/ncdb/ncdb_reader.py @@ -18,6 +18,7 @@ from .cross import CrossReader from .contrib import ContribReader from .formal import FormalReader +from .coveritem_flags import CoveritemFlagsReader from .design_units import DesignUnitsReader from .manifest import Manifest from .constants import ( @@ -26,6 +27,7 @@ MEMBER_ATTRS, MEMBER_TAGS, MEMBER_PROPERTIES, MEMBER_TOGGLE, MEMBER_FSM, MEMBER_CROSS, MEMBER_DESIGN_UNITS, MEMBER_CONTRIB_DIR, MEMBER_FORMAL, NCDB_FORMAT, + MEMBER_COVERITEM_FLAGS, ) from ucis.mem.mem_ucis import MemUCIS @@ -84,6 +86,7 @@ def read(self, path: str) -> MemUCIS: cross_bytes = zf.read(MEMBER_CROSS) if MEMBER_CROSS in names else b'' du_bytes = zf.read(MEMBER_DESIGN_UNITS) if MEMBER_DESIGN_UNITS in names else b'' formal_bytes = zf.read(MEMBER_FORMAL) if MEMBER_FORMAL in names else b'' + ci_flags_bytes = zf.read(MEMBER_COVERITEM_FLAGS) if MEMBER_COVERITEM_FLAGS in names else b'' # Collect all contrib/* members contrib_members = { n: zf.read(n) for n in names if n.startswith(MEMBER_CONTRIB_DIR) @@ -117,8 +120,6 @@ def read(self, path: str) -> MemUCIS: _fixup_instance_du_links(db) # Apply optional attrs, tags, typed properties, toggle and FSM metadata - if attrs_bytes: - AttrsReader().deserialize(attrs_bytes, db) if tags_bytes: TagsReader().deserialize(tags_bytes, db) if props_bytes: @@ -139,6 +140,8 @@ def read(self, path: str) -> MemUCIS: # Formal verification data (optional) if formal_bytes: FormalReader().apply(db, formal_bytes) + if ci_flags_bytes: + CoveritemFlagsReader().deserialize(ci_flags_bytes, db) # Register source files as file handles in db for fh in file_handles: @@ -191,4 +194,7 @@ def read(self, path: str) -> MemUCIS: if node.getComment() is not None: hn.setComment(node.getComment()) + if attrs_bytes: + AttrsReader().deserialize(attrs_bytes, db) + return db diff --git a/src/ucis/ncdb/ncdb_writer.py b/src/ucis/ncdb/ncdb_writer.py index 50acf96..6db9733 100644 --- a/src/ucis/ncdb/ncdb_writer.py +++ b/src/ucis/ncdb/ncdb_writer.py @@ -18,6 +18,7 @@ from .cross import CrossWriter from .contrib import ContribWriter from .formal import FormalWriter +from .coveritem_flags import CoveritemFlagsWriter from .design_units import DesignUnitsWriter from .manifest import Manifest from .constants import ( @@ -25,6 +26,7 @@ MEMBER_COUNTS, MEMBER_HISTORY, MEMBER_SOURCES, MEMBER_ATTRS, MEMBER_TAGS, MEMBER_PROPERTIES, MEMBER_TOGGLE, MEMBER_FSM, MEMBER_CROSS, MEMBER_DESIGN_UNITS, MEMBER_FORMAL, + MEMBER_COVERITEM_FLAGS, ) from ucis.history_node_kind import HistoryNodeKind @@ -82,6 +84,7 @@ def write(self, db, path: str) -> None: du_bytes = DesignUnitsWriter().serialize(db) contrib_members = ContribWriter().serialize(db) formal_bytes = FormalWriter().serialize(db) + ci_flags_bytes = CoveritemFlagsWriter().serialize(db) # 7. Manifest manifest = Manifest.build(db, scope_tree_bytes, counts, all_nodes) @@ -95,7 +98,8 @@ def write(self, db, path: str) -> None: zf.writestr(MEMBER_COUNTS, counts_bytes) zf.writestr(MEMBER_HISTORY, history_bytes) zf.writestr(MEMBER_SOURCES, sources_bytes) - if attrs_bytes != b'{"version":1,"entries":[]}': + _EMPTY_ATTRS_V2 = b'{"version":2,"scopes":[],"coveritems":[],"history":[],"global":{}}' + if attrs_bytes not in (b'{"version":1,"entries":[]}', _EMPTY_ATTRS_V2): zf.writestr(MEMBER_ATTRS, attrs_bytes) if tags_bytes != b'{"version":1,"entries":[]}': zf.writestr(MEMBER_TAGS, tags_bytes) @@ -113,3 +117,5 @@ def write(self, db, path: str) -> None: zf.writestr(member_name, member_bytes) if formal_bytes: zf.writestr(MEMBER_FORMAL, formal_bytes) + if ci_flags_bytes: + zf.writestr(MEMBER_COVERITEM_FLAGS, ci_flags_bytes) diff --git a/src/ucis/ncdb/scope_tree.py b/src/ucis/ncdb/scope_tree.py index 40cce4e..4f4f575 100644 --- a/src/ucis/ncdb/scope_tree.py +++ b/src/ucis/ncdb/scope_tree.py @@ -26,6 +26,7 @@ from .constants import ( SCOPE_MARKER_REGULAR, SCOPE_MARKER_TOGGLE_PAIR, PRESENCE_FLAGS, PRESENCE_SOURCE, PRESENCE_WEIGHT, PRESENCE_AT_LEAST, + PRESENCE_GOAL, PRESENCE_SOURCE_TYPE, TOGGLE_BIN_0_TO_1, TOGGLE_BIN_1_TO_0, COVER_TYPE_DEFAULTS, ) @@ -117,6 +118,12 @@ def _write_regular_scope(self, scope): has_flags = (hasattr(scope, 'm_flags') and scope.m_flags != 0) weight = scope.getWeight() if hasattr(scope, 'getWeight') else 1 has_weight = (weight is not None and weight != 1) + goal = scope.getGoal() if hasattr(scope, 'getGoal') else -1 + has_goal = (goal is not None and goal != -1) + + source_type = getattr(scope, 'm_source_type', None) + has_source_type = (source_type is not None + and int(source_type) != int(SourceT.NONE)) # Cover items under this scope cover_items = list(scope.coverItems(CoverTypeT.ALL)) @@ -149,6 +156,8 @@ def _write_regular_scope(self, scope): if has_src: presence |= PRESENCE_SOURCE if has_weight: presence |= PRESENCE_WEIGHT if has_at_least: presence |= PRESENCE_AT_LEAST + if has_goal: presence |= PRESENCE_GOAL + if has_source_type: presence |= PRESENCE_SOURCE_TYPE # Count child sub-scopes child_scopes = list(scope.scopes(ScopeTypeT.ALL)) @@ -170,6 +179,10 @@ def _write_regular_scope(self, scope): w(encode_varint(weight)) if has_at_least: w(encode_varint(at_least_override)) + if has_goal: + w(encode_varint(goal)) + if has_source_type: + w(encode_varint(int(source_type))) w(encode_varint(len(child_scopes))) w(encode_varint(num_coveritems)) @@ -283,6 +296,12 @@ def _read_regular_scope(self, data: bytes, offset: int, parent, counts_iter): weight, offset = decode_varint(data, offset) if presence & PRESENCE_AT_LEAST: at_least_override, offset = decode_varint(data, offset) + goal = -1 + if presence & PRESENCE_GOAL: + goal, offset = decode_varint(data, offset) + source_type_val = int(SourceT.NONE) + if presence & PRESENCE_SOURCE_TYPE: + source_type_val, offset = decode_varint(data, offset) num_children, offset = decode_varint(data, offset) num_coveritems, offset = decode_varint(data, offset) @@ -315,6 +334,11 @@ def _read_regular_scope(self, data: bytes, offset: int, parent, counts_iter): else: scope = parent.createScope(name, srcinfo, weight, SourceT.NONE, scope_type, flags) + if goal != -1 and hasattr(scope, 'setGoal'): + scope.setGoal(goal) + if source_type_val != int(SourceT.NONE) and hasattr(scope, 'm_source_type'): + scope.m_source_type = SourceT(source_type_val) + # Coveritems for _ in range(num_coveritems): ci_name_ref, offset = decode_varint(data, offset) diff --git a/src/ucis/sqlite/sqlite_scope.py b/src/ucis/sqlite/sqlite_scope.py index 43e7e38..c2ae9e5 100644 --- a/src/ucis/sqlite/sqlite_scope.py +++ b/src/ucis/sqlite/sqlite_scope.py @@ -151,8 +151,24 @@ def createScope(self, name: str, srcinfo: SourceInfo, weight: int, def createInstance(self, name: str, fileinfo: SourceInfo, weight: int, source: SourceT, type: ScopeTypeT, du_scope: 'Scope', flags: FlagsT) -> 'Scope': - """Create an instance scope""" - return self.createScope(name, fileinfo, weight, source, type, flags) + """Create an instance scope with design-unit linkage. + + Persists the instance-to-DU association in the design_units table + so that getInstanceDu() can retrieve it later. + """ + scope = self.createScope(name, fileinfo, weight, source, type, flags) + if du_scope is not None: + du_id = getattr(du_scope, 'scope_id', None) + if du_id is not None: + du_name = du_scope.getScopeName() + du_type = (du_scope._scope_type + if getattr(du_scope, '_scope_type', None) is not None + else 0) + self.ucis_db.conn.execute( + "INSERT OR REPLACE INTO design_units " + "(du_scope_id, du_name, du_type) VALUES (?, ?, ?)", + (scope.scope_id, du_name, du_type)) + return scope def createToggle(self, name: str, canonical_name: str, flags: FlagsT, toggle_metric, toggle_type, toggle_dir) -> 'Scope': @@ -281,26 +297,36 @@ def getScopeType(self) -> ScopeTypeT: def getInstanceDu(self) -> 'SqliteScope': """Get the design-unit scope associated with this instance. - Searches sibling scopes (same parent) for the first DU_MODULE (or any - DU_ANY) scope and returns it. This mirrors how MemInstanceScope works. + First checks the design_units table for an explicit link recorded by + createInstance(). Falls back to a sibling-scope heuristic for + databases created before the fix. """ + # Explicit lookup via design_units table + row = self.ucis_db.conn.execute( + "SELECT du_name FROM design_units WHERE du_scope_id = ?", + (self.scope_id,)).fetchone() + if row is not None: + du_name = row[0] + du_row = self.ucis_db.conn.execute( + "SELECT scope_id FROM scopes WHERE scope_name = ? AND scope_type = ?", + (du_name, ScopeTypeT.DU_MODULE)).fetchone() + if du_row is not None: + return SqliteScope(self.ucis_db, du_row[0]) + + # Fallback: search sibling scopes for a DU scope self._ensure_loaded() du_mask = (ScopeTypeT.DU_MODULE | ScopeTypeT.DU_ARCH | ScopeTypeT.DU_PACKAGE | ScopeTypeT.DU_PROGRAM | ScopeTypeT.DU_INTERFACE) - # Look in parent's children for a DU scope parent_id = self._parent_id if parent_id is None: - # Top-level: search root-level scopes in db cursor = self.ucis_db.conn.execute( "SELECT scope_id FROM scopes WHERE parent_id IS NULL AND (scope_type & ?) != 0", - (int(du_mask),) - ) + (int(du_mask),)) else: cursor = self.ucis_db.conn.execute( "SELECT scope_id FROM scopes WHERE parent_id = ? AND (scope_type & ?) != 0", - (parent_id, int(du_mask)) - ) + (parent_id, int(du_mask))) row = cursor.fetchone() if row: return SqliteScope(self.ucis_db, row[0]) diff --git a/tests/unit/ncdb/test_attrs.py b/tests/unit/ncdb/test_attrs.py index 2c33f4a..f0c9065 100644 --- a/tests/unit/ncdb/test_attrs.py +++ b/tests/unit/ncdb/test_attrs.py @@ -51,8 +51,8 @@ def test_attrs_writer_empty(): db.createScope("blk", None, 1, SourceT.SV, ScopeTypeT.BLOCK, 0) data = AttrsWriter().serialize(db) payload = json.loads(data) - assert payload["version"] == 1 - assert payload["entries"] == [] + assert payload["version"] == 2 + assert payload["scopes"] == [] def test_attrs_writer_single(): @@ -60,8 +60,8 @@ def test_attrs_writer_single(): db, block = _make_db_with_attrs({"author": "alice"}) data = AttrsWriter().serialize(db) payload = json.loads(data) - assert len(payload["entries"]) == 1 - assert payload["entries"][0]["attrs"] == {"author": "alice"} + assert len(payload["scopes"]) == 1 + assert payload["scopes"][0]["attrs"] == {"author": "alice"} def test_attrs_writer_multiple_keys(): @@ -69,7 +69,7 @@ def test_attrs_writer_multiple_keys(): db, block = _make_db_with_attrs({"k1": "v1", "k2": "v2", "k3": "v3"}) data = AttrsWriter().serialize(db) payload = json.loads(data) - assert payload["entries"][0]["attrs"] == {"k1": "v1", "k2": "v2", "k3": "v3"} + assert payload["scopes"][0]["attrs"] == {"k1": "v1", "k2": "v2", "k3": "v3"} def test_attrs_reader_applies_attrs(): diff --git a/tests/unit/ncdb/test_ucis_compliance.py b/tests/unit/ncdb/test_ucis_compliance.py new file mode 100644 index 0000000..a8d040d --- /dev/null +++ b/tests/unit/ncdb/test_ucis_compliance.py @@ -0,0 +1,255 @@ +""" +NCDB UCIS compliance tests. + +Validates that the NCDB format correctly round-trips all UCIS 1.0 LRM +data model features. Organised by implementation phase from +NCDB_COMPLIANCE_PLAN.md. +""" + +import os +import tempfile +import pytest + +from ucis.mem.mem_ucis import MemUCIS +from ucis.ncdb.ncdb_writer import NcdbWriter +from ucis.ncdb.ncdb_reader import NcdbReader +from ucis.scope_type_t import ScopeTypeT +from ucis.source_t import SourceT +from ucis.cover_type_t import CoverTypeT +from ucis.cover_data import CoverData +from ucis.cover_flags_t import CoverFlagsT +from ucis.history_node_kind import HistoryNodeKind +from ucis.source_info import SourceInfo + + +def _roundtrip(db): + """Write MemUCIS to NCDB tempfile, read back, return new MemUCIS.""" + with tempfile.NamedTemporaryFile(suffix=".cdb", delete=False) as f: + path = f.name + try: + NcdbWriter().write(db, path) + return NcdbReader().read(path) + finally: + os.unlink(path) + + +# ═══════════════════════════════════════════════════════════════════════ +# Phase 1 — Scope goal + source type +# ═══════════════════════════════════════════════════════════════════════ + +class TestPhase1ScopeGoalSourceType: + + def test_scope_goal_default(self): + """Scopes with default goal (-1) round-trip correctly.""" + db = MemUCIS() + du = db.createScope("tb", None, 1, SourceT.NONE, + ScopeTypeT.DU_MODULE, 0) + assert du.getGoal() == -1 + db2 = _roundtrip(db) + du2 = list(db2.scopes(ScopeTypeT.ALL))[0] + assert du2.getGoal() == -1 + + def test_scope_goal_custom(self): + """Scopes with non-default goal round-trip correctly.""" + db = MemUCIS() + cg = db.createScope("cg", None, 1, SourceT.NONE, + ScopeTypeT.COVERGROUP, 0) + cg.setGoal(90) + db2 = _roundtrip(db) + cg2 = list(db2.scopes(ScopeTypeT.ALL))[0] + assert cg2.getGoal() == 90 + + def test_scope_source_type(self): + """Scope source type (SourceT) round-trips correctly.""" + db = MemUCIS() + scope = db.createScope("s", None, 1, SourceT.NONE, + ScopeTypeT.COVERPOINT, 0) + # SourceT is stored on the scope at creation + db2 = _roundtrip(db) + s2 = list(db2.scopes(ScopeTypeT.ALL))[0] + # For now: confirm scope survives; source_type test will be + # expanded once the field is serialized. + assert s2.getScopeName() == "s" + + def test_weight_and_at_least_preserved(self): + """Confirm existing weight + at_least still work after changes.""" + db = MemUCIS() + cp = db.createScope("cp", None, 5, SourceT.NONE, + ScopeTypeT.COVERPOINT, 0) + cd = CoverData(CoverTypeT.CVGBIN, CoverFlagsT.IS_32BIT) + cd.data = 3 + cd.at_least = 2 + cp.createNextCover("bin0", cd, None) + + db2 = _roundtrip(db) + cp2 = list(db2.scopes(ScopeTypeT.ALL))[0] + assert cp2.getWeight() == 5 + ci = list(cp2.coverItems(0xFFFFFFFF))[0] + assert ci.getCoverData().data == 3 + assert ci.getCoverData().at_least == 2 + + +# ═══════════════════════════════════════════════════════════════════════ +# Phase 2 — Per-coveritem flags +# ═══════════════════════════════════════════════════════════════════════ + +class TestPhase2CoveritemFlags: + + def test_coveritem_flags_roundtrip(self): + """Per-coveritem flags survive NCDB round-trip.""" + db = MemUCIS() + scope = db.createScope("br", None, 1, SourceT.NONE, + ScopeTypeT.BRANCH, 0) + cd = CoverData(CoverTypeT.BRANCHBIN, CoverFlagsT.IS_32BIT) + cd.data = 1 + scope.createNextCover("true", cd, None) + + cd2 = CoverData(CoverTypeT.BRANCHBIN, CoverFlagsT.IS_32BIT) + cd2.data = 0 + cd2.flags |= 0x00000020 # UCIS_EXCLUDE_PRAGMA + scope.createNextCover("else", cd2, None) + + db2 = _roundtrip(db) + sc2 = list(db2.scopes(ScopeTypeT.ALL))[0] + items = list(sc2.coverItems(0xFFFFFFFF)) + assert len(items) == 2 + # First bin: no exclusion + assert (items[0].getCoverFlags() & 0x00000020) == 0 + # Second bin: excluded + assert (items[1].getCoverFlags() & 0x00000020) != 0 + + def test_coveritem_flags_zero_by_default(self): + """Coveritems with no special flags still work.""" + db = MemUCIS() + scope = db.createScope("cp", None, 1, SourceT.NONE, + ScopeTypeT.COVERPOINT, 0) + cd = CoverData(CoverTypeT.CVGBIN, CoverFlagsT.IS_32BIT) + cd.data = 5 + scope.createNextCover("b", cd, None) + + db2 = _roundtrip(db) + sc2 = list(db2.scopes(ScopeTypeT.ALL))[0] + items = list(sc2.coverItems(0xFFFFFFFF)) + assert items[0].getCoverData().data == 5 + + +# ═══════════════════════════════════════════════════════════════════════ +# Phase 3 — Attribute system +# ═══════════════════════════════════════════════════════════════════════ + +class TestPhase3Attributes: + + def test_scope_string_attr_roundtrip(self): + """Scope-level string attributes survive round-trip.""" + db = MemUCIS() + scope = db.createScope("s", None, 1, SourceT.NONE, + ScopeTypeT.DU_MODULE, 0) + scope.setAttribute("mykey", "myvalue") + + db2 = _roundtrip(db) + s2 = list(db2.scopes(ScopeTypeT.ALL))[0] + assert s2.getAttribute("mykey") == "myvalue" + + def test_coveritem_attr_roundtrip(self): + """Coveritem-level attributes survive round-trip.""" + db = MemUCIS() + scope = db.createScope("cp", None, 1, SourceT.NONE, + ScopeTypeT.COVERPOINT, 0) + cd = CoverData(CoverTypeT.CVGBIN, CoverFlagsT.IS_32BIT) + cd.data = 1 + scope.createNextCover("b0", cd, None) + items = list(scope.coverItems(0xFFFFFFFF)) + items[0].setAttribute("BINRHS", "a") + + db2 = _roundtrip(db) + s2 = list(db2.scopes(ScopeTypeT.ALL))[0] + items2 = list(s2.coverItems(0xFFFFFFFF)) + assert items2[0].getAttribute("BINRHS") == "a" + + def test_history_node_attr_roundtrip(self): + """History-node attributes survive round-trip.""" + db = MemUCIS() + hn = db.createHistoryNode(None, "test1", "/path", + HistoryNodeKind.TEST) + hn.setAttribute("run_id", "42") + + db2 = _roundtrip(db) + nodes = list(db2.historyNodes(HistoryNodeKind.TEST)) + assert len(nodes) >= 1 + assert nodes[0].getAttribute("run_id") == "42" + + def test_global_attr_roundtrip(self): + """DB-level global attributes survive round-trip.""" + db = MemUCIS() + db.setAttribute("tool_version", "1.2.3") + + db2 = _roundtrip(db) + assert db2.getAttribute("tool_version") == "1.2.3" + + +# ═══════════════════════════════════════════════════════════════════════ +# Phase 4 — Properties +# ═══════════════════════════════════════════════════════════════════════ + +class TestPhase4Properties: + + def test_expr_terms_roundtrip(self): + """EXPR_TERMS string property round-trips.""" + from ucis.str_property import StrProperty + db = MemUCIS() + scope = db.createScope("cond", None, 1, SourceT.NONE, + ScopeTypeT.COND, 0) + scope.setStringProperty(-1, StrProperty.EXPR_TERMS, "a#b#c") + + db2 = _roundtrip(db) + s2 = list(db2.scopes(ScopeTypeT.ALL))[0] + assert s2.getStringProperty(-1, StrProperty.EXPR_TERMS) == "a#b#c" + + def test_du_signature_roundtrip(self): + """DU_SIGNATURE string property round-trips.""" + from ucis.str_property import StrProperty + db = MemUCIS() + du = db.createScope("mod", None, 1, SourceT.NONE, + ScopeTypeT.DU_MODULE, 0) + du.setStringProperty(-1, StrProperty.DU_SIGNATURE, "abc123") + + db2 = _roundtrip(db) + du2 = list(db2.scopes(ScopeTypeT.ALL))[0] + assert du2.getStringProperty(-1, StrProperty.DU_SIGNATURE) == "abc123" + + +# ═══════════════════════════════════════════════════════════════════════ +# Phase 5 — History hierarchy + coveritem tags +# ═══════════════════════════════════════════════════════════════════════ + +class TestPhase5HistoryAndTags: + + def test_history_node_hierarchy(self): + """History node parent-child relationships survive round-trip.""" + db = MemUCIS() + merge = db.createHistoryNode(None, "merge", "/merge", + HistoryNodeKind.MERGE) + db.createHistoryNode(merge, "test1", "/t1", + HistoryNodeKind.TEST) + db.createHistoryNode(merge, "test2", "/t2", + HistoryNodeKind.TEST) + + db2 = _roundtrip(db) + merges = list(db2.historyNodes(HistoryNodeKind.MERGE)) + tests = list(db2.historyNodes(HistoryNodeKind.TEST)) + assert len(merges) >= 1 + assert len(tests) >= 2 + + def test_scope_tags_roundtrip(self): + """Scope tags survive round-trip (existing feature).""" + db = MemUCIS() + scope = db.createScope("s", None, 1, SourceT.NONE, + ScopeTypeT.DU_MODULE, 0) + scope.addTag("vplan_req_1") + scope.addTag("critical") + + db2 = _roundtrip(db) + s2 = list(db2.scopes(ScopeTypeT.ALL))[0] + tags = set(s2.getTags()) + assert "vplan_req_1" in tags + assert "critical" in tags From 2d48113748f7e7535bc602076fae7bb6eaafd2db Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 28 Feb 2026 22:11:12 -0500 Subject: [PATCH 2/4] ncdb: derive coveritem flags from type defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UCIS LRM defines IS_32BIT, HAS_GOAL, HAS_WEIGHT as structural flags determined by cover type, not per-bin state. All CVGBIN items have IS_32BIT|HAS_GOAL|HAS_WEIGHT (0x19); all others have IS_32BIT (0x01). These are now stored in COVER_TYPE_DEFAULTS and applied automatically during deserialization. coveritem_flags.bin only stores flags that DIFFER from the type default (exclusion flags, IS_BR_ELSE, IS_FSM_RESET, etc.). For typical databases this means 0 entries → member not written. Impact on merged.vdb benchmark (131K bins): - coveritem_flags.bin: 264KB → 0 (not written) - Read: 3.13s → 2.79s (-11%, ~340ms saved) - Write: 2.71s → 2.63s (-3%, ~80ms saved) - Merge: unchanged (fast path never touches flags) Full test suite: 1002 passed, 0 failed --- src/ucis/ncdb/constants.py | 30 +++++++++++++++--------------- src/ucis/ncdb/coveritem_flags.py | 9 ++++++++- src/ucis/ncdb/scope_tree.py | 6 ++++-- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/ucis/ncdb/constants.py b/src/ucis/ncdb/constants.py index faf0fb3..16289ab 100644 --- a/src/ucis/ncdb/constants.py +++ b/src/ucis/ncdb/constants.py @@ -67,21 +67,21 @@ # Used by reader to reconstruct coveritem objects without per-item storage. COVER_TYPE_DEFAULTS: dict = { - CoverTypeT.TOGGLEBIN: (0, 0, 1), - CoverTypeT.STMTBIN: (0, 0, 1), - CoverTypeT.BRANCHBIN: (0, 0, 1), - CoverTypeT.CONDBIN: (0, 0, 1), - CoverTypeT.EXPRBIN: (0, 0, 1), - CoverTypeT.FSMBIN: (0, 0, 1), - CoverTypeT.CVGBIN: (0, 1, 1), - CoverTypeT.DEFAULTBIN: (0, 0, 1), - CoverTypeT.IGNOREBIN: (0, 0, 1), - CoverTypeT.ILLEGALBIN: (0, 0, 1), - CoverTypeT.BLOCKBIN: (0, 0, 1), - CoverTypeT.COVERBIN: (0, 0, 1), - CoverTypeT.ASSERTBIN: (0, 0, 1), - CoverTypeT.PASSBIN: (0, 0, 1), - CoverTypeT.FAILBIN: (0, 0, 1), + CoverTypeT.TOGGLEBIN: (0x01, 0, 1), + CoverTypeT.STMTBIN: (0x01, 0, 1), + CoverTypeT.BRANCHBIN: (0x01, 0, 1), + CoverTypeT.CONDBIN: (0x01, 0, 1), + CoverTypeT.EXPRBIN: (0x01, 0, 1), + CoverTypeT.FSMBIN: (0x01, 0, 1), + CoverTypeT.CVGBIN: (0x19, 1, 1), + CoverTypeT.DEFAULTBIN: (0x01, 0, 1), + CoverTypeT.IGNOREBIN: (0x01, 0, 1), + CoverTypeT.ILLEGALBIN: (0x01, 0, 1), + CoverTypeT.BLOCKBIN: (0x01, 0, 1), + CoverTypeT.COVERBIN: (0x01, 0, 1), + CoverTypeT.ASSERTBIN: (0x01, 0, 1), + CoverTypeT.PASSBIN: (0x01, 0, 1), + CoverTypeT.FAILBIN: (0x01, 0, 1), } # ── Scope-type → implicit child cover type mapping ──────────────────────── diff --git a/src/ucis/ncdb/coveritem_flags.py b/src/ucis/ncdb/coveritem_flags.py index 1995c1a..b38bd73 100644 --- a/src/ucis/ncdb/coveritem_flags.py +++ b/src/ucis/ncdb/coveritem_flags.py @@ -17,6 +17,7 @@ from .varint import encode_varint, decode_varint from .dfs_util import dfs_scope_list +from .constants import COVER_TYPE_DEFAULTS _VERSION = 1 _COVER_ALL = 0xFFFFFFFF @@ -32,7 +33,9 @@ def serialize(self, db) -> bytes: for scope in scopes: for ci in scope.coverItems(_COVER_ALL): flags = ci.getCoverFlags() - if flags != 0: + ct = ci.getCoverData().type + default_flags = COVER_TYPE_DEFAULTS.get(ct, (0, 0, 1))[0] + if flags != default_flags: entries.append((global_ci_idx, flags)) global_ci_idx += 1 @@ -83,4 +86,8 @@ def deserialize(self, data: bytes, db) -> None: if entry_pos < len(entries) and entries[entry_pos][0] == global_ci_idx: ci.setCoverFlags(entries[entry_pos][1]) entry_pos += 1 + else: + ct = ci.getCoverData().type + default_flags = COVER_TYPE_DEFAULTS.get(ct, (0, 0, 1))[0] + ci.setCoverFlags(default_flags) global_ci_idx += 1 diff --git a/src/ucis/ncdb/scope_tree.py b/src/ucis/ncdb/scope_tree.py index 4f4f575..2d42561 100644 --- a/src/ucis/ncdb/scope_tree.py +++ b/src/ucis/ncdb/scope_tree.py @@ -265,7 +265,8 @@ def _read_toggle_pair(self, data: bytes, offset: int, parent, counts_iter): # Create the two implicit TOGGLEBIN coveritems for (bin_name, count) in ((TOGGLE_BIN_0_TO_1, count_0to1), (TOGGLE_BIN_1_TO_0, count_1to0)): - cd = CoverData(CoverTypeT.TOGGLEBIN, 0) + cd = CoverData(CoverTypeT.TOGGLEBIN, + COVER_TYPE_DEFAULTS.get(CoverTypeT.TOGGLEBIN, (0,0,1))[0]) cd.data = count scope.createNextCover(bin_name, cd, None) @@ -344,7 +345,8 @@ def _read_regular_scope(self, data: bytes, offset: int, parent, counts_iter): ci_name_ref, offset = decode_varint(data, offset) ci_name = self._st.get(ci_name_ref) count = next(counts_iter, 0) - cd = CoverData(child_cover_type, 0) + default_flags = COVER_TYPE_DEFAULTS.get(child_cover_type, (0, 0, 1))[0] + cd = CoverData(child_cover_type, default_flags) cd.data = count if at_least_override is not None or (child_cover_type and COVER_TYPE_DEFAULTS.get(child_cover_type, (0,0,1))[1] != 0): From b3f19b1678cd6eb1bf2e011a494977b42859653d Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Mon, 2 Mar 2026 01:34:19 +0000 Subject: [PATCH 3/4] Performance tweaks Signed-off-by: Matthew Ballance --- doc/source/reference/formats/ncdb-format.rst | 334 ++++++++++++++++--- src/ucis/mem/mem_cover_index_iterator.py | 22 +- src/ucis/mem/mem_instance_scope.py | 13 +- src/ucis/mem/mem_scope_iterator.py | 22 +- src/ucis/ncdb/dfs_util.py | 5 + src/ucis/ncdb/ncdb_reader.py | 30 +- src/ucis/ncdb/scope_tree.py | 21 +- src/ucis/report/coverage_report_builder.py | 102 +++--- src/ucis/scope_type_t.py | 4 +- 9 files changed, 399 insertions(+), 154 deletions(-) diff --git a/doc/source/reference/formats/ncdb-format.rst b/doc/source/reference/formats/ncdb-format.rst index 73f1b81..9406219 100644 --- a/doc/source/reference/formats/ncdb-format.rst +++ b/doc/source/reference/formats/ncdb-format.rst @@ -100,31 +100,39 @@ non-default. - Ordered list of source file paths; indices match file IDs in ``scope_tree.bin``. * - ``attrs.bin`` - — - - User-defined attribute assignments. + - User-defined attribute assignments (V2 JSON: scopes, coveritems, + history nodes, and global attributes). * - ``tags.json`` - — - - Tag assignments for scopes and coveritems. + - Tag assignments for scopes (sparse, DFS-indexed). * - ``toggle.bin`` - — - - Per-signal toggle metadata (canonical name, type, direction). + - Per-signal toggle metadata (JSON: canonical name, metric, type, + direction). * - ``fsm.bin`` - — - - FSM state and transition metadata. + - FSM state-index overrides (JSON, sparse; only written when state + indices differ from the default 0, 1, 2, … sequence). * - ``cross.bin`` - — - - Cross-coverpoint link records. + - Cross-coverpoint link records (JSON: crossed coverpoint sibling names). * - ``properties.json`` - — - - Typed property values (int, real, string, handle). + - Typed string property values (DFS scope-indexed). * - ``design_units.json`` - — - - Design-unit records (module, package, interface, program). + - Design-unit name-to-DFS-index lookup table (name, index, scope type). * - ``formal.bin`` - — - - Formal-verification assertion data. - * - ``contrib/NNNNN.bin`` + - Formal-verification assertion data (JSON: status, radius, witness). + * - ``coveritem_flags.bin`` + - — + - Per-coveritem non-default flags (sparse delta-encoded binary). + * - ``contrib/.bin`` - — - Per-test coveritem contribution arrays (delta-encoded, sparse). + One file per history node that has contributions; ```` is + the integer history-node index (not zero-padded). ----------- @@ -320,12 +328,14 @@ Every scope record begins with a one-byte **marker**: [presence : varint ] bitfield of optional fields present (see below) — optional fields, each present only if the corresponding bit is set — - [flags : varint ] only if PRESENCE_FLAGS (bit 0) set - [file_id : varint ] only if PRESENCE_SOURCE (bit 1) set - [line : varint ] " - [token : varint ] " - [weight : varint ] only if PRESENCE_WEIGHT (bit 2) set - [at_least : varint ] only if PRESENCE_AT_LEAST (bit 3) set + [flags : varint ] only if PRESENCE_FLAGS (bit 0) set + [file_id : varint ] only if PRESENCE_SOURCE (bit 1) set + [line : varint ] " + [token : varint ] " + [weight : varint ] only if PRESENCE_WEIGHT (bit 2) set + [at_least : varint ] only if PRESENCE_AT_LEAST (bit 3) set + [goal : varint ] only if PRESENCE_GOAL (bit 5) set + [source_type : varint ] only if PRESENCE_SOURCE_TYPE (bit 6) set — always present — [num_children : varint] number of child scope records that follow @@ -361,6 +371,16 @@ Every scope record begins with a one-byte **marker**: - ``PRESENCE_AT_LEAST`` - An ``at_least`` threshold that overrides the cover-type default is stored at the scope level (applies to all coveritems in the scope). + * - 4 + - ``PRESENCE_CVG_OPTS`` + - Reserved for covergroup options (not yet used by the writer). + * - 5 + - ``PRESENCE_GOAL`` + - Non-default scope goal (≠ −1) is stored. + * - 6 + - ``PRESENCE_SOURCE_TYPE`` + - Explicit ``SourceT`` enum value is stored. When absent, the source + type defaults to ``SourceT.NONE``. **Cover-type defaults** (used when ``PRESENCE_AT_LEAST`` is absent): @@ -373,11 +393,11 @@ Every scope record begins with a one-byte **marker**: - at_least default - weight default * - ``CVGBIN`` - - 0 + - ``0x19`` - **1** - 1 * - All others (TOGGLEBIN, STMTBIN, BRANCHBIN, …) - - 0 + - ``0x01`` - 0 - 1 @@ -508,21 +528,28 @@ run (``kind: "TEST"``) or a merge operation (``kind: "MERGE"``). [ { - "name": "regression_seed_42", - "parent": null, + "logical_name": "regression_seed_42", + "physical_name": null, "kind": "TEST", - "teststatus": 0, - "toolcategory": "sim", + "test_status": 0, + "tool_category": "sim", "date": "2026-02-25", - "simtime": 1500.0, - "timeunit": "ns", - "runcwd": "/home/user/sim", - "cputime": 12.3, + "sim_time": 1500.0, + "time_unit": "ns", + "run_cwd": "/home/user/sim", + "cpu_time": 12.3, "seed": "42", "cmd": "vsim -seed 42 top", "args": "", - "user": "jsmith", - "cost": 0.0 + "compulsory": null, + "user_name": "jsmith", + "cost": 0.0, + "ucis_version": null, + "vendor_id": null, + "vendor_tool": null, + "vendor_tool_version": null, + "same_tests": null, + "comment": null } ] @@ -533,35 +560,35 @@ run (``kind: "TEST"``) or a merge operation (``kind: "MERGE"``). * - Field - Type - Description - * - ``name`` + * - ``logical_name`` - string - Unique name for this history node (test name or merge label). - * - ``parent`` + * - ``physical_name`` - string | null - - Name of the parent history node, or ``null`` for a root node. + - Physical file name associated with the history node, or ``null``. * - ``kind`` - ``"TEST"`` | ``"MERGE"`` - History node kind. - * - ``teststatus`` + * - ``test_status`` - integer - Test status code: 0 = OK, 1 = WARNING, 2 = ERROR, 3 = FATAL, 4 = NOTRUN. - * - ``toolcategory`` + * - ``tool_category`` - string - Free-form tool category (e.g. ``"sim"``, ``"formal"``). * - ``date`` - string - Date string (ISO 8601 recommended). - * - ``simtime`` + * - ``sim_time`` - number - - Simulation end time in ``timeunit`` units. - * - ``timeunit`` + - Simulation end time in ``time_unit`` units. + * - ``time_unit`` - string - Simulation time unit (e.g. ``"ns"``, ``"ps"``). - * - ``runcwd`` + * - ``run_cwd`` - string - Working directory of the simulation run. - * - ``cputime`` + * - ``cpu_time`` - number - CPU seconds consumed. * - ``seed`` @@ -573,12 +600,33 @@ run (``kind: "TEST"``) or a merge operation (``kind: "MERGE"``). * - ``args`` - string - Additional arguments. - * - ``user`` + * - ``compulsory`` + - any | null + - Compulsory flag (tool-defined), or ``null`` if unset. + * - ``user_name`` - string - Username that ran the simulation. * - ``cost`` - number - Simulation cost (tool-defined). + * - ``ucis_version`` + - string | null + - UCIS version associated with this history node, or ``null``. + * - ``vendor_id`` + - string | null + - Vendor identifier, or ``null``. + * - ``vendor_tool`` + - string | null + - Vendor tool name, or ``null``. + * - ``vendor_tool_version`` + - string | null + - Vendor tool version, or ``null``. + * - ``same_tests`` + - integer | null + - Number of identical tests merged, or ``null``. + * - ``comment`` + - string | null + - Free-form comment, or ``null``. ----------- @@ -654,11 +702,11 @@ A merge operation appends a ``"MERGE"``-kind history node to ``history.json``: .. code-block:: json { - "name": "merge:output.cdb", - "parent": null, + "logical_name": "merge:output.cdb", + "physical_name": null, "kind": "MERGE", - "teststatus": 0, - "toolcategory": "merge", + "test_status": 0, + "tool_category": "merge", "date": "2026-02-25T21:00:00Z" } @@ -675,32 +723,198 @@ do not support, and must not fail if an expected optional member is absent. 11.1 attrs.bin ============== -User-defined attribute assignments for scopes and coveritems. +User-defined attribute assignments. Despite the ``.bin`` extension, this +member is JSON-encoded. + +**Format v2** (current): + +.. code-block:: json + + { + "version": 2, + "scopes": [ + {"idx": 0, "attrs": {"key": "value"}} + ], + "coveritems": [ + {"scope_idx": 0, "ci_idx": 1, "attrs": {"key": "value"}} + ], + "history": [ + {"idx": 0, "kind": "TEST", "attrs": {"key": "value"}} + ], + "global": {"key": "value"} + } + +``idx`` / ``scope_idx`` values are DFS scope indices (same ordering as +``scope_tree.bin``). ``ci_idx`` is the zero-based coveritem position +within its parent scope. Only objects with at least one attribute are +included (sparse). The reader also accepts legacy **v1** files that store +only scope-level attributes. 11.2 tags.json ============== -Tag assignments. A JSON object mapping tag names to arrays of scope paths. +Tag assignments for scopes (sparse, DFS-indexed). + +.. code-block:: json + + { + "version": 1, + "entries": [ + {"idx": 0, "tags": ["tag_a", "tag_b"]} + ] + } + +``idx`` is the DFS scope index. Only scopes with at least one tag are +included. 11.3 toggle.bin ================ -Per-signal toggle metadata for ``TOGGLE``-type scopes. Records the -canonical signal name, toggle type (NET, REG, …), and direction (IN, OUT, …). +Per-signal toggle metadata for ``TOGGLE``-type scopes. Despite the ``.bin`` +extension, this member is JSON-encoded. + +.. code-block:: json + + { + "version": 1, + "entries": [ + {"idx": 5, "canonical": "top.clk", "metric": 0, "type": 1, "dir": 2} + ] + } + +``idx`` is the DFS scope index. All fields except ``idx`` are optional and +are omitted when they match the defaults (``metric`` = ``ToggleMetricT._2STOGGLE``, +``type`` = ``ToggleTypeT.NET``, ``dir`` = ``ToggleDirT.INTERNAL``). Only +``TOGGLE`` scopes with at least one non-default value are included. 11.4 fsm.bin ============= -FSM metadata for ``FSM``-type scopes. Records state names and transition -labels that correspond to coveritems in ``counts.bin``. +FSM state-index overrides for ``FSM``-type scopes. Despite the ``.bin`` +extension, this member is JSON-encoded. State and transition names are +already stored in ``scope_tree.bin`` as FSMBIN coveritems under FSM_STATES +and FSM_TRANS sub-scopes; this member only records non-sequential state +indices. + +.. code-block:: json -11.5 contrib/NNNNN.bin + { + "version": 1, + "entries": [ + {"fsm_idx": 3, "states": [{"name": "IDLE", "index": 5}]} + ] + } + +``fsm_idx`` is the DFS scope index of the ``FSM`` scope. Only FSM scopes +whose state indices differ from the default 0, 1, 2, … sequence are included. +The member is omitted entirely when all indices are sequential. + +11.5 cross.bin +=============== + +Cross-coverpoint link records for ``CROSS``-type scopes. Despite the +``.bin`` extension, this member is JSON-encoded. + +.. code-block:: json + + { + "version": 1, + "entries": [ + {"idx": 12, "crossed": ["cp_a", "cp_b"]} + ] + } + +``idx`` is the DFS scope index of the ``CROSS`` scope. ``crossed`` lists +the ``getScopeName()`` values of each crossed coverpoint (sibling scopes +within the same parent COVERGROUP/COVERINSTANCE). + +11.6 properties.json +===================== + +Typed string property values for scopes (DFS-indexed). + +.. code-block:: json + + { + "version": 1, + "entries": [ + {"kind": "scope", "idx": 0, "key": 1, "type": "str", "value": "comment text"} + ] + } + +``key`` is the integer value of the ``StrProperty`` enum. Only scopes with +explicitly-set properties are included. + +11.7 design_units.json ======================= -Per-test contribution arrays. One file per test (zero-padded 5-digit -sequence number matches the TEST history node order). Each file encodes a -sparse, delta-encoded array of per-test hit counts, allowing reconstruction -of which tests hit which bins. +Design-unit name-to-DFS-index lookup table. + +.. code-block:: json + + { + "version": 1, + "units": [ + {"name": "top", "idx": 0, "type": 2} + ] + } + +``type`` is the integer value of ``ScopeTypeT`` (e.g. 2 = ``DU_MODULE``). +Only DU_ANY scopes are included. The member is omitted when no design units +are present. + +11.8 formal.bin +================ + +Formal-verification assertion data. Despite the ``.bin`` extension, this +member is JSON-encoded. + +.. code-block:: json + + { + "version": 1, + "entries": [ + {"idx": 42, "status": 1, "radius": 100, "witness": "/path/to/witness.vcd"} + ] + } + +``idx`` is the flat DFS coveritem index (same ordering as ``counts.bin``). +Fields ``status``, ``radius``, and ``witness`` are each omitted when they +match the defaults (0, 0, ``null`` respectively). Defaults: +``status`` = ``FormalStatusT.NONE`` (0), ``radius`` = 0. + +11.9 coveritem_flags.bin +========================= + +Per-coveritem non-default flags. This member uses a true binary encoding +(sparse, delta-encoded varint pairs). + +.. code-block:: text + + [version : varint] always 1 + [num_entries : varint] number of (index, flags) pairs + per entry: + [delta_idx : varint] coveritem DFS index delta from previous entry + [flags : varint] ucisFlagsT value + +Only coveritems whose flags differ from the cover-type default (see +cover-type defaults table in Section 6.2) are included. The member is +omitted entirely when all coveritems use default flags. + +11.10 contrib/.bin +============================= + +Per-test contribution arrays. One file per history node that recorded +contributions; ```` is the integer history-node index (not +zero-padded). Each file encodes a sparse, delta-encoded array of per-test +hit counts, allowing reconstruction of which tests hit which bins. + +.. code-block:: text + + [num_entries : varint] + per entry (sorted by bin_index, ascending): + [delta_bin_index : varint] bin_index − previous bin_index + [count : varint] hit count for this bin from this test ----------- @@ -718,6 +932,20 @@ of which tests hit which bins. - Initial release. Scope-tree V2 encoding with presence bitfield and TOGGLE_PAIR optimization. Varint + UINT32 dual-mode counts encoding. Same-schema fast-merge path via ``schema_hash``. + * - ``1.0`` (UCIS compliance update) + - Added presence bits 4–6 (``PRESENCE_CVG_OPTS``, ``PRESENCE_GOAL``, + ``PRESENCE_SOURCE_TYPE``) to scope records. Added + ``coveritem_flags.bin`` member for per-coveritem non-default flags. + Updated ``history.json`` to UCIS-compliant field names + (``logical_name``, ``physical_name``, ``test_status``, ``sim_time``, + ``time_unit``, ``run_cwd``, ``cpu_time``, ``user_name``) and added + vendor/tool fields (``ucis_version``, ``vendor_id``, ``vendor_tool``, + ``vendor_tool_version``, ``same_tests``, ``comment``, ``compulsory``). + Upgraded ``attrs.bin`` to V2 format with sections for scopes, + coveritems, history nodes, and global attributes. Updated + cover-type default flags to ``0x01`` (most types) / ``0x19`` + (``CVGBIN``). Documented ``cross.bin``, ``properties.json``, + ``design_units.json``, ``formal.bin``, and ``contrib/`` formats. ----------- diff --git a/src/ucis/mem/mem_cover_index_iterator.py b/src/ucis/mem/mem_cover_index_iterator.py index f8402d4..4f7835e 100644 --- a/src/ucis/mem/mem_cover_index_iterator.py +++ b/src/ucis/mem/mem_cover_index_iterator.py @@ -11,23 +11,17 @@ class MemCoverIndexIterator(object): def __init__(self, coveritems : List[MemCoverIndex], mask : CoverTypeT): self.coveritems = coveritems - self.mask = mask + self.mask = int(mask) self.idx = 0 - + def __iter__(self): return self - + def __next__(self): - next = None - - while self.idx < len(self.coveritems) and next is None: + mask = self.mask + while self.idx < len(self.coveritems): n = self.coveritems[self.idx] - - if (n.data.type & self.mask) != 0: - next = n self.idx += 1 - - if next is None: - raise StopIteration - - return next \ No newline at end of file + if (int(n.data.type) & mask) != 0: + return n + raise StopIteration diff --git a/src/ucis/mem/mem_instance_scope.py b/src/ucis/mem/mem_instance_scope.py index c1bf752..9959e6e 100644 --- a/src/ucis/mem/mem_instance_scope.py +++ b/src/ucis/mem/mem_instance_scope.py @@ -50,15 +50,16 @@ def createScope(self, source : SourceT, type : ScopeTypeT, flags : FlagsT) -> 'Scope': - if (type & ScopeTypeT.COVERGROUP) != 0: + itype = int(type) + if (itype & int(ScopeTypeT.COVERGROUP)) != 0: ret = MemCovergroup(self, name, srcinfo, weight, source) - elif (type & ScopeTypeT.BLOCK) != 0: + elif (itype & int(ScopeTypeT.BLOCK)) != 0: ret = MemBlockScope(self, name, srcinfo, weight, source, flags) - elif (type & ScopeTypeT.BRANCH) != 0: + elif (itype & int(ScopeTypeT.BRANCH)) != 0: ret = MemBranchScope(self, name, srcinfo, weight, source, flags) - elif (type & ScopeTypeT.TOGGLE) != 0: + elif (itype & int(ScopeTypeT.TOGGLE)) != 0: ret = MemToggleScope(self, name, srcinfo, weight, source, flags) - elif (type & ScopeTypeT.FSM) != 0: + elif (itype & int(ScopeTypeT.FSM)) != 0: from ucis.mem.mem_fsm_scope import MemFSMScope ret = MemFSMScope(self, name, srcinfo, weight, source, flags) else: @@ -96,4 +97,4 @@ def createToggle(self, def getIthCoverItem(self, i)->CoverItem: return self.m_cover_item_l[i] - \ No newline at end of file + diff --git a/src/ucis/mem/mem_scope_iterator.py b/src/ucis/mem/mem_scope_iterator.py index 8d760c5..d802086 100644 --- a/src/ucis/mem/mem_scope_iterator.py +++ b/src/ucis/mem/mem_scope_iterator.py @@ -11,25 +11,19 @@ class MemScopeIterator(object): def __init__(self, nodes : List['MemScope'], mask): self.nodes = nodes self.idx = 0 - self.mask = mask - + self.mask = int(mask) + def __iter__(self): return self - + def __next__(self): next = None + mask = self.mask - while next is None and self.idx < len(self.nodes): + while self.idx < len(self.nodes): n = self.nodes[self.idx] - # TODO: qualify mask - if (n.getScopeType() & self.mask) != 0: - next = n - self.idx += 1 - - if next is not None: - break + if (int(n.getScopeType()) & mask) != 0: + return n - if next is None: - raise StopIteration - return next \ No newline at end of file + raise StopIteration diff --git a/src/ucis/ncdb/dfs_util.py b/src/ucis/ncdb/dfs_util.py index 92969fa..0e89fe4 100644 --- a/src/ucis/ncdb/dfs_util.py +++ b/src/ucis/ncdb/dfs_util.py @@ -32,6 +32,10 @@ def dfs_scope_list(db) -> list: Toggle-pair BRANCH scopes are included (they appear in scope_tree.bin as TOGGLE_PAIR records but still occupy one DFS slot each). """ + cached = getattr(db, '_dfs_scope_cache', None) + if cached is not None: + return cached + result = [] def _visit(scope): @@ -43,4 +47,5 @@ def _visit(scope): for top_scope in db.scopes(ScopeTypeT.ALL): _visit(top_scope) + db._dfs_scope_cache = result return result diff --git a/src/ucis/ncdb/ncdb_reader.py b/src/ucis/ncdb/ncdb_reader.py index b72a7a9..c335350 100644 --- a/src/ucis/ncdb/ncdb_reader.py +++ b/src/ucis/ncdb/ncdb_reader.py @@ -44,24 +44,26 @@ def _fixup_instance_du_links(db: MemUCIS) -> None: """ from ucis.mem.mem_instance_scope import MemInstanceScope + _DU_MASK = 0x000000001F000000 + _INSTANCE_VAL = int(ScopeTypeT.INSTANCE) + def _fix_parent(parent): - # Build name → DU map from real (attached) children + children = parent.m_children if hasattr(parent, 'm_children') else list(parent.scopes(ScopeTypeT.ALL)) du_map = {} - for child in parent.scopes(ScopeTypeT.ALL): - if ScopeTypeT.DU_ANY(child.getScopeType()): + instances = [] + for child in children: + st = int(child.getScopeType()) + if st & _DU_MASK: du_map[child.getScopeName()] = child - - # Replace placeholder DU refs on INSTANCE scopes - for child in parent.scopes(ScopeTypeT.ALL): - if isinstance(child, MemInstanceScope): - du = child.m_du_scope - if du is not None and du.m_parent is None: - # Detached placeholder — replace with real DU if available - real_du = du_map.get(child.getScopeName()) - if real_du is not None: - child.m_du_scope = real_du - # Recurse + if st == _INSTANCE_VAL: + instances.append(child) _fix_parent(child) + for child in instances: + du = child.m_du_scope + if du is not None and du.m_parent is None: + real_du = du_map.get(child.getScopeName()) + if real_du is not None: + child.m_du_scope = real_du _fix_parent(db) diff --git a/src/ucis/ncdb/scope_tree.py b/src/ucis/ncdb/scope_tree.py index 2d42561..51764ac 100644 --- a/src/ucis/ncdb/scope_tree.py +++ b/src/ucis/ncdb/scope_tree.py @@ -308,6 +308,7 @@ def _read_regular_scope(self, data: bytes, offset: int, parent, counts_iter): num_coveritems, offset = decode_varint(data, offset) child_cover_type = None + at_least = 0 if num_coveritems > 0: ctv, offset = decode_varint(data, offset) child_cover_type = CoverTypeT(ctv) @@ -315,14 +316,18 @@ def _read_regular_scope(self, data: bytes, offset: int, parent, counts_iter): at_least = at_least_override if at_least_override is not None else defaults[1] if scope_type == ScopeTypeT.INSTANCE: - # createInstance() requires a DU reference; find the matching DU - # that was already serialized (DU scopes precede INSTANCE in DFS). du_scope = None - for sibling in parent.scopes(ScopeTypeT.ALL): - if (ScopeTypeT.DU_ANY(sibling.getScopeType()) - and sibling.getScopeName() == name): - du_scope = sibling - break + _du_mask = 0x000000001F000000 + if hasattr(parent, 'm_children'): + for sibling in parent.m_children: + if (int(sibling.getScopeType()) & _du_mask) and sibling.getScopeName() == name: + du_scope = sibling + break + else: + for sibling in parent.scopes(ScopeTypeT.ALL): + if ScopeTypeT.DU_ANY(sibling.getScopeType()) and sibling.getScopeName() == name: + du_scope = sibling + break if du_scope is None: # DU not yet in parent (INSTANCE precedes DU in source ordering). # Create a detached placeholder so createInstance() can succeed @@ -350,7 +355,7 @@ def _read_regular_scope(self, data: bytes, offset: int, parent, counts_iter): cd.data = count if at_least_override is not None or (child_cover_type and COVER_TYPE_DEFAULTS.get(child_cover_type, (0,0,1))[1] != 0): - cd.at_least = at_least if 'at_least' in dir() else 0 + cd.at_least = at_least scope.createNextCover(ci_name, cd, None) # Child scopes diff --git a/src/ucis/report/coverage_report_builder.py b/src/ucis/report/coverage_report_builder.py index 70a8ed6..e0b0103 100644 --- a/src/ucis/report/coverage_report_builder.py +++ b/src/ucis/report/coverage_report_builder.py @@ -31,18 +31,26 @@ def build(db : 'UCIS') ->'CoverageReport': def _build(self)->'CoverageReport': - + + all_coverage = 0.0 + all_div = 0 for iscope in self.db.scopes(ScopeTypeT.INSTANCE): - self.build_covergroups(iscope) - + cov, div = self.build_covergroups(iscope) + all_coverage += cov + all_div += div + + if all_div > 0: + self.report.coverage = all_coverage / all_div + else: + self.report.coverage = 0.0 + return self.report - def build_covergroups(self, iscope): - + coverage = 0.0 div = 0 - + for cg_t in iscope.scopes(ScopeTypeT.COVERGROUP): cg = self.build_covergroup(cg_t) if cg.weight > 0: @@ -50,13 +58,9 @@ def build_covergroups(self, iscope): div += cg.weight self.report.covergroups.append(cg) self.report.covergroup_m[cg.instname] = cg - - # Handle case when there are no covergroups - if div > 0: - self.report.coverage = coverage/div - else: - self.report.coverage = 0.0 - + + return coverage, div + def build_covergroup(self, cg_n)->CoverageReport.Covergroup: cg_r = CoverageReport.Covergroup( cg_n.getScopeName(), @@ -85,7 +89,12 @@ def build_covergroup(self, cg_n)->CoverageReport.Covergroup: for cr in cg_r.crosses: coverage += cr.coverage * cr.weight div += cr.weight - + + for sub in cg_r.covergroups: + if sub.weight > 0: + coverage += sub.coverage * sub.weight + div += sub.weight + if div > 0: coverage /= div cg_r.coverage = coverage @@ -96,37 +105,44 @@ def build_coverpoint(self, cp_n : Coverpoint): cp_r = CoverageReport.Coverpoint(cp_n.getScopeName()) cp_r.weight = cp_n.getWeight() - # Read in bins + # Read in bins — check both direct cover items and typed bin + # sub-scopes (CVGBINSCOPE, IGNOREBINSCOPE, ILLEGALBINSCOPE). num_hit = 0 total = 0 - for ci_n in cp_n.coverItems(CoverTypeT.CVGBIN): - cvg_data = ci_n.getCoverData() - - if cvg_data.data >= cvg_data.at_least: - num_hit += 1 - - cp_r.bins.append(CoverageReport.CoverBin( - ci_n.getName(), - cvg_data.at_least, - cvg_data.data)) - total += 1 - - for ci_n in cp_n.coverItems(CoverTypeT.IGNOREBIN): - cvg_data = ci_n.getCoverData() - - cp_r.ignore_bins.append(CoverageReport.CoverBin( - ci_n.getName(), - cvg_data.at_least, - cvg_data.data)) - - for ci_n in cp_n.coverItems(CoverTypeT.ILLEGALBIN): - cvg_data = ci_n.getCoverData() - - cp_r.illegal_bins.append(CoverageReport.CoverBin( - ci_n.getName(), - cvg_data.at_least, - cvg_data.data)) + def _collect_items(scope): + """Yield (cover_item, effective_type) from a scope.""" + for ci in scope.coverItems(CoverTypeT.CVGBIN): + yield ci, CoverTypeT.CVGBIN + for ci in scope.coverItems(CoverTypeT.IGNOREBIN): + yield ci, CoverTypeT.IGNOREBIN + for ci in scope.coverItems(CoverTypeT.ILLEGALBIN): + yield ci, CoverTypeT.ILLEGALBIN + + # Collect from direct items on the coverpoint + sources = [cp_n] + # Also collect from typed bin sub-scopes + for sub in cp_n.scopes(ScopeTypeT.CVGBINSCOPE): + sources.append(sub) + for sub in cp_n.scopes(ScopeTypeT.IGNOREBINSCOPE): + sources.append(sub) + for sub in cp_n.scopes(ScopeTypeT.ILLEGALBINSCOPE): + sources.append(sub) + + for src in sources: + for ci_n, ct in _collect_items(src): + cvg_data = ci_n.getCoverData() + bin_obj = CoverageReport.CoverBin( + ci_n.getName(), cvg_data.at_least, cvg_data.data) + if ct == CoverTypeT.CVGBIN: + cp_r.bins.append(bin_obj) + total += 1 + if cvg_data.data >= cvg_data.at_least: + num_hit += 1 + elif ct == CoverTypeT.IGNOREBIN: + cp_r.ignore_bins.append(bin_obj) + elif ct == CoverTypeT.ILLEGALBIN: + cp_r.illegal_bins.append(bin_obj) if total > 0: cp_r.coverage = (100*num_hit)/total @@ -161,4 +177,4 @@ def build_cross(self, cr_n : Cross): - \ No newline at end of file + diff --git a/src/ucis/scope_type_t.py b/src/ucis/scope_type_t.py index 418f2b6..be4134e 100644 --- a/src/ucis/scope_type_t.py +++ b/src/ucis/scope_type_t.py @@ -190,6 +190,6 @@ def DU_ANY(t): >>> if ScopeTypeT.DU_ANY(scope.getType()): ... print("This is a design unit") """ - return (t & (ScopeTypeT.DU_MODULE|ScopeTypeT.DU_ARCH|ScopeTypeT.DU_PACKAGE|ScopeTypeT.DU_PROGRAM|ScopeTypeT.DU_INTERFACE)) != 0 + return (int(t) & 0x000000001F000000) != 0 - \ No newline at end of file + From 48317a294eeccfd2ad8c1b5d3b88ce6062e557f8 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Mon, 2 Mar 2026 01:43:26 +0000 Subject: [PATCH 4/4] report-builder fix Signed-off-by: Matthew Ballance --- src/ucis/report/coverage_report_builder.py | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/ucis/report/coverage_report_builder.py b/src/ucis/report/coverage_report_builder.py index e0b0103..5fb0e6f 100644 --- a/src/ucis/report/coverage_report_builder.py +++ b/src/ucis/report/coverage_report_builder.py @@ -77,23 +77,27 @@ def build_covergroup(self, cg_n)->CoverageReport.Covergroup: for cg_in in cg_n.scopes(ScopeTypeT.COVERINSTANCE): cg_r.covergroups.append(self.build_covergroup(cg_in)) - # Determine the covergroup coverage + # Determine the covergroup coverage. + # If the covergroup has type-level coverpoints/crosses (the aggregate + # view across all instances), use those. Otherwise fall back to the + # average of sub-instances. coverage = 0.0 - div = 0 - for cp in cg_r.coverpoints: - if cp.weight > 0: - coverage += cp.coverage * cp.weight - div += cp.weight - - for cr in cg_r.crosses: - coverage += cr.coverage * cr.weight - div += cr.weight - - for sub in cg_r.covergroups: - if sub.weight > 0: - coverage += sub.coverage * sub.weight - div += sub.weight + + if cg_r.coverpoints or cg_r.crosses: + for cp in cg_r.coverpoints: + if cp.weight > 0: + coverage += cp.coverage * cp.weight + div += cp.weight + + for cr in cg_r.crosses: + coverage += cr.coverage * cr.weight + div += cr.weight + else: + for sub in cg_r.covergroups: + if sub.weight > 0: + coverage += sub.coverage * sub.weight + div += sub.weight if div > 0: coverage /= div