test_parser.py 5.29 KB
Newer Older
one's avatar
one committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Tests for hytop.gpu.parser."""

from __future__ import annotations

import json

import pytest

from hytop.gpu.parser import parse_hy_smi_output, parse_number, strip_ansi

# ---------------------------------------------------------------------------
# Real hy-smi JSON fixture (from actual 8-card Hygon DCU node)
# ---------------------------------------------------------------------------

# Full-flag output (--showtemp --showpower --showhcuclocks --showmemuse --showuse --json)
# Representative cards: card0 (idle) and card7 (100% HCU load)
HY_SMI_FULL = {
    "card0": {
        "Average Graphics Package Power (W)": "157.0",
        "Temperature (Sensor edge) (C)": "31.0",
        "Temperature (Sensor junction) (C)": "34.0",
        "Temperature (Sensor mem) (C)": "28.0",
        "Temperature (Sensor core) (C)": "30.0",
        "HCU use (%)": "0.0",
        "HCU memory use (%)": "89",
        "sclk clock level": "10",
        "sclk clock speed": "1500Mhz",
    },
    "card7": {
        "Average Graphics Package Power (W)": "141.0",
        "Temperature (Sensor edge) (C)": "28.0",
        "Temperature (Sensor junction) (C)": "33.0",
        "Temperature (Sensor mem) (C)": "25.0",
        "Temperature (Sensor core) (C)": "25.0",
        "HCU use (%)": "100.0",
        "HCU memory use (%)": "89",
        "sclk clock level": "10",
        "sclk clock speed": "1500Mhz",
    },
}

# Temp-only output (--showtemp --json): extra sensor keys should be ignored
HY_SMI_TEMP_ONLY = {
    "card0": {
        "Temperature (Sensor edge) (C)": "28.0",
        "Temperature (Sensor junction) (C)": "31.0",
        "Temperature (Sensor mem) (C)": "25.0",
        "Temperature (Sensor core) (C)": "26.0",
    },
}


# ---------------------------------------------------------------------------
# strip_ansi
# ---------------------------------------------------------------------------


class TestStripAnsi:
    def test_plain_text_unchanged(self):
        assert strip_ansi("hello") == "hello"

    def test_color_codes_removed(self):
        assert strip_ansi("\x1b[31mred\x1b[0m") == "red"

    def test_empty_string(self):
        assert strip_ansi("") == ""

    def test_multiple_codes(self):
        assert strip_ansi("\x1b[1m\x1b[4mbold\x1b[0m") == "bold"


# ---------------------------------------------------------------------------
# parse_number
# ---------------------------------------------------------------------------


class TestParseNumber:
    def test_integer_string(self):
        assert parse_number("89") == pytest.approx(89.0)

    def test_float_string(self):
        assert parse_number("157.0") == pytest.approx(157.0)

    def test_value_with_unit_suffix(self):
        # "1500Mhz" — real sclk clock speed format from hy-smi
        assert parse_number("1500Mhz") == pytest.approx(1500.0)

    def test_no_number_raises(self):
        with pytest.raises(ValueError, match="cannot parse"):
            parse_number("N/A")

    def test_negative_number(self):
        assert parse_number("-5.5") == pytest.approx(-5.5)


# ---------------------------------------------------------------------------
# parse_hy_smi_output — with real fixture data
# ---------------------------------------------------------------------------


class TestParseHySmiOutput:
    def test_full_output_card_count(self):
        raw = json.dumps(HY_SMI_FULL)
        result = parse_hy_smi_output(raw, sample_ts=1.0)
        assert set(result.keys()) == {0, 7}

    def test_full_output_card0_metrics(self):
        raw = json.dumps(HY_SMI_FULL)
        result = parse_hy_smi_output(raw, sample_ts=1.0)
        s = result[0]
        assert s.temp_c == pytest.approx(30.0)
        assert s.avg_pwr_w == pytest.approx(157.0)
        assert s.hcu_pct == pytest.approx(0.0)
        assert s.vram_pct == pytest.approx(89.0)  # integer string "89" → 89.0
        assert s.sclk_mhz == pytest.approx(1500.0)  # "1500Mhz" → 1500.0

    def test_full_output_card7_hcu_load(self):
        raw = json.dumps(HY_SMI_FULL)
        result = parse_hy_smi_output(raw, sample_ts=1.0)
        assert result[7].hcu_pct == pytest.approx(100.0)

    def test_temp_only_output(self):
        raw = json.dumps(HY_SMI_TEMP_ONLY)
        result = parse_hy_smi_output(raw, sample_ts=1.0)
        s = result[0]
        assert s.temp_c == pytest.approx(26.0)
        # Unrelated sensor keys must not populate fields
        assert s.avg_pwr_w is None
        assert s.hcu_pct is None

    def test_sample_ts_propagated(self):
        raw = json.dumps(HY_SMI_FULL)
        result = parse_hy_smi_output(raw, sample_ts=42.5)
        assert result[0].ts == pytest.approx(42.5)

    def test_unknown_card_keys_ignored(self):
        payload = {"sys_info": {"foo": "bar"}, "card0": HY_SMI_FULL["card0"]}
        result = parse_hy_smi_output(json.dumps(payload), sample_ts=1.0)
        assert list(result.keys()) == [0]

    def test_empty_string_returns_empty(self):
        assert parse_hy_smi_output("", sample_ts=1.0) == {}

    def test_invalid_json_returns_empty(self):
        assert parse_hy_smi_output("not json", sample_ts=1.0) == {}

    def test_ansi_stripped_before_parse(self):
        # Some hy-smi versions emit ANSI colors; parser must strip them first
        raw_with_ansi = "\x1b[0m" + json.dumps(HY_SMI_TEMP_ONLY) + "\x1b[0m"
        result = parse_hy_smi_output(raw_with_ansi, sample_ts=1.0)
        assert 0 in result