Skip to content

Commit 20d5fc6

Browse files
Merge pull request #74 from amd/alex_build_from_model
--gen-reference-config + --plugin-config functional test + bug fix
2 parents 1bc0aad + 4b8ae98 commit 20d5fc6

File tree

7 files changed

+586
-3
lines changed

7 files changed

+586
-3
lines changed

nodescraper/plugins/inband/kernel/analyzer_args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@ def build_from_model(cls, datamodel: KernelDataModel) -> "KernelAnalyzerArgs":
6161
Returns:
6262
KernelAnalyzerArgs: instance of analyzer args class
6363
"""
64-
return cls(exp_kernel=datamodel.kernel_info)
64+
return cls(exp_kernel=datamodel.kernel_version)

nodescraper/plugins/inband/memory/analyzer_args.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,21 @@
2525
###############################################################################
2626
from nodescraper.models.analyzerargs import AnalyzerArgs
2727

28+
from .memorydata import MemoryDataModel
29+
2830

2931
class MemoryAnalyzerArgs(AnalyzerArgs):
3032
ratio: float = 0.66
3133
memory_threshold: str = "30Gi"
34+
35+
@classmethod
36+
def build_from_model(cls, datamodel: MemoryDataModel) -> "MemoryAnalyzerArgs":
37+
"""build analyzer args from data model
38+
39+
Args:
40+
datamodel (MemoryDataModel): data model for plugin
41+
42+
Returns:
43+
MemoryAnalyzerArgs: instance of analyzer args class
44+
"""
45+
return cls(memory_threshold=datamodel.mem_total)

nodescraper/plugins/inband/memory/memory_plugin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ class MemoryPlugin(InBandDataPlugin[MemoryDataModel, None, MemoryAnalyzerArgs]):
3939
COLLECTOR = MemoryCollector
4040

4141
ANALYZER = MemoryAnalyzer
42+
43+
ANALYZER_ARGS = MemoryAnalyzerArgs

nodescraper/plugins/inband/os/analyzer_args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@ def build_from_model(cls, datamodel: OsDataModel) -> "OsAnalyzerArgs":
6161
Returns:
6262
OsAnalyzerArgs: instance of analyzer args class
6363
"""
64-
return cls(exp_os=datamodel.os_name)
64+
return cls(exp_os=datamodel.os_name, exact_match=True)

nodescraper/plugins/inband/rocm/analyzer_args.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,6 @@ def build_from_model(cls, datamodel: RocmDataModel) -> "RocmAnalyzerArgs":
6161
Returns:
6262
RocmAnalyzerArgs: instance of analyzer args class
6363
"""
64-
return cls(exp_rocm=datamodel.rocm_version)
64+
return cls(
65+
exp_rocm=datamodel.rocm_version, exp_rocm_latest=datamodel.rocm_latest_versioned_path
66+
)
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
###############################################################################
2+
#
3+
# MIT License
4+
#
5+
# Copyright (c) 2025 Advanced Micro Devices, Inc.
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
#
25+
###############################################################################
26+
"""
27+
Functional tests for reference config generation and usage workflow.
28+
29+
Tests the complete workflow:
30+
1. Generate reference config from system using --gen-reference-config
31+
2. Use the generated config with --plugin-configs
32+
"""
33+
import json
34+
from pathlib import Path
35+
36+
import pytest
37+
38+
from nodescraper.pluginregistry import PluginRegistry
39+
40+
41+
def find_reference_config(log_path):
42+
"""Find reference_config.json in timestamped log directory.
43+
44+
Args:
45+
log_path: Base log path where logs are stored
46+
47+
Returns:
48+
Path to reference_config.json or None if not found
49+
"""
50+
log_path = Path(log_path)
51+
if not log_path.exists():
52+
return None
53+
54+
log_dirs = list(log_path.glob("scraper_logs_*"))
55+
if not log_dirs:
56+
return None
57+
58+
most_recent = max(log_dirs, key=lambda p: p.stat().st_mtime)
59+
60+
reference_config = most_recent / "reference_config.json"
61+
if reference_config.exists():
62+
return reference_config
63+
64+
return None
65+
66+
67+
@pytest.fixture(scope="module")
68+
def all_plugin_names():
69+
"""Get list of all available plugin names."""
70+
registry = PluginRegistry()
71+
return sorted(registry.plugins.keys())
72+
73+
74+
def test_gen_reference_config_all_plugins(run_cli_command, tmp_path, all_plugin_names):
75+
"""Test generating reference config with all plugins via run-plugins subcommand.
76+
77+
Note: When running all plugins, some may fail but as long as at least one succeeds,
78+
the reference config should be generated.
79+
"""
80+
log_path = str(tmp_path / "logs_gen_ref_all")
81+
82+
result = run_cli_command(
83+
[
84+
"--log-path",
85+
log_path,
86+
"--gen-reference-config",
87+
"run-plugins",
88+
]
89+
+ all_plugin_names,
90+
check=False,
91+
)
92+
93+
assert result.returncode in [0, 1, 2, 120], (
94+
f"Unexpected return code: {result.returncode}\n"
95+
f"stdout: {result.stdout[:500]}\nstderr: {result.stderr[:500]}"
96+
)
97+
98+
reference_config_path = find_reference_config(log_path)
99+
100+
if reference_config_path is None:
101+
pytest.skip(
102+
"reference_config.json was not created - likely all plugins failed or timed out. "
103+
"This can happen in test environments."
104+
)
105+
106+
assert reference_config_path.exists()
107+
108+
with open(reference_config_path) as f:
109+
config = json.load(f)
110+
assert "plugins" in config
111+
assert isinstance(config["plugins"], dict)
112+
assert len(config["plugins"]) > 0
113+
114+
115+
def test_gen_reference_config_subset_plugins(run_cli_command, tmp_path):
116+
"""Test generating reference config with a subset of plugins."""
117+
log_path = str(tmp_path / "logs_gen_ref_subset")
118+
plugins = ["BiosPlugin", "OsPlugin", "KernelPlugin"]
119+
120+
result = run_cli_command(
121+
["--log-path", log_path, "--gen-reference-config", "run-plugins"] + plugins,
122+
check=False,
123+
)
124+
125+
assert result.returncode in [0, 1, 2]
126+
127+
reference_config_path = find_reference_config(log_path)
128+
assert reference_config_path is not None, "reference_config.json was not created"
129+
assert reference_config_path.exists()
130+
131+
with open(reference_config_path) as f:
132+
config = json.load(f)
133+
assert "plugins" in config
134+
135+
136+
def test_use_generated_reference_config(run_cli_command, tmp_path):
137+
"""Test using a generated reference config with --plugin-configs."""
138+
gen_log_path = str(tmp_path / "logs_gen")
139+
use_log_path = str(tmp_path / "logs_use")
140+
141+
plugins = ["BiosPlugin", "OsPlugin", "UptimePlugin"]
142+
143+
gen_result = run_cli_command(
144+
["--log-path", gen_log_path, "--gen-reference-config", "run-plugins"] + plugins,
145+
check=False,
146+
)
147+
148+
assert gen_result.returncode in [0, 1, 2]
149+
150+
reference_config_path = find_reference_config(gen_log_path)
151+
assert reference_config_path is not None, "reference_config.json was not created"
152+
assert reference_config_path.exists()
153+
154+
use_result = run_cli_command(
155+
["--log-path", use_log_path, "--plugin-configs", str(reference_config_path)],
156+
check=False,
157+
)
158+
159+
assert use_result.returncode in [0, 1, 2]
160+
output = use_result.stdout + use_result.stderr
161+
assert len(output) > 0
162+
163+
164+
def test_full_workflow_all_plugins(run_cli_command, tmp_path, all_plugin_names):
165+
"""
166+
Test complete workflow: generate reference config from all plugins,
167+
then use it with --plugin-configs.
168+
169+
Note: May skip if plugins fail to generate config in test environment.
170+
"""
171+
gen_log_path = str(tmp_path / "logs_gen_workflow")
172+
use_log_path = str(tmp_path / "logs_use_workflow")
173+
174+
gen_result = run_cli_command(
175+
[
176+
"--log-path",
177+
gen_log_path,
178+
"--gen-reference-config",
179+
"run-plugins",
180+
]
181+
+ all_plugin_names,
182+
check=False,
183+
)
184+
185+
assert gen_result.returncode in [0, 1, 2, 120], (
186+
f"Generation failed with return code {gen_result.returncode}\n"
187+
f"stdout: {gen_result.stdout[:500]}\n"
188+
f"stderr: {gen_result.stderr[:500]}"
189+
)
190+
191+
reference_config_path = find_reference_config(gen_log_path)
192+
193+
if reference_config_path is None:
194+
pytest.skip(
195+
"reference_config.json was not generated - plugins may have failed in test environment"
196+
)
197+
198+
assert reference_config_path.exists()
199+
200+
with open(reference_config_path) as f:
201+
config = json.load(f)
202+
assert "plugins" in config, "Config missing 'plugins' key"
203+
204+
for _plugin_name, plugin_config in config["plugins"].items():
205+
if "analysis_args" in plugin_config:
206+
assert isinstance(plugin_config["analysis_args"], dict)
207+
208+
use_result = run_cli_command(
209+
["--log-path", use_log_path, "--plugin-configs", str(reference_config_path)],
210+
check=False,
211+
)
212+
213+
assert use_result.returncode in [0, 1, 2], (
214+
f"Using config failed with return code {use_result.returncode}\n"
215+
f"stdout: {use_result.stdout}\n"
216+
f"stderr: {use_result.stderr}"
217+
)
218+
219+
output = use_result.stdout + use_result.stderr
220+
assert len(output) > 0, "No output generated when using reference config"
221+
222+
use_log_dirs = list(Path(tmp_path).glob("logs_use_workflow*"))
223+
assert len(use_log_dirs) > 0, "No log directory created when using config"
224+
225+
226+
def test_reference_config_with_analysis_args(run_cli_command, tmp_path):
227+
"""Test that generated reference config includes analysis_args where available."""
228+
log_path = str(tmp_path / "logs_analysis_args")
229+
230+
plugins_with_build_from_model = [
231+
"BiosPlugin",
232+
"CmdlinePlugin",
233+
"DeviceEnumerationPlugin",
234+
"DkmsPlugin",
235+
"KernelPlugin",
236+
"KernelModulePlugin",
237+
"MemoryPlugin",
238+
"OsPlugin",
239+
"PackagePlugin",
240+
"ProcessPlugin",
241+
"RocmPlugin",
242+
"SysctlPlugin",
243+
]
244+
245+
result = run_cli_command(
246+
["--log-path", log_path, "--gen-reference-config", "run-plugins"]
247+
+ plugins_with_build_from_model,
248+
check=False,
249+
)
250+
251+
assert result.returncode in [0, 1, 2, 120]
252+
253+
reference_config_path = find_reference_config(log_path)
254+
255+
if reference_config_path is None:
256+
pytest.skip(
257+
"reference_config.json was not created - plugins may have failed in test environment"
258+
)
259+
260+
assert reference_config_path.exists()
261+
262+
with open(reference_config_path) as f:
263+
config = json.load(f)
264+
plugins_with_args = [
265+
name for name, conf in config["plugins"].items() if "analysis_args" in conf
266+
]
267+
assert len(plugins_with_args) > 0, "No plugins have analysis_args in generated config"
268+
269+
270+
def test_reference_config_structure(run_cli_command, tmp_path):
271+
"""Test that generated reference config has correct structure."""
272+
log_path = str(tmp_path / "logs_structure")
273+
274+
result = run_cli_command(
275+
["--log-path", log_path, "--gen-reference-config", "run-plugins", "OsPlugin"],
276+
check=False,
277+
)
278+
279+
assert result.returncode in [0, 1, 2]
280+
281+
reference_config_path = find_reference_config(log_path)
282+
assert reference_config_path is not None, "reference_config.json was not created"
283+
assert reference_config_path.exists()
284+
285+
with open(reference_config_path) as f:
286+
config = json.load(f)
287+
288+
assert "plugins" in config
289+
assert isinstance(config["plugins"], dict)
290+
291+
if "OsPlugin" in config["plugins"]:
292+
os_config = config["plugins"]["OsPlugin"]
293+
if "analysis_args" in os_config:
294+
assert "exp_os" in os_config["analysis_args"]
295+
296+
297+
def test_gen_reference_config_without_run_plugins(run_cli_command, tmp_path):
298+
"""Test generating reference config without specifying plugins (uses default)."""
299+
log_path = str(tmp_path / "logs_default")
300+
301+
result = run_cli_command(
302+
["--log-path", log_path, "--gen-reference-config"],
303+
check=False,
304+
)
305+
306+
assert result.returncode in [0, 1, 2]
307+
308+
reference_config_path = find_reference_config(log_path)
309+
assert reference_config_path is not None, "reference_config.json was not created"
310+
assert reference_config_path.exists()
311+
312+
with open(reference_config_path) as f:
313+
config = json.load(f)
314+
assert "plugins" in config
315+
316+
317+
def test_reference_config_json_valid(run_cli_command, tmp_path):
318+
"""Test that generated reference config is valid JSON."""
319+
log_path = str(tmp_path / "logs_valid_json")
320+
321+
result = run_cli_command(
322+
[
323+
"--log-path",
324+
log_path,
325+
"--gen-reference-config",
326+
"run-plugins",
327+
"BiosPlugin",
328+
"OsPlugin",
329+
],
330+
check=False,
331+
)
332+
333+
assert result.returncode in [0, 1, 2]
334+
335+
reference_config_path = find_reference_config(log_path)
336+
assert reference_config_path is not None, "reference_config.json was not created"
337+
assert reference_config_path.exists()
338+
339+
with open(reference_config_path) as f:
340+
config = json.load(f)
341+
json_str = json.dumps(config, indent=2)
342+
assert len(json_str) > 0
343+
344+
reparsed = json.loads(json_str)
345+
assert reparsed == config

0 commit comments

Comments
 (0)