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.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_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_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/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/_accel/_ncdb_accel.o b/src/ucis/ncdb/_accel/_ncdb_accel.o index 51ffa7e..301c7e5 100644 Binary files a/src/ucis/ncdb/_accel/_ncdb_accel.o and b/src/ucis/ncdb/_accel/_ncdb_accel.o differ 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..16289ab 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 ───────────────────────────────────────────── @@ -65,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 ──────────────────────── @@ -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..b38bd73 --- /dev/null +++ b/src/ucis/ncdb/coveritem_flags.py @@ -0,0 +1,93 @@ +""" +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 +from .constants import COVER_TYPE_DEFAULTS + +_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() + 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 + + 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 + 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/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 83d8873..c335350 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 @@ -42,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) @@ -84,6 +88,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 +122,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 +142,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 +196,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..51764ac 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)) @@ -252,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) @@ -283,11 +297,18 @@ 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) child_cover_type = None + at_least = 0 if num_coveritems > 0: ctv, offset = decode_varint(data, offset) child_cover_type = CoverTypeT(ctv) @@ -295,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 @@ -315,16 +340,22 @@ 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) 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): - 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..5fb0e6f 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(), @@ -73,19 +77,28 @@ 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 - + + 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 cg_r.coverage = coverage @@ -96,37 +109,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 +181,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 + 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