Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **SCEPTRE App**: The `reg_config` manual register-map path. It was dead code: the app never populated it, so every device always took the automatic numbering branch. Output is unchanged.

### Fixed
- **Common Utils**: fix cc commands parsing to be resilient to header changes.
- **SCEPTRE App**: With more than one PowerWorld provider, only the last one's `hil_tags` reached the object list, and only the last one's `objects.txt` was written. Tags now aggregate across providers and every PowerWorld provider gets the combined `objects.txt`.
- **SCEPTRE App**: Every `sunspec` inverter raised `KeyError`: the SunSpec register mappings are keyed `PowerDistribution` but received `power-distribution`. The whole protocol was unusable.
- **SCEPTRE App**: `fep` hosts raised `TypeError`, built without the required `device_subtype`.
Expand Down Expand Up @@ -80,4 +81,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.2.0]

- Initial release with various applications including `sceptre`, `helics`, and `ot-sim`.
- Initial release with various applications including `sceptre`, `helics`, and `ot-sim`.
66 changes: 48 additions & 18 deletions src/python/phenix_apps/common/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,16 +447,34 @@ def test_mm_vm_uuid_returns_none_when_header_lacks_name_or_uuid():
assert utils.mm_vm_uuid(mm, "foo") is None


# `cc commands` per-host rows: [id, prefix, command, responses, ...].
def _cc_cmd_row(host="gibson1336", **cols):
# `cc commands` header, from minimega cmd/minimega/cc_cli.go (cliCCCommand).
_CC_CMD_HEADER = (
"id",
"prefix",
"command",
"responses",
"issued",
"background",
"once",
"sent",
"received",
"connectivity",
"level",
"filter",
)
# Before minimega 3.3.0 there was no `issued` column.
_CC_CMD_HEADER_LEGACY = tuple(c for c in _CC_CMD_HEADER if c != "issued")


def _cc_cmd_row(host="gibson1336", header=_CC_CMD_HEADER, **cols):
"""One `cc commands` per-host response. Pass any subset of columns by name."""
values = {"id": "7", "prefix": "", "command": "[whoami]", "responses": "0"}
values.update(cols)

return {
"Host": host,
"Header": list(utils.CC_CMD_COLUMNS),
"Tabular": [[values.get(col, "") for col in utils.CC_CMD_COLUMNS]],
"Header": list(header),
"Tabular": [[values.get(col, "") for col in header]],
"Error": "",
}

Expand Down Expand Up @@ -730,18 +748,30 @@ def test_mm_wait_for_prefix_ignores_other_prefixes(monkeypatch):
assert clock.now >= 5.0


def test_cc_cmd_columns_match_minimega_cc_cli():
# Source of truth: cmd/minimega/cc_cli.go, cliCCCommand's resp.Header.
assert utils.CC_CMD_COLUMNS == (
"id",
"prefix",
"command",
"responses",
"background",
"once",
"sent",
"received",
"connectivity",
"level",
"filter",
@pytest.mark.parametrize("header", [_CC_CMD_HEADER, _CC_CMD_HEADER_LEGACY])
def test_mm_wait_for_cmd_reads_columns_by_header(monkeypatch, header):
# Columns are located by name, so a minimega that adds or reorders
# columns must not break the content match.
_install_fake_clock(monkeypatch)
row = _cc_cmd_row(
"gibson1337", header=header, id="20", responses="1", sent="[exp/ours.sh]"
)
mm = _StubMM(cc_commands_rows=[row])

utils.mm_wait_for_cmd(
mm,
"20",
host="gibson1337",
match_column="sent",
match_value="[exp/ours.sh]",
appear_grace=30.0,
)


@pytest.mark.parametrize("header", [_CC_CMD_HEADER, _CC_CMD_HEADER_LEGACY])
def test_mm_wait_for_prefix_reads_columns_by_header(header):
mm = _StubMM(
cc_commands_rows=[_cc_cmd_row(header=header, prefix="testing", responses="2")]
)

utils.mm_wait_for_prefix(mm, "testing", 2, poll_rate=0.0)
48 changes: 14 additions & 34 deletions src/python/phenix_apps/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,27 +235,15 @@ def expand_shorthand(short: str) -> list:
return [short]


# `cc commands` columns, in order (minimega cmd/minimega/cc_cli.go)
CC_CMD_COLUMNS = (
"id",
"prefix",
"command",
"responses",
"background",
"once",
"sent",
"received",
"connectivity",
"level",
"filter",
)
def _cc_cmd_rows(resp: dict) -> list[dict[str, str]]:
"""Rows of one host's ``cc commands`` response, keyed by its own header.

Column order varies across minimega versions, so never index by position.
A truncated row simply lacks the missing keys.
"""
header = resp.get("Header") or []

def _cc_cmd_field(row: list, column: str) -> str | None:
"""Value of ``column`` in a ``cc commands`` row, or None if truncated."""
idx = CC_CMD_COLUMNS.index(column)

return row[idx] if idx < len(row) else None
return [dict(zip(header, row, strict=False)) for row in resp.get("Tabular") or []]


class _WaitLog:
Expand Down Expand Up @@ -768,27 +756,21 @@ def mm_wait_for_cmd(
seen = host is None

while True:
# >>> mm.cc_commands()
# 'Header': ['id', 'prefix', 'command', 'responses', 'background', 'once', 'sent', 'received', 'connectivity', 'level', 'filter']
# 'Tabular': [['1', 'testing', '[/usr/bin/iperf3 --version]', '15', 'false', 'true', '[]', '[]', '', '', 'os=linux && iperf=1']]
for resp in mm_cc_all_hosts(mm, mm.cc_commands):
if host is not None and resp.get("Host") != host:
continue

for row in resp.get("Tabular") or []:
if not row or row[0] != cmd_id:
for row in _cc_cmd_rows(resp):
if row.get("id") != cmd_id:
continue

# Same id, different payload: not ours.
if (
match_value is not None
and _cc_cmd_field(row, match_column) != match_value
):
if match_value is not None and row.get(match_column) != match_value:
continue

seen = True

if int(_cc_cmd_field(row, "responses") or 0) > 0:
if int(row.get("responses") or 0) > 0:
return

now = time.monotonic()
Expand Down Expand Up @@ -849,16 +831,14 @@ def mm_wait_for_prefix(
wait_log = _WaitLog()

while True:
# 'Header': ['id', 'prefix', 'command', 'responses', 'background', 'once', 'sent', 'received', 'connectivity', 'level', 'filter']
# 'Tabular': [['1', 'testing', '[/usr/bin/iperf3 --version]', '15', 'false', 'true', '[]', '[]', '', '', 'os=linux && iperf=1']]
seen = 0

for resp in mm_cc_all_hosts(mm, mm.cc_commands):
for row in resp.get("Tabular") or []:
if _cc_cmd_field(row, "prefix") != prefix:
for row in _cc_cmd_rows(resp):
if row.get("prefix") != prefix:
continue

seen += int(_cc_cmd_field(row, "responses") or 0)
seen += int(row.get("responses") or 0)

if seen >= num_responses:
return
Expand Down
Loading