682ae2d014
* Remove L2 channels from --list-metrics --list-metrics moved to general options List metrics for the current architecture Filter blocks for metrics Removed test for --list-metrics in profile mode Test the options don't throw error Fixed --config-dir error Test stdout for command line options Provide path list for loading panel configs Show L2 Cache (per) channel metrics Changed command line option names Can show two levels only Removed filtering blocks Moved blocks to original position Removed filter block tests Removed filtering Formaating fix Readability enhancement Test formatting Filter L2 channels without sysinfo Show avilable metrics for current arch Intermediate commit Fixed tests Added argument sanitization Added list_metrics to ctest merge iconflict resolution Updated test marker Updated changelog Fixed formatting * Updated docs
9378 líneas
292 KiB
Python
9378 líneas
292 KiB
Python
##############################################################################
|
||
# MIT License
|
||
#
|
||
# Copyright (c) 2021 - 2025 Advanced Micro Devices, Inc. All Rights Reserved.
|
||
#
|
||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
# of this software and associated documentation files (the "Software"), to deal
|
||
# in the Software without restriction, including without limitation the rights
|
||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
# copies of the Software, and to permit persons to whom the Software is
|
||
# furnished to do so, subject to the following conditions:
|
||
#
|
||
# The above copyright notice and this permission notice shall be included in
|
||
# all copies or substantial portions of the Software.
|
||
#
|
||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
# THE SOFTWARE.
|
||
|
||
##############################################################################
|
||
|
||
import builtins
|
||
import inspect
|
||
import io
|
||
import json
|
||
import locale
|
||
import logging
|
||
import os
|
||
import pathlib
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import tempfile
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
from unittest import mock
|
||
|
||
import pandas as pd
|
||
import pytest
|
||
|
||
import utils.utils as utils
|
||
|
||
logging.trace = lambda *args, **kwargs: None
|
||
|
||
##################################################
|
||
## Generated tests ##
|
||
##################################################
|
||
|
||
# =============================================================================
|
||
# HELPER FUNCTIONS FOR TESTING
|
||
# =============================================================================
|
||
|
||
|
||
def check_resource_allocation():
|
||
"""Check if CTEST resource allocation is enabled for parallel testing and set
|
||
HIP_VISIBLE_DEVICES variable accordingly with assigned gpu index.
|
||
"""
|
||
|
||
if "CTEST_RESOURCE_GROUP_COUNT" not in os.environ:
|
||
return
|
||
|
||
if "CTEST_RESOURCE_GROUP_0_GPUS" in os.environ:
|
||
resource = os.environ["CTEST_RESOURCE_GROUP_0_GPUS"]
|
||
# extract assigned gpu id from env var: example format -> 'id:0,slots:1'
|
||
for item in resource.split(","):
|
||
key, value = item.split(":")
|
||
if key == "id":
|
||
os.environ["HIP_VISIBLE_DEVICES"] = value
|
||
return
|
||
|
||
return
|
||
|
||
|
||
def check_file_pattern(pattern, file_path):
|
||
"""Check if the given pattern exists in the file"""
|
||
content = ""
|
||
with open(file_path) as f:
|
||
content = f.read()
|
||
return len(re.findall(pattern, content)) != 0
|
||
|
||
|
||
def get_output_dir(suffix="_output", clean_existing=True):
|
||
"""
|
||
Provides a unique output directory based on the name of the calling test function
|
||
with a suffix applied.
|
||
|
||
Args:
|
||
suffix (str, optional): suffix to append to output_dir.
|
||
Defaults to "_output".
|
||
clean_existing (bool, optional): Whether to remove existing directory if exists.
|
||
Defaults to True.
|
||
"""
|
||
|
||
output_dir = inspect.stack()[1].function + suffix
|
||
if clean_existing:
|
||
if Path(output_dir).exists():
|
||
shutil.rmtree(output_dir)
|
||
return output_dir
|
||
|
||
|
||
def setup_workload_dir(input_dir, suffix="_tmp", clean_existing=True):
|
||
"""Provides a unique input workoad directory with contents of input_dir
|
||
based on the name of the calling test function.
|
||
|
||
Setup is a NOOP when tests run serially.
|
||
"""
|
||
|
||
if "PYTEST_XDIST_WORKER_COUNT" not in os.environ:
|
||
return input_dir
|
||
|
||
output_dir = inspect.stack()[1].function + suffix
|
||
if clean_existing:
|
||
if Path(output_dir).exists():
|
||
shutil.rmtree(output_dir)
|
||
|
||
shutil.copytree(input_dir, output_dir)
|
||
return output_dir
|
||
|
||
|
||
def clean_output_dir(cleanup, output_dir):
|
||
"""Remove output directory generated from rocprofiler-compute execution
|
||
|
||
Args:
|
||
cleanup (boolean): flag to enable/disable directory cleanup
|
||
output_dir (string): name of directory to remove
|
||
"""
|
||
if cleanup:
|
||
if Path(output_dir).exists():
|
||
try:
|
||
shutil.rmtree(output_dir)
|
||
except OSError:
|
||
print(
|
||
"WARNING: shutil.rmdir(output_dir): directory may not be empty..."
|
||
)
|
||
return
|
||
|
||
|
||
def check_csv_files(output_dir, num_devices, num_kernels):
|
||
"""Check profiling output csv files for expected
|
||
number of entries (based on kernel invocations)
|
||
|
||
Args:
|
||
output_dir (string): output directory containing csv files
|
||
num_kernels (int): number of kernels expected to have been profiled
|
||
|
||
Returns:
|
||
dict: dictionary housing file contents as pandas dataframe
|
||
"""
|
||
|
||
file_dict = {}
|
||
files_in_workload = os.listdir(output_dir)
|
||
for file in files_in_workload:
|
||
if file.endswith(".csv"):
|
||
file_dict[file] = pd.read_csv(output_dir + "/" + file)
|
||
if "roofline" in file:
|
||
assert len(file_dict[file].index) >= num_devices
|
||
elif not "sysinfo" in file:
|
||
assert len(file_dict[file].index) >= num_kernels
|
||
elif file.endswith(".pdf"):
|
||
file_dict[file] = "pdf"
|
||
elif file.endswith(".json"):
|
||
file_dict[file] = "json"
|
||
return file_dict
|
||
|
||
|
||
def get_num_pmc_file(output_dir):
|
||
"""
|
||
Returns:
|
||
int: number of pmc perf text files in perfmon dir
|
||
"""
|
||
|
||
perfmon_path = Path(output_dir) / "perfmon"
|
||
return len([
|
||
f for f in perfmon_path.iterdir() if f.is_file() and f.suffix == ".txt"
|
||
])
|
||
|
||
|
||
# =============================================================================
|
||
# VERSION UTILITIES TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_get_version_finds_version_in_home(tmp_path, monkeypatch):
|
||
"""Test that get_version correctly reads version and SHA from a VERSION file in the
|
||
given directory.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path provided by pytest for test isolation.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture to modify or simulate behavior
|
||
of modules/functions.
|
||
|
||
Returns:
|
||
None: Asserts correctness of version, SHA, and mode returned by get_version.
|
||
"""
|
||
version_content = "1.2.3"
|
||
version_file = tmp_path / "VERSION"
|
||
version_file.write_text(version_content)
|
||
monkeypatch.setattr(
|
||
utils, "capture_subprocess_output", lambda *a, **k: (True, "abc123")
|
||
)
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"console_error",
|
||
lambda *a, **k: pytest.fail("console_error should not be called"),
|
||
)
|
||
result = utils.get_version(tmp_path)
|
||
assert result["version"] == version_content
|
||
assert result["sha"] == "abc123"
|
||
assert result["mode"] == "dev"
|
||
|
||
|
||
def test_get_version_finds_version_in_parent(tmp_path, monkeypatch):
|
||
"""
|
||
Test that get_version finds VERSION file in a parent directory when not present
|
||
in the given directory.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path provided by pytest for test isolation.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture to modify or simulate behavior
|
||
of modules/functions.
|
||
|
||
Returns:
|
||
None: Asserts correctness of version, SHA, and mode returned by get_version.
|
||
"""
|
||
parent = tmp_path / "parent"
|
||
parent.mkdir()
|
||
version_content = "2.0.0"
|
||
version_file = parent / "VERSION"
|
||
version_file.write_text(version_content)
|
||
monkeypatch.setattr(
|
||
utils, "capture_subprocess_output", lambda *a, **k: (True, "def456")
|
||
)
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"console_error",
|
||
lambda *a, **k: pytest.fail("console_error should not be called"),
|
||
)
|
||
child = parent / "child"
|
||
child.mkdir()
|
||
result = utils.get_version(child)
|
||
assert result["version"] == version_content
|
||
assert result["sha"] == "def456"
|
||
assert result["mode"] == "dev"
|
||
|
||
|
||
def test_get_version_console_error_when_no_version(monkeypatch):
|
||
"""
|
||
Test that get_version calls console_error when no VERSION file is found in any
|
||
directory.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture to modify or simulate
|
||
behavior of modules/functions.
|
||
|
||
Returns:
|
||
None: Asserts that console_error is called with the expected message and
|
||
raises RuntimeError.
|
||
"""
|
||
fake_path = Path("/nonexistent/path")
|
||
monkeypatch.setattr(builtins, "open", mock.Mock(side_effect=FileNotFoundError))
|
||
called = {}
|
||
|
||
def fake_console_error(msg, *args, **kwargs):
|
||
called["msg"] = msg
|
||
raise RuntimeError("console_error called")
|
||
|
||
monkeypatch.setattr(utils, "console_error", fake_console_error)
|
||
monkeypatch.setattr(utils, "capture_subprocess_output", lambda *a, **k: (False, ""))
|
||
with pytest.raises(RuntimeError, match="console_error called"):
|
||
utils.get_version(fake_path)
|
||
assert "Cannot find VERSION file" in called["msg"]
|
||
|
||
|
||
def test_get_version_git_success(tmp_path, monkeypatch):
|
||
"""
|
||
Test get_version returns correct version info when git command succeeds.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts version, sha, and mode are correct.
|
||
"""
|
||
version_content = "1.0.0"
|
||
version_file = tmp_path / "VERSION"
|
||
version_file.write_text(version_content)
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "abc123")
|
||
)
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_error",
|
||
lambda *a, **k: pytest.fail("console_error should not be called"),
|
||
)
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_version(tmp_path)
|
||
assert result["version"] == version_content
|
||
assert result["sha"] == "abc123"
|
||
assert result["mode"] == "dev"
|
||
|
||
|
||
def test_get_version_git_fails_sha_file(tmp_path, monkeypatch):
|
||
"""
|
||
Test get_version returns correct version info when git fails but VERSION.sha exists.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts version, sha, and mode are correct.
|
||
"""
|
||
version_content = "2.0.0"
|
||
sha_content = "def456"
|
||
version_file = tmp_path / "VERSION"
|
||
sha_file = tmp_path / "VERSION.sha"
|
||
version_file.write_text(version_content)
|
||
sha_file.write_text(sha_content)
|
||
|
||
def fail_git(*a, **k):
|
||
return (False, "git error")
|
||
|
||
monkeypatch.setattr("utils.utils.capture_subprocess_output", fail_git)
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_error",
|
||
lambda *a, **k: pytest.fail("console_error should not be called"),
|
||
)
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_version(tmp_path)
|
||
assert result["version"] == version_content
|
||
assert result["sha"] == sha_content
|
||
assert result["mode"] == "release"
|
||
|
||
|
||
def test_get_version_git_and_sha_fail(tmp_path, monkeypatch):
|
||
"""
|
||
Test get_version returns unknown sha and mode when both git and VERSION.sha fail.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts version is correct, sha and mode are 'unknown'.
|
||
"""
|
||
version_content = "3.0.0"
|
||
version_file = tmp_path / "VERSION"
|
||
version_file.write_text(version_content)
|
||
|
||
def fail_git(*a, **k):
|
||
return (False, "git error")
|
||
|
||
monkeypatch.setattr("utils.utils.capture_subprocess_output", fail_git)
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_error",
|
||
lambda *a, **k: pytest.fail("console_error should not be called"),
|
||
)
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_version(tmp_path)
|
||
assert result["version"] == version_content
|
||
assert result["sha"] == "unknown"
|
||
assert result["mode"] == "unknown"
|
||
|
||
|
||
# =============================================================================
|
||
# ROCPROF DETECTION TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_detect_rocprof_env_rocprof_not_found(monkeypatch):
|
||
"""
|
||
Test detect_rocprof when ROCPROF is set to 'rocprof' but the binary cannot be
|
||
found. Should revert to default 'rocprof' and call console_warning, then fail
|
||
with console_error.
|
||
"""
|
||
|
||
class DummyArgs:
|
||
rocprofiler_sdk_library_path = "/fake/path"
|
||
|
||
# Set ROCPROF to 'rocprof'
|
||
monkeypatch.setenv("ROCPROF", "rocprof")
|
||
# shutil.which returns None for 'rocprof'
|
||
monkeypatch.setattr("shutil.which", lambda cmd: None)
|
||
# Track calls to console_warning and console_error
|
||
warnings = []
|
||
errors = []
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_warning", lambda msg, *a, **k: warnings.append(msg)
|
||
)
|
||
|
||
def fake_console_error(msg, *a, **k):
|
||
errors.append(msg)
|
||
raise RuntimeError("console_error called")
|
||
|
||
monkeypatch.setattr("utils.utils.console_error", fake_console_error)
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(RuntimeError, match="console_error called"):
|
||
utils_mod.detect_rocprof(DummyArgs())
|
||
assert any("Unable to resolve path to rocprofv3 binary" in w for w in warnings)
|
||
assert any(
|
||
"Please verify installation or set ROCPROF environment variable" in e
|
||
for e in errors
|
||
)
|
||
|
||
|
||
def test_detect_rocprof_env_rocprof_found(monkeypatch):
|
||
"""
|
||
Test detect_rocprof when ROCPROF is set to 'rocprof' and the binary is found.
|
||
Should resolve the path and return 'rocprof'.
|
||
"""
|
||
|
||
class DummyArgs:
|
||
rocprofiler_sdk_library_path = "/fake/path"
|
||
|
||
monkeypatch.setenv("ROCPROF", "rocprof")
|
||
# shutil.which returns a fake path for 'rocprof'
|
||
monkeypatch.setattr(
|
||
"shutil.which", lambda cmd: "/usr/bin/rocprof" if cmd == "rocprof" else None
|
||
)
|
||
# Path.resolve returns the same path for simplicity
|
||
monkeypatch.setattr("pathlib.Path.resolve", lambda self: self)
|
||
# Track debug logs
|
||
logs = []
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_debug", lambda msg, *a, **k: logs.append(str(msg))
|
||
)
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.detect_rocprof(DummyArgs())
|
||
assert result == "rocprof"
|
||
assert any(
|
||
"ROC Profiler: /usr/bin/rocprof" in log_entry
|
||
or "rocprof_cmd is rocprof" in log_entry
|
||
for log_entry in logs
|
||
)
|
||
|
||
|
||
def test_detect_rocprof_env_not_set(monkeypatch):
|
||
"""
|
||
Test detect_rocprof when ROCPROF is not set in the environment.
|
||
Should default to 'rocprofv3' and resolve its path.
|
||
"""
|
||
|
||
class DummyArgs:
|
||
rocprofiler_sdk_library_path = "/fake/path"
|
||
|
||
monkeypatch.delenv("ROCPROF", raising=False)
|
||
monkeypatch.setattr(
|
||
"shutil.which", lambda cmd: "/usr/bin/rocprofv3" if cmd == "rocprofv3" else None
|
||
)
|
||
monkeypatch.setattr("pathlib.Path.resolve", lambda self: self)
|
||
logs = []
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_debug", lambda msg, *a, **k: logs.append(str(msg))
|
||
)
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.detect_rocprof(DummyArgs())
|
||
assert result == "rocprofv3"
|
||
assert any(
|
||
"ROC Profiler: /usr/bin/rocprofv3" in log_entry
|
||
or "rocprof_cmd is rocprofv3" in log_entry
|
||
for log_entry in logs
|
||
)
|
||
|
||
|
||
def test_detect_rocprof_sdk(monkeypatch):
|
||
"""
|
||
Test detect_rocprof when ROCPROF is set
|
||
to 'rocprofiler-sdk' and the library path exists.
|
||
Should return 'rocprofiler-sdk'.
|
||
"""
|
||
|
||
class DummyArgs:
|
||
rocprofiler_sdk_library_path = "/some/sdk/path"
|
||
|
||
monkeypatch.setenv("ROCPROF", "rocprofiler-sdk")
|
||
monkeypatch.setattr("pathlib.Path.exists", lambda self: True)
|
||
logs = []
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_debug", lambda msg, *a, **k: logs.append(str(msg))
|
||
)
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.detect_rocprof(DummyArgs())
|
||
assert result == "rocprofiler-sdk"
|
||
assert any("rocprof_cmd is rocprofiler-sdk" in log_entry for log_entry in logs)
|
||
|
||
|
||
def test_capture_subprocess_output_with_new_env(monkeypatch):
|
||
"""
|
||
Test capture_subprocess_output with custom environment variables.
|
||
Verifies that new_env parameter is properly passed to subprocess.
|
||
"""
|
||
|
||
class DummyProcess:
|
||
def __init__(self):
|
||
self.stdout = type(
|
||
"MockStdout", (), {"readline": lambda: "", "fileno": lambda: 1}
|
||
)()
|
||
self._poll_count = 0
|
||
|
||
def poll(self):
|
||
if self._poll_count == 0:
|
||
self._poll_count += 1
|
||
return None
|
||
return 0
|
||
|
||
def wait(self):
|
||
return 0
|
||
|
||
dummy_process = DummyProcess()
|
||
popen_calls = []
|
||
|
||
def dummy_popen(*args, **kwargs):
|
||
popen_calls.append(kwargs)
|
||
return dummy_process
|
||
|
||
monkeypatch.setattr("subprocess.Popen", dummy_popen)
|
||
|
||
class DummySelector:
|
||
def register(self, fileobj, event, callback):
|
||
pass
|
||
|
||
def select(self, timeout=1):
|
||
return []
|
||
|
||
def close(self):
|
||
pass
|
||
|
||
monkeypatch.setattr("selectors.DefaultSelector", DummySelector)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
custom_env = {"CUSTOM_VAR": "test_value"}
|
||
utils_mod.capture_subprocess_output(["echo", "test"], new_env=custom_env)
|
||
|
||
# Verify that custom environment was passed
|
||
assert len(popen_calls) == 1
|
||
assert popen_calls[0]["env"] == custom_env
|
||
|
||
|
||
def test_capture_subprocess_output_profile_mode(monkeypatch):
|
||
"""
|
||
Test capture_subprocess_output with profileMode flag enabled.
|
||
Verifies different behavior when profiling mode is active.
|
||
"""
|
||
|
||
class DummyProcess:
|
||
def __init__(self):
|
||
self.stdout = type(
|
||
"MockStdout", (), {"readline": lambda: "", "fileno": lambda: 1}
|
||
)()
|
||
|
||
def poll(self):
|
||
return 0
|
||
|
||
def wait(self):
|
||
return 0
|
||
|
||
monkeypatch.setattr("subprocess.Popen", lambda *a, **k: DummyProcess())
|
||
|
||
class DummySelector:
|
||
def register(self, fileobj, event, callback):
|
||
pass
|
||
|
||
def select(self, timeout=1):
|
||
return []
|
||
|
||
def close(self):
|
||
pass
|
||
|
||
monkeypatch.setattr("selectors.DefaultSelector", DummySelector)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
success, output = utils_mod.capture_subprocess_output(
|
||
["echo", "test"], profileMode=True, enable_logging=False
|
||
)
|
||
|
||
assert success is True
|
||
assert isinstance(output, str)
|
||
|
||
|
||
def test_capture_subprocess_output_failure(monkeypatch):
|
||
"""
|
||
Test capture_subprocess_output returns
|
||
(False, output) when subprocess exits with nonzero code.
|
||
"""
|
||
lines = ["fail\n"]
|
||
|
||
class DummyStdout:
|
||
def __init__(self, lines):
|
||
self._lines = lines
|
||
self._idx = 0
|
||
|
||
def readline(self):
|
||
if self._idx < len(self._lines):
|
||
val = self._lines[self._idx]
|
||
self._idx += 1
|
||
return val
|
||
return ""
|
||
|
||
class DummyProcess:
|
||
def __init__(self):
|
||
self.stdout = DummyStdout(lines)
|
||
self._poll_count = 0
|
||
|
||
def poll(self):
|
||
if self._poll_count == 0:
|
||
self._poll_count += 1
|
||
return None
|
||
return 1
|
||
|
||
def wait(self):
|
||
return 1
|
||
|
||
dummy_process = DummyProcess()
|
||
|
||
def dummy_popen(*args, **kwargs):
|
||
return dummy_process
|
||
|
||
monkeypatch.setattr("subprocess.Popen", dummy_popen)
|
||
|
||
class DummySelector:
|
||
def __init__(self):
|
||
self._registered = []
|
||
|
||
def register(self, fileobj, event, callback):
|
||
self._registered.append((fileobj, event, callback))
|
||
|
||
def select(self):
|
||
if hasattr(self, "_called"):
|
||
return []
|
||
self._called = True
|
||
key_obj = type(
|
||
"Key",
|
||
(),
|
||
{
|
||
"data": staticmethod(self._registered[0][2]),
|
||
"fileobj": self._registered[0][0],
|
||
},
|
||
)()
|
||
return [(key_obj, 1)]
|
||
|
||
def close(self):
|
||
pass
|
||
|
||
monkeypatch.setattr("selectors.DefaultSelector", DummySelector)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
import utils.utils as utils_mod
|
||
|
||
success, output = utils_mod.capture_subprocess_output(["fail", "test"])
|
||
assert success is False
|
||
assert "fail" in output
|
||
|
||
|
||
def test_capture_subprocess_output_unicode_decode(monkeypatch):
|
||
"""
|
||
Test capture_subprocess_output handles
|
||
UnicodeDecodeError in handle_output gracefully.
|
||
"""
|
||
|
||
class DummyStdout:
|
||
def __init__(self):
|
||
self._called = False
|
||
|
||
def readline(self):
|
||
if not self._called:
|
||
self._called = True
|
||
raise UnicodeDecodeError("utf-8", b"", 0, 1, "reason")
|
||
return ""
|
||
|
||
class DummyProcess:
|
||
def __init__(self):
|
||
self.stdout = DummyStdout()
|
||
self._poll_count = 0
|
||
|
||
def poll(self):
|
||
if self._poll_count == 0:
|
||
self._poll_count += 1
|
||
return None
|
||
return 0
|
||
|
||
def wait(self):
|
||
return 0
|
||
|
||
dummy_process = DummyProcess()
|
||
|
||
def dummy_popen(*args, **kwargs):
|
||
return dummy_process
|
||
|
||
monkeypatch.setattr("subprocess.Popen", dummy_popen)
|
||
|
||
class DummySelector:
|
||
def __init__(self):
|
||
self._registered = []
|
||
|
||
def register(self, fileobj, event, callback):
|
||
self._registered.append((fileobj, event, callback))
|
||
|
||
def select(self):
|
||
if hasattr(self, "_called"):
|
||
return []
|
||
self._called = True
|
||
key_obj = type(
|
||
"Key",
|
||
(),
|
||
{
|
||
"data": staticmethod(self._registered[0][2]),
|
||
"fileobj": self._registered[0][0],
|
||
},
|
||
)()
|
||
return [(key_obj, 1)]
|
||
|
||
def close(self):
|
||
pass
|
||
|
||
monkeypatch.setattr("selectors.DefaultSelector", DummySelector)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
import utils.utils as utils_mod
|
||
|
||
success, output = utils_mod.capture_subprocess_output(["echo", "test"])
|
||
assert success is True
|
||
assert output == ""
|
||
|
||
|
||
# =============================================================================
|
||
# JSON DATA PARSING TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_get_agent_dict_basic():
|
||
"""
|
||
Test get_agent_dict correctly maps agent IDs to agent objects.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"agents": [
|
||
{"id": {"handle": 1}, "type": 2, "node_id": 100},
|
||
{"id": {"handle": 2}, "type": 2, "node_id": 200},
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
result = utils.get_agent_dict(data)
|
||
|
||
# Verify correct mapping
|
||
assert len(result) == 2
|
||
assert result[1]["node_id"] == 100
|
||
assert result[2]["node_id"] == 200
|
||
assert result[1]["type"] == 2
|
||
assert result[2]["type"] == 2
|
||
|
||
|
||
def test_get_agent_dict_empty_agents():
|
||
"""
|
||
Test get_agent_dict with an empty agents list.
|
||
"""
|
||
data = {"rocprofiler-sdk-tool": [{"agents": []}]}
|
||
|
||
result = utils.get_agent_dict(data)
|
||
|
||
assert result == {}
|
||
|
||
|
||
def test_get_agent_dict_missing_keys(monkeypatch):
|
||
"""
|
||
Test get_agent_dict behavior when expected keys are missing.
|
||
"""
|
||
# Case 1: Missing 'agents' key
|
||
data1 = {"rocprofiler-sdk-tool": [{}]}
|
||
|
||
with pytest.raises(KeyError):
|
||
utils.get_agent_dict(data1)
|
||
|
||
# Case 2: Missing 'rocprofiler-sdk-tool' key
|
||
data2 = {}
|
||
|
||
with pytest.raises(KeyError):
|
||
utils.get_agent_dict(data2)
|
||
|
||
# Case 3: Empty 'rocprofiler-sdk-tool' list
|
||
data3 = {"rocprofiler-sdk-tool": []}
|
||
|
||
with pytest.raises(IndexError):
|
||
utils.get_agent_dict(data3)
|
||
|
||
|
||
def test_get_agent_dict_duplicate_agent_ids():
|
||
"""
|
||
Test get_agent_dict behavior with duplicate agent IDs.
|
||
The function should overwrite previous entries with the same ID.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"agents": [
|
||
{"id": {"handle": 1}, "type": 2, "node_id": 100, "name": "first"},
|
||
{"id": {"handle": 1}, "type": 2, "node_id": 200, "name": "second"},
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
result = utils.get_agent_dict(data)
|
||
|
||
assert len(result) == 1
|
||
assert result[1]["node_id"] == 200
|
||
assert result[1]["name"] == "second"
|
||
|
||
|
||
def test_get_agent_dict_non_integer_handles():
|
||
"""
|
||
Test get_agent_dict with non-integer handle values.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"agents": [
|
||
{"id": {"handle": "agent_1"}, "type": 2, "node_id": 100},
|
||
{"id": {"handle": "agent_2"}, "type": 2, "node_id": 200},
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
result = utils.get_agent_dict(data)
|
||
|
||
assert len(result) == 2
|
||
assert result["agent_1"]["node_id"] == 100
|
||
assert result["agent_2"]["node_id"] == 200
|
||
|
||
|
||
# =========================================================================
|
||
# Tests for get_gpuid_dict function
|
||
# =========================================================================
|
||
def test_get_gpuid_dict_basic():
|
||
"""Test that get_gpuid_dict correctly maps agent IDs to GPU IDs for a basic case.
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that agent IDs are correctly mapped to GPU IDs
|
||
based on node_id ordering.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"agents": [
|
||
{"id": {"handle": 100}, "node_id": 5, "type": 2}, # GPU agent
|
||
{"id": {"handle": 101}, "node_id": 3, "type": 2}, # GPU agent
|
||
{"id": {"handle": 102}, "node_id": 7, "type": 2}, # GPU agent
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
expected = {101: 0, 100: 1, 102: 2}
|
||
|
||
result = utils.get_gpuid_dict(data)
|
||
assert result == expected
|
||
|
||
|
||
def test_get_gpuid_dict_no_gpu_agents():
|
||
"""Test that get_gpuid_dict returns an empty dictionary
|
||
when no GPU agents are present.
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that an empty dictionary is returned
|
||
when there are no GPU agents.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"agents": [
|
||
{"id": {"handle": 100}, "node_id": 5, "type": 1}, # Non-GPU agent
|
||
{"id": {"handle": 101}, "node_id": 3, "type": 3}, # Non-GPU agent
|
||
{"id": {"handle": 102}, "node_id": 7, "type": 0}, # Non-GPU agent
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
result = utils.get_gpuid_dict(data)
|
||
assert result == {}
|
||
|
||
|
||
def test_get_gpuid_dict_mixed_agents():
|
||
"""Test that get_gpuid_dict correctly ignores non-GPU agents
|
||
and only maps GPU agents.
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that only GPU agents (type 2) are included
|
||
in the mapping.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"agents": [
|
||
{"id": {"handle": 100}, "node_id": 5, "type": 2}, # GPU agent
|
||
{"id": {"handle": 101}, "node_id": 3, "type": 1}, # Non-GPU agent
|
||
{"id": {"handle": 102}, "node_id": 7, "type": 2}, # GPU agent
|
||
{"id": {"handle": 103}, "node_id": 2, "type": 0}, # Non-GPU agent
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
# Expected mapping after sorting by node_id and filtering by type 2: 100->0, 102->1
|
||
expected = {100: 0, 102: 1}
|
||
|
||
result = utils.get_gpuid_dict(data)
|
||
assert result == expected
|
||
|
||
|
||
def test_get_gpuid_dict_sorting():
|
||
"""Test that get_gpuid_dict correctly sorts GPU agents by node_id
|
||
to determine GPU ID ordering.
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that GPU agents are sorted by node_id before
|
||
being assigned sequential GPU IDs.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"agents": [
|
||
{"id": {"handle": 100}, "node_id": 10, "type": 2}, # GPU agent
|
||
{"id": {"handle": 101}, "node_id": 5, "type": 2}, # GPU agent
|
||
{"id": {"handle": 102}, "node_id": 8, "type": 2}, # GPU agent
|
||
{"id": {"handle": 103}, "node_id": 1, "type": 2}, # GPU agent
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
expected = {103: 0, 101: 1, 102: 2, 100: 3}
|
||
|
||
result = utils.get_gpuid_dict(data)
|
||
assert result == expected
|
||
|
||
|
||
def test_get_gpuid_dict_empty_agents():
|
||
"""Test that get_gpuid_dict handles an empty agents list correctly.
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that an empty dictionary is returned when the
|
||
agents list is empty.
|
||
"""
|
||
# Sample data with empty agents list
|
||
data = {"rocprofiler-sdk-tool": [{"agents": []}]}
|
||
|
||
result = utils.get_gpuid_dict(data)
|
||
assert result == {}
|
||
|
||
|
||
# Tests for v3_json_get_counters function =====================================
|
||
def test_v3_json_get_counters_normal_case():
|
||
"""Test v3_json_get_counters with a valid data structure
|
||
containing multiple counters.
|
||
|
||
This test verifies that the function correctly extracts
|
||
counters from the JSON data
|
||
and creates a mapping using (agent_id, counter_id)
|
||
tuples as keys.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"agent_id": {"handle": 100},
|
||
"name": "counter1",
|
||
},
|
||
{
|
||
"id": {"handle": 2},
|
||
"agent_id": {"handle": 100},
|
||
"name": "counter2",
|
||
},
|
||
{
|
||
"id": {"handle": 1},
|
||
"agent_id": {"handle": 200},
|
||
"name": "counter3",
|
||
},
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
counter_map = utils.v3_json_get_counters(data)
|
||
|
||
assert len(counter_map) == 3
|
||
assert counter_map[(100, 1)]["name"] == "counter1"
|
||
assert counter_map[(100, 2)]["name"] == "counter2"
|
||
assert counter_map[(200, 1)]["name"] == "counter3"
|
||
|
||
|
||
def test_v3_json_get_counters_empty_counters():
|
||
"""Test v3_json_get_counters with an empty counters array.
|
||
|
||
This test ensures the function handles the case where no counters are present
|
||
and returns an empty dictionary.
|
||
"""
|
||
data = {"rocprofiler-sdk-tool": [{"counters": []}]}
|
||
|
||
counter_map = utils.v3_json_get_counters(data)
|
||
|
||
assert len(counter_map) == 0
|
||
assert counter_map == {}
|
||
|
||
|
||
def test_v3_json_get_counters_duplicate_keys():
|
||
"""Test v3_json_get_counters with duplicate
|
||
(agent_id, counter_id) tuples.
|
||
|
||
This test verifies that when multiple counters
|
||
have the same (agent_id, counter_id) tuple,
|
||
the last counter overwrites previous ones
|
||
in the returned dictionary.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"agent_id": {"handle": 100},
|
||
"name": "counter1",
|
||
},
|
||
{
|
||
"id": {"handle": 1},
|
||
"agent_id": {"handle": 100},
|
||
"name": "counter2",
|
||
},
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
counter_map = utils.v3_json_get_counters(data)
|
||
|
||
assert len(counter_map) == 1
|
||
assert counter_map[(100, 1)]["name"] == "counter2"
|
||
|
||
|
||
def test_v3_json_get_counters_various_value_types():
|
||
"""Test v3_json_get_counters with different types of values for handles.
|
||
|
||
This test ensures the function correctly handles different data types
|
||
(integers and strings) for the handle values.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"agent_id": {"handle": 100},
|
||
"name": "counter1",
|
||
},
|
||
{
|
||
"id": {"handle": "2"},
|
||
"agent_id": {"handle": 100},
|
||
"name": "counter2",
|
||
},
|
||
{
|
||
"id": {"handle": 3},
|
||
"agent_id": {"handle": "200"},
|
||
"name": "counter3",
|
||
},
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
counter_map = utils.v3_json_get_counters(data)
|
||
|
||
assert len(counter_map) == 3
|
||
assert counter_map[(100, 1)]["name"] == "counter1"
|
||
assert counter_map[(100, "2")]["name"] == "counter2"
|
||
assert counter_map[("200", 3)]["name"] == "counter3"
|
||
|
||
|
||
def test_v3_json_get_counters_missing_key():
|
||
"""Test v3_json_get_counters raises KeyError when required keys are missing.
|
||
|
||
This test verifies that the function raises a KeyError when
|
||
the agent_id key is missing.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"counters": [{"id": {"handle": 1}, "name": "counter1"}]
|
||
} # Missing agent_id
|
||
]
|
||
}
|
||
|
||
with pytest.raises(KeyError):
|
||
utils.v3_json_get_counters(data)
|
||
|
||
|
||
def test_v3_json_get_counters_missing_nested_key():
|
||
"""Test v3_json_get_counters raises KeyError when
|
||
nested required keys are missing.
|
||
|
||
This test verifies that the function raises a KeyError
|
||
when the handle keyis missing from the id dictionary.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{"counters": [{"id": {}, "agent_id": {"handle": 100}, "name": "counter1"}]}
|
||
]
|
||
}
|
||
|
||
with pytest.raises(KeyError):
|
||
utils.v3_json_get_counters(data)
|
||
|
||
|
||
def test_v3_json_get_counters_data_structure():
|
||
"""Test that v3_json_get_counters preserves the entire counter
|
||
object in the mapping.
|
||
|
||
This test ensures that the function stores the entire counter
|
||
object in the mapping, not just selected fields.
|
||
"""
|
||
counter_object = {
|
||
"id": {"handle": 1},
|
||
"agent_id": {"handle": 100},
|
||
"name": "counter1",
|
||
"description": "Test counter",
|
||
"block": "SQ",
|
||
"event_id": 123,
|
||
"enabled": True,
|
||
}
|
||
|
||
data = {"rocprofiler-sdk-tool": [{"counters": [counter_object]}]}
|
||
|
||
counter_map = utils.v3_json_get_counters(data)
|
||
|
||
assert len(counter_map) == 1
|
||
assert counter_map[(100, 1)] == counter_object
|
||
assert counter_map[(100, 1)]["description"] == "Test counter"
|
||
assert counter_map[(100, 1)]["block"] == "SQ"
|
||
assert counter_map[(100, 1)]["event_id"] == 123
|
||
assert counter_map[(100, 1)]["enabled"] is True
|
||
|
||
|
||
def test_v3_json_get_dispatches_normal_case():
|
||
"""
|
||
Test v3_json_get_dispatches with valid data containing multiple dispatch records.
|
||
|
||
Args:
|
||
None
|
||
|
||
Returns:
|
||
None: Asserts the function correctly maps all dispatch records
|
||
by their correlation IDs.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"buffer_records": {
|
||
"kernel_dispatch": [
|
||
{
|
||
"correlation_id": {"internal": "id1"},
|
||
"start_timestamp": 100,
|
||
"end_timestamp": 200,
|
||
},
|
||
{
|
||
"correlation_id": {"internal": "id2"},
|
||
"start_timestamp": 300,
|
||
"end_timestamp": 400,
|
||
},
|
||
{
|
||
"correlation_id": {"internal": "id3"},
|
||
"start_timestamp": 500,
|
||
"end_timestamp": 600,
|
||
},
|
||
]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
result = utils.v3_json_get_dispatches(data)
|
||
|
||
assert len(result) == 3
|
||
assert result["id1"]["start_timestamp"] == 100
|
||
assert result["id2"]["end_timestamp"] == 400
|
||
assert result["id3"]["correlation_id"]["internal"] == "id3"
|
||
|
||
|
||
def test_v3_json_get_dispatches_empty_case():
|
||
"""
|
||
Test v3_json_get_dispatches with data containing no dispatch records.
|
||
|
||
Args:
|
||
None
|
||
|
||
Returns:
|
||
None: Asserts the function returns an empty dictionary
|
||
when no dispatch records are present.
|
||
"""
|
||
data = {"rocprofiler-sdk-tool": [{"buffer_records": {"kernel_dispatch": []}}]}
|
||
|
||
result = utils.v3_json_get_dispatches(data)
|
||
|
||
assert len(result) == 0
|
||
assert isinstance(result, dict)
|
||
|
||
|
||
def test_v3_json_get_dispatches_missing_fields():
|
||
"""
|
||
Test v3_json_get_dispatches handling of data with missing required fields.
|
||
|
||
Args:
|
||
None
|
||
|
||
Returns:
|
||
None: Asserts the function raises a KeyError when required fields are missing.
|
||
"""
|
||
data = {"rocprofiler-sdk-tool": [{"buffer_records": {}}]}
|
||
|
||
with pytest.raises(KeyError):
|
||
utils.v3_json_get_dispatches(data)
|
||
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{"buffer_records": {"kernel_dispatch": [{"start_timestamp": 100}]}}
|
||
]
|
||
}
|
||
|
||
with pytest.raises(KeyError):
|
||
utils.v3_json_get_dispatches(data)
|
||
|
||
|
||
def test_v3_json_get_dispatches_duplicate_ids():
|
||
"""
|
||
Test v3_json_get_dispatches handling of duplicate correlation IDs.
|
||
|
||
Args:
|
||
None
|
||
|
||
Returns:
|
||
None: Asserts that when duplicate correlation IDs exist,
|
||
the function keeps the latest record.
|
||
"""
|
||
data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"buffer_records": {
|
||
"kernel_dispatch": [
|
||
{
|
||
"correlation_id": {"internal": "id1"},
|
||
"start_timestamp": 100,
|
||
"end_timestamp": 200,
|
||
},
|
||
{
|
||
"correlation_id": {"internal": "id1"},
|
||
"start_timestamp": 300,
|
||
"end_timestamp": 400,
|
||
}, # Duplicate ID
|
||
{
|
||
"correlation_id": {"internal": "id3"},
|
||
"start_timestamp": 500,
|
||
"end_timestamp": 600,
|
||
},
|
||
]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
result = utils.v3_json_get_dispatches(data)
|
||
|
||
assert len(result) == 2
|
||
assert result["id1"]["start_timestamp"] == 300
|
||
assert result["id1"]["end_timestamp"] == 400
|
||
assert "id3" in result
|
||
|
||
|
||
# =============================================================================
|
||
# JSON TO CSV CONVERSION TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_v3_json_to_csv_basic_functionality(tmp_path, monkeypatch):
|
||
"""
|
||
Test basic functionality of v3_json_to_csv with a minimal valid JSON input.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior
|
||
"""
|
||
|
||
valid_json = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"metadata": {"pid": 12345},
|
||
"agents": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"type": 2,
|
||
"node_id": 0,
|
||
"wave_front_size": 64,
|
||
}
|
||
],
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 101},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER1",
|
||
}
|
||
],
|
||
"kernel_symbols": {
|
||
"kernel1": {
|
||
"formatted_kernel_name": "TestKernel",
|
||
"private_segment_size": 0,
|
||
}
|
||
},
|
||
"buffer_records": {
|
||
"kernel_dispatch": [
|
||
{
|
||
"correlation_id": {"internal": "corr1"},
|
||
"start_timestamp": 100,
|
||
"end_timestamp": 200,
|
||
}
|
||
]
|
||
},
|
||
"callback_records": {
|
||
"counter_collection": [
|
||
{
|
||
"thread_id": 67890,
|
||
"lds_block_size_v": 0,
|
||
"arch_vgpr_count": 32,
|
||
"sgpr_count": 16,
|
||
"dispatch_data": {
|
||
"dispatch_info": {
|
||
"dispatch_id": 1,
|
||
"agent_id": {"handle": 1},
|
||
"queue_id": {"handle": 2},
|
||
"kernel_id": "kernel1",
|
||
"grid_size": {"x": 1, "y": 1, "z": 1},
|
||
"workgroup_size": {"x": 64, "y": 1, "z": 1},
|
||
},
|
||
"correlation_id": {
|
||
"internal": "corr1",
|
||
"external": "ext1",
|
||
},
|
||
},
|
||
"records": [{"counter_id": {"handle": 101}, "value": 42}],
|
||
}
|
||
]
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
json_path = tmp_path / "test.json"
|
||
with open(json_path, "w") as f:
|
||
json.dump(valid_json, f)
|
||
|
||
csv_path = tmp_path / "output.csv"
|
||
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_dispatches",
|
||
lambda data: {
|
||
"corr1": valid_json["rocprofiler-sdk-tool"][0]["buffer_records"][
|
||
"kernel_dispatch"
|
||
][0]
|
||
},
|
||
)
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"get_agent_dict",
|
||
lambda data: {1: valid_json["rocprofiler-sdk-tool"][0]["agents"][0]},
|
||
)
|
||
monkeypatch.setattr(utils, "get_gpuid_dict", lambda data: {1: 0})
|
||
monkeypatch.setattr(
|
||
utils, "v3_json_get_counters", lambda data: {(1, 101): {"name": "COUNTER1"}}
|
||
)
|
||
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
assert csv_path.exists()
|
||
df = pd.read_csv(csv_path)
|
||
|
||
assert "Dispatch_ID" in df.columns
|
||
assert "GPU_ID" in df.columns
|
||
assert "Kernel_Name" in df.columns
|
||
assert "COUNTER1" in df.columns
|
||
assert len(df) == 1
|
||
assert df["Dispatch_ID"][0] == 1
|
||
assert df["Kernel_Name"][0] == "TestKernel"
|
||
assert df["COUNTER1"][0] == 42
|
||
assert df["Start_Timestamp"][0] == 100
|
||
assert df["End_Timestamp"][0] == 200
|
||
|
||
|
||
def test_v3_json_to_csv_no_dispatches(tmp_path, monkeypatch):
|
||
"""
|
||
Test v3_json_to_csv with a JSON file that has no dispatches.
|
||
Should create an empty CSV with headers.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior
|
||
"""
|
||
|
||
empty_json = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"metadata": {"pid": 12345},
|
||
"agents": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"type": 2,
|
||
"node_id": 0,
|
||
"wave_front_size": 64,
|
||
}
|
||
],
|
||
"counters": [],
|
||
"kernel_symbols": {},
|
||
"buffer_records": {"kernel_dispatch": []},
|
||
"callback_records": {"counter_collection": []},
|
||
}
|
||
]
|
||
}
|
||
|
||
json_path = tmp_path / "empty.json"
|
||
with open(json_path, "w") as f:
|
||
json.dump(empty_json, f)
|
||
csv_path = tmp_path / "empty_output.csv"
|
||
|
||
monkeypatch.setattr(utils, "v3_json_get_dispatches", lambda data: {})
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"get_agent_dict",
|
||
lambda data: {1: empty_json["rocprofiler-sdk-tool"][0]["agents"][0]},
|
||
)
|
||
monkeypatch.setattr(utils, "get_gpuid_dict", lambda data: {1: 0})
|
||
monkeypatch.setattr(utils, "v3_json_get_counters", lambda data: {})
|
||
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
assert csv_path.exists()
|
||
df = pd.read_csv(csv_path)
|
||
|
||
assert "Dispatch_ID" in df.columns
|
||
assert "GPU_ID" in df.columns
|
||
assert "Kernel_Name" in df.columns
|
||
assert len(df) == 0
|
||
|
||
|
||
def test_v3_json_to_csv_accumulated_counters(tmp_path, monkeypatch):
|
||
"""
|
||
Test v3_json_to_csv handling of accumulated counters (with _ACCUM suffix).
|
||
Should rename them to SQ_ACCUM_PREV_HIRES.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior
|
||
"""
|
||
|
||
json_data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"metadata": {"pid": 12345},
|
||
"agents": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"type": 2,
|
||
"node_id": 0,
|
||
"wave_front_size": 64,
|
||
}
|
||
],
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 101},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER_ACCUM",
|
||
}
|
||
],
|
||
"kernel_symbols": {
|
||
"kernel1": {
|
||
"formatted_kernel_name": "TestKernel",
|
||
"private_segment_size": 0,
|
||
}
|
||
},
|
||
"buffer_records": {
|
||
"kernel_dispatch": [
|
||
{
|
||
"correlation_id": {"internal": "corr1"},
|
||
"start_timestamp": 100,
|
||
"end_timestamp": 200,
|
||
}
|
||
]
|
||
},
|
||
"callback_records": {
|
||
"counter_collection": [
|
||
{
|
||
"thread_id": 67890,
|
||
"lds_block_size_v": 0,
|
||
"arch_vgpr_count": 32,
|
||
"sgpr_count": 16,
|
||
"dispatch_data": {
|
||
"dispatch_info": {
|
||
"dispatch_id": 1,
|
||
"agent_id": {"handle": 1},
|
||
"queue_id": {"handle": 2},
|
||
"kernel_id": "kernel1",
|
||
"grid_size": {"x": 1, "y": 1, "z": 1},
|
||
"workgroup_size": {"x": 64, "y": 1, "z": 1},
|
||
},
|
||
"correlation_id": {
|
||
"internal": "corr1",
|
||
"external": "ext1",
|
||
},
|
||
},
|
||
"records": [{"counter_id": {"handle": 101}, "value": 42}],
|
||
}
|
||
]
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
json_path = tmp_path / "accum.json"
|
||
with open(json_path, "w") as f:
|
||
json.dump(json_data, f)
|
||
|
||
csv_path = tmp_path / "accum_output.csv"
|
||
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_dispatches",
|
||
lambda data: {
|
||
"corr1": json_data["rocprofiler-sdk-tool"][0]["buffer_records"][
|
||
"kernel_dispatch"
|
||
][0]
|
||
},
|
||
)
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"get_agent_dict",
|
||
lambda data: {1: json_data["rocprofiler-sdk-tool"][0]["agents"][0]},
|
||
)
|
||
monkeypatch.setattr(utils, "get_gpuid_dict", lambda data: {1: 0})
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_counters",
|
||
lambda data: {(1, 101): {"name": "COUNTER_ACCUM"}},
|
||
)
|
||
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
assert csv_path.exists()
|
||
df = pd.read_csv(csv_path)
|
||
|
||
assert "COUNTER_ACCUM" not in df.columns
|
||
assert "SQ_ACCUM_PREV_HIRES" in df.columns
|
||
assert df["SQ_ACCUM_PREV_HIRES"][0] == 42
|
||
|
||
|
||
def test_v3_json_to_csv_duplicate_counters(tmp_path, monkeypatch):
|
||
"""
|
||
Test v3_json_to_csv handling of duplicate counter names.
|
||
Should sum the values.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior
|
||
"""
|
||
|
||
json_data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"metadata": {"pid": 12345},
|
||
"agents": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"type": 2,
|
||
"node_id": 0,
|
||
"wave_front_size": 64,
|
||
}
|
||
],
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 101},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER1",
|
||
},
|
||
{
|
||
"id": {"handle": 102},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER1",
|
||
},
|
||
],
|
||
"kernel_symbols": {
|
||
"kernel1": {
|
||
"formatted_kernel_name": "TestKernel",
|
||
"private_segment_size": 0,
|
||
}
|
||
},
|
||
"buffer_records": {
|
||
"kernel_dispatch": [
|
||
{
|
||
"correlation_id": {"internal": "corr1"},
|
||
"start_timestamp": 100,
|
||
"end_timestamp": 200,
|
||
}
|
||
]
|
||
},
|
||
"callback_records": {
|
||
"counter_collection": [
|
||
{
|
||
"thread_id": 67890,
|
||
"lds_block_size_v": 0,
|
||
"arch_vgpr_count": 32,
|
||
"sgpr_count": 16,
|
||
"dispatch_data": {
|
||
"dispatch_info": {
|
||
"dispatch_id": 1,
|
||
"agent_id": {"handle": 1},
|
||
"queue_id": {"handle": 2},
|
||
"kernel_id": "kernel1",
|
||
"grid_size": {"x": 1, "y": 1, "z": 1},
|
||
"workgroup_size": {"x": 64, "y": 1, "z": 1},
|
||
},
|
||
"correlation_id": {
|
||
"internal": "corr1",
|
||
"external": "ext1",
|
||
},
|
||
},
|
||
"records": [
|
||
{"counter_id": {"handle": 101}, "value": 42},
|
||
{"counter_id": {"handle": 102}, "value": 58},
|
||
],
|
||
}
|
||
]
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
json_path = tmp_path / "duplicate.json"
|
||
with open(json_path, "w") as f:
|
||
json.dump(json_data, f)
|
||
|
||
csv_path = tmp_path / "duplicate_output.csv"
|
||
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_dispatches",
|
||
lambda data: {
|
||
"corr1": json_data["rocprofiler-sdk-tool"][0]["buffer_records"][
|
||
"kernel_dispatch"
|
||
][0]
|
||
},
|
||
)
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"get_agent_dict",
|
||
lambda data: {1: json_data["rocprofiler-sdk-tool"][0]["agents"][0]},
|
||
)
|
||
monkeypatch.setattr(utils, "get_gpuid_dict", lambda data: {1: 0})
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_counters",
|
||
lambda data: {(1, 101): {"name": "COUNTER1"}, (1, 102): {"name": "COUNTER1"}},
|
||
)
|
||
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
assert csv_path.exists()
|
||
df = pd.read_csv(csv_path)
|
||
|
||
assert df["COUNTER1"][0] == 100 # 42 + 58
|
||
|
||
|
||
def test_v3_json_to_csv_file_not_found(monkeypatch):
|
||
"""
|
||
Test v3_json_to_csv handling of non-existent input file.
|
||
Should raise FileNotFoundError.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior
|
||
"""
|
||
with pytest.raises(FileNotFoundError):
|
||
utils.v3_json_to_csv("/nonexistent/path.json", "output.csv")
|
||
|
||
|
||
def test_v3_json_to_csv_invalid_json(tmp_path):
|
||
"""
|
||
Test v3_json_to_csv handling of invalid JSON input.
|
||
Should raise JSONDecodeError.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
"""
|
||
json_path = tmp_path / "invalid.json"
|
||
with open(json_path, "w") as f:
|
||
f.write("{invalid json")
|
||
|
||
csv_path = tmp_path / "invalid_output.csv"
|
||
|
||
with pytest.raises(json.JSONDecodeError):
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
|
||
def test_v3_json_to_csv_missing_required_keys(tmp_path):
|
||
"""
|
||
Test v3_json_to_csv handling of JSON missing required keys.
|
||
Should raise KeyError.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
"""
|
||
|
||
invalid_json = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
# Missing "metadata", "agents", etc.
|
||
"kernel_symbols": {}
|
||
}
|
||
]
|
||
}
|
||
|
||
json_path = tmp_path / "missing_keys.json"
|
||
with open(json_path, "w") as f:
|
||
json.dump(invalid_json, f)
|
||
|
||
csv_path = tmp_path / "missing_keys_output.csv"
|
||
|
||
with pytest.raises(KeyError):
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
|
||
def test_v3_json_to_csv_complex_dispatch(tmp_path, monkeypatch):
|
||
"""
|
||
Test v3_json_to_csv with a more complex dispatch scenario including
|
||
multiple dispatches and 3D grid/workgroup sizes.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior
|
||
"""
|
||
|
||
complex_json = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"metadata": {"pid": 12345},
|
||
"agents": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"type": 2,
|
||
"node_id": 0,
|
||
"wave_front_size": 64,
|
||
},
|
||
{
|
||
"id": {"handle": 2},
|
||
"type": 2,
|
||
"node_id": 1,
|
||
"wave_front_size": 32,
|
||
},
|
||
],
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 101},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER1",
|
||
},
|
||
{
|
||
"id": {"handle": 102},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER2",
|
||
},
|
||
],
|
||
"kernel_symbols": {
|
||
"kernel1": {
|
||
"formatted_kernel_name": "Kernel1",
|
||
"private_segment_size": 16,
|
||
},
|
||
"kernel2": {
|
||
"formatted_kernel_name": "Kernel2",
|
||
"private_segment_size": 32,
|
||
},
|
||
},
|
||
"buffer_records": {
|
||
"kernel_dispatch": [
|
||
{
|
||
"correlation_id": {"internal": "corr1"},
|
||
"start_timestamp": 100,
|
||
"end_timestamp": 200,
|
||
},
|
||
{
|
||
"correlation_id": {"internal": "corr2"},
|
||
"start_timestamp": 300,
|
||
"end_timestamp": 400,
|
||
},
|
||
]
|
||
},
|
||
"callback_records": {
|
||
"counter_collection": [
|
||
{
|
||
"thread_id": 67890,
|
||
"lds_block_size_v": 64,
|
||
"arch_vgpr_count": 32,
|
||
"sgpr_count": 16,
|
||
"dispatch_data": {
|
||
"dispatch_info": {
|
||
"dispatch_id": 1,
|
||
"agent_id": {"handle": 1},
|
||
"queue_id": {"handle": 2},
|
||
"kernel_id": "kernel1",
|
||
"grid_size": {"x": 2, "y": 3, "z": 4},
|
||
"workgroup_size": {"x": 8, "y": 4, "z": 2},
|
||
},
|
||
"correlation_id": {
|
||
"internal": "corr1",
|
||
"external": "ext1",
|
||
},
|
||
},
|
||
"records": [
|
||
{"counter_id": {"handle": 101}, "value": 42},
|
||
{"counter_id": {"handle": 102}, "value": 24},
|
||
],
|
||
},
|
||
{
|
||
"thread_id": 67891,
|
||
"lds_block_size_v": 128,
|
||
"arch_vgpr_count": 64,
|
||
"sgpr_count": 32,
|
||
"dispatch_data": {
|
||
"dispatch_info": {
|
||
"dispatch_id": 2,
|
||
"agent_id": {"handle": 1},
|
||
"queue_id": {"handle": 3},
|
||
"kernel_id": "kernel2",
|
||
"grid_size": {"x": 16, "y": 8, "z": 4},
|
||
"workgroup_size": {"x": 16, "y": 16, "z": 1},
|
||
},
|
||
"correlation_id": {
|
||
"internal": "corr2",
|
||
"external": "ext2",
|
||
},
|
||
},
|
||
"records": [
|
||
{"counter_id": {"handle": 101}, "value": 84},
|
||
{"counter_id": {"handle": 102}, "value": 36},
|
||
],
|
||
},
|
||
]
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
json_path = tmp_path / "complex.json"
|
||
with open(json_path, "w") as f:
|
||
json.dump(complex_json, f)
|
||
|
||
csv_path = tmp_path / "complex_output.csv"
|
||
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_dispatches",
|
||
lambda data: {
|
||
"corr1": complex_json["rocprofiler-sdk-tool"][0]["buffer_records"][
|
||
"kernel_dispatch"
|
||
][0],
|
||
"corr2": complex_json["rocprofiler-sdk-tool"][0]["buffer_records"][
|
||
"kernel_dispatch"
|
||
][1],
|
||
},
|
||
)
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"get_agent_dict",
|
||
lambda data: {
|
||
1: complex_json["rocprofiler-sdk-tool"][0]["agents"][0],
|
||
2: complex_json["rocprofiler-sdk-tool"][0]["agents"][1],
|
||
},
|
||
)
|
||
monkeypatch.setattr(utils, "get_gpuid_dict", lambda data: {1: 0, 2: 1})
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_counters",
|
||
lambda data: {(1, 101): {"name": "COUNTER1"}, (1, 102): {"name": "COUNTER2"}},
|
||
)
|
||
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
assert csv_path.exists()
|
||
df = pd.read_csv(csv_path)
|
||
|
||
assert len(df) == 2
|
||
|
||
assert df["Grid_Size"][0] == 24
|
||
assert df["Workgroup_Size"][0] == 64
|
||
assert df["Kernel_Name"][0] == "Kernel1"
|
||
assert df["COUNTER1"][0] == 42
|
||
assert df["COUNTER2"][0] == 24
|
||
assert df["GPU_ID"][0] == 0
|
||
assert df["Wave_Size"][0] == 64
|
||
|
||
assert df["Grid_Size"][1] == 512
|
||
assert df["Workgroup_Size"][1] == 256
|
||
assert df["Kernel_Name"][1] == "Kernel2"
|
||
assert df["COUNTER1"][1] == 84
|
||
assert df["COUNTER2"][1] == 36
|
||
assert df["GPU_ID"][1] == 0
|
||
assert df["Wave_Size"][1] == 64
|
||
|
||
|
||
def test_v3_json_to_csv_missing_counters_handling(tmp_path, monkeypatch):
|
||
"""
|
||
Test v3_json_to_csv handles cases where different
|
||
dispatches have different sets of counters.
|
||
This addresses the DataFrame creation issue
|
||
where arrays have different lengths.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying behavior
|
||
"""
|
||
|
||
json_data = {
|
||
"rocprofiler-sdk-tool": [
|
||
{
|
||
"metadata": {"pid": 12345},
|
||
"agents": [
|
||
{
|
||
"id": {"handle": 1},
|
||
"type": 2,
|
||
"node_id": 0,
|
||
"wave_front_size": 64,
|
||
}
|
||
],
|
||
"counters": [
|
||
{
|
||
"id": {"handle": 101},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER1",
|
||
},
|
||
{
|
||
"id": {"handle": 102},
|
||
"agent_id": {"handle": 1},
|
||
"name": "COUNTER2",
|
||
},
|
||
],
|
||
"kernel_symbols": {
|
||
"kernel1": {
|
||
"formatted_kernel_name": "Kernel1",
|
||
"private_segment_size": 16,
|
||
},
|
||
"kernel2": {
|
||
"formatted_kernel_name": "Kernel2",
|
||
"private_segment_size": 32,
|
||
},
|
||
},
|
||
"buffer_records": {
|
||
"kernel_dispatch": [
|
||
{
|
||
"correlation_id": {"internal": "corr1"},
|
||
"start_timestamp": 100,
|
||
"end_timestamp": 200,
|
||
},
|
||
{
|
||
"correlation_id": {"internal": "corr2"},
|
||
"start_timestamp": 300,
|
||
"end_timestamp": 400,
|
||
},
|
||
]
|
||
},
|
||
"callback_records": {
|
||
"counter_collection": [
|
||
{
|
||
"thread_id": 67890,
|
||
"lds_block_size_v": 64,
|
||
"arch_vgpr_count": 32,
|
||
"sgpr_count": 16,
|
||
"dispatch_data": {
|
||
"dispatch_info": {
|
||
"dispatch_id": 1,
|
||
"agent_id": {"handle": 1},
|
||
"queue_id": {"handle": 2},
|
||
"kernel_id": "kernel1",
|
||
"grid_size": {"x": 2, "y": 3, "z": 4},
|
||
"workgroup_size": {"x": 8, "y": 4, "z": 2},
|
||
},
|
||
"correlation_id": {
|
||
"internal": "corr1",
|
||
"external": "ext1",
|
||
},
|
||
},
|
||
"records": [
|
||
{"counter_id": {"handle": 101}, "value": 42}
|
||
], # Only COUNTER1
|
||
},
|
||
{
|
||
"thread_id": 67891,
|
||
"lds_block_size_v": 128,
|
||
"arch_vgpr_count": 64,
|
||
"sgpr_count": 32,
|
||
"dispatch_data": {
|
||
"dispatch_info": {
|
||
"dispatch_id": 2,
|
||
"agent_id": {"handle": 1},
|
||
"queue_id": {"handle": 3},
|
||
"kernel_id": "kernel2",
|
||
"grid_size": {"x": 16, "y": 8, "z": 4},
|
||
"workgroup_size": {"x": 16, "y": 16, "z": 1},
|
||
},
|
||
"correlation_id": {
|
||
"internal": "corr2",
|
||
"external": "ext2",
|
||
},
|
||
},
|
||
"records": [
|
||
{"counter_id": {"handle": 102}, "value": 84}
|
||
], # Only COUNTER2
|
||
},
|
||
]
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
json_path = tmp_path / "missing_counters.json"
|
||
with open(json_path, "w") as f:
|
||
json.dump(json_data, f)
|
||
|
||
csv_path = tmp_path / "missing_counters_output.csv"
|
||
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_dispatches",
|
||
lambda data: {
|
||
"corr1": json_data["rocprofiler-sdk-tool"][0]["buffer_records"][
|
||
"kernel_dispatch"
|
||
][0],
|
||
"corr2": json_data["rocprofiler-sdk-tool"][0]["buffer_records"][
|
||
"kernel_dispatch"
|
||
][1],
|
||
},
|
||
)
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"get_agent_dict",
|
||
lambda data: {1: json_data["rocprofiler-sdk-tool"][0]["agents"][0]},
|
||
)
|
||
monkeypatch.setattr(utils, "get_gpuid_dict", lambda data: {1: 0})
|
||
monkeypatch.setattr(
|
||
utils,
|
||
"v3_json_get_counters",
|
||
lambda data: {(1, 101): {"name": "COUNTER1"}, (1, 102): {"name": "COUNTER2"}},
|
||
)
|
||
|
||
try:
|
||
utils.v3_json_to_csv(json_path, csv_path)
|
||
|
||
assert csv_path.exists()
|
||
df = pd.read_csv(csv_path)
|
||
|
||
assert len(df) == 2
|
||
|
||
assert "COUNTER1" in df.columns
|
||
assert "COUNTER2" in df.columns
|
||
|
||
except ValueError as e:
|
||
if "All arrays must be of the same length" in str(e):
|
||
pytest.skip(
|
||
"v3_json_to_csv does not currently "
|
||
"handle missing counters gracefully - arrays have different lengths"
|
||
)
|
||
else:
|
||
raise
|
||
|
||
|
||
# =============================================================================
|
||
# RESOURCE ALLOCATION TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_check_resource_allocation_no_ctest(monkeypatch):
|
||
"""
|
||
Test check_resource_allocation when CTEST_RESOURCE_GROUP_COUNT is not set.
|
||
Should return without setting HIP_VISIBLE_DEVICES.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying environment
|
||
"""
|
||
monkeypatch.delenv("CTEST_RESOURCE_GROUP_COUNT", raising=False)
|
||
monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising=False)
|
||
|
||
from tests.test_utils import check_resource_allocation
|
||
|
||
result = check_resource_allocation()
|
||
|
||
assert result is None
|
||
assert "HIP_VISIBLE_DEVICES" not in os.environ
|
||
|
||
|
||
def test_check_resource_allocation_with_gpu_resource(monkeypatch):
|
||
"""
|
||
Test check_resource_allocation when CTEST resource allocation is
|
||
enabled with GPU resource. Should extract GPU ID and set HIP_VISIBLE_DEVICES.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying environment
|
||
"""
|
||
monkeypatch.setenv("CTEST_RESOURCE_GROUP_COUNT", "1")
|
||
monkeypatch.setenv("CTEST_RESOURCE_GROUP_0_GPUS", "id:2,slots:1")
|
||
monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising=False)
|
||
from tests.test_utils import check_resource_allocation
|
||
|
||
result = check_resource_allocation()
|
||
|
||
assert result is None
|
||
assert os.environ["HIP_VISIBLE_DEVICES"] == "2"
|
||
|
||
|
||
def test_check_resource_allocation_no_gpu_resource(monkeypatch):
|
||
"""
|
||
Test check_resource_allocation when CTEST is enabled but no GPU
|
||
resource is specified.Should return without setting HIP_VISIBLE_DEVICES.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying environment
|
||
"""
|
||
monkeypatch.setenv("CTEST_RESOURCE_GROUP_COUNT", "1")
|
||
monkeypatch.delenv("CTEST_RESOURCE_GROUP_0_GPUS", raising=False)
|
||
monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising=False)
|
||
|
||
from tests.test_utils import check_resource_allocation
|
||
|
||
result = check_resource_allocation()
|
||
|
||
assert result is None
|
||
assert "HIP_VISIBLE_DEVICES" not in os.environ
|
||
|
||
|
||
def test_check_resource_allocation_malformed_resource(monkeypatch):
|
||
"""
|
||
Test check_resource_allocation with malformed CTEST_RESOURCE_GROUP_0_GPUS format.
|
||
Should handle gracefully without crashing.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for modifying environment
|
||
"""
|
||
monkeypatch.setenv("CTEST_RESOURCE_GROUP_COUNT", "1")
|
||
monkeypatch.setenv("CTEST_RESOURCE_GROUP_0_GPUS", "malformed_resource_string")
|
||
monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising=False)
|
||
|
||
from tests.test_utils import check_resource_allocation
|
||
|
||
try:
|
||
result = check_resource_allocation()
|
||
assert result is None
|
||
except (ValueError, IndexError):
|
||
pass
|
||
|
||
|
||
# =============================================================================
|
||
# FILE PATTERN MATCHING TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_check_file_pattern_match_found():
|
||
"""
|
||
Test check_file_pattern when the pattern is found in the file.
|
||
Should return True.
|
||
"""
|
||
|
||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
||
f.write("This is a test file\nwith multiple lines\nand some pattern text\n")
|
||
temp_file_path = f.name
|
||
|
||
try:
|
||
result = check_file_pattern("pattern", temp_file_path)
|
||
assert result is True
|
||
|
||
result = check_file_pattern(r"test.*file", temp_file_path)
|
||
assert result is True
|
||
|
||
finally:
|
||
os.unlink(temp_file_path)
|
||
|
||
|
||
def test_check_file_pattern_file_not_found():
|
||
"""
|
||
Test check_file_pattern when the file doesn't exist.
|
||
Should raise FileNotFoundError.
|
||
"""
|
||
with pytest.raises(FileNotFoundError):
|
||
check_file_pattern("pattern", "/nonexistent/file/path.txt")
|
||
|
||
|
||
# =============================================================================
|
||
# TEXT PARSING UTILITIES TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_parse_text_basic(tmp_path):
|
||
"""Test parse_text with a simple valid input file.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts that counters are correctly extracted from a simple file.
|
||
"""
|
||
test_file = tmp_path / "test_counters.txt"
|
||
test_file.write_text("pmc: counter1 counter2 counter3")
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
assert result == ["counter1", "counter2", "counter3"]
|
||
|
||
|
||
def test_parse_text_empty_file(tmp_path):
|
||
"""Test parse_text with an empty file.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts that an empty file returns an empty list.
|
||
"""
|
||
test_file = tmp_path / "empty.txt"
|
||
test_file.write_text("")
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
assert result == []
|
||
|
||
|
||
def test_parse_text_no_pmc_entries(tmp_path):
|
||
"""Test parse_text with a file that doesn't contain any 'pmc:' entries.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts that a file without 'pmc:' returns an empty list.
|
||
"""
|
||
test_file = tmp_path / "no_pmc.txt"
|
||
test_file.write_text("line1\nline2\nline3")
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
assert result == []
|
||
|
||
|
||
def test_parse_text_with_comments(tmp_path):
|
||
"""Test parse_text with lines that have comments after the counters.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts that comments are properly stripped from counter lines.
|
||
"""
|
||
test_file = tmp_path / "comments.txt"
|
||
test_file.write_text("pmc: counter1 counter2 # This is a comment")
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
assert result == ["counter1", "counter2"]
|
||
|
||
|
||
def test_parse_text_multiple_lines(tmp_path):
|
||
"""Test parse_text with multiple 'pmc:' lines.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts counters from multiple lines are correctly combined.
|
||
"""
|
||
test_file = tmp_path / "multiple_lines.txt"
|
||
test_file.write_text("pmc: counter1 counter2\npmc: counter3 counter4")
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
assert result == ["counter1", "counter2", "counter3", "counter4"]
|
||
|
||
|
||
def test_parse_text_mixed_lines(tmp_path):
|
||
"""Test parse_text with a mix of 'pmc:' and non-'pmc:' lines.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts that only counters from 'pmc:' lines are extracted.
|
||
"""
|
||
test_file = tmp_path / "mixed_lines.txt"
|
||
test_file.write_text(
|
||
"line1\npmc: counter1 counter2\nline3\npmc: counter3 counter4\nline5"
|
||
)
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
assert result == ["counter1", "counter2", "counter3", "counter4"]
|
||
|
||
|
||
def test_parse_text_whitespace_handling(tmp_path):
|
||
"""Test parse_text with various whitespace combinations.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts that whitespace is properly handled in counter extraction.
|
||
"""
|
||
test_file = tmp_path / "whitespace.txt"
|
||
test_file.write_text("pmc: counter1\t\tcounter2 counter3")
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
|
||
result = [item for item in result if item.strip()]
|
||
|
||
expected = ["counter1", "counter2", "counter3"]
|
||
assert result == expected
|
||
|
||
test_file.write_text("pmc: counter1 counter2\npmc: counter3 counter4")
|
||
result = utils.parse_text(str(test_file))
|
||
result = [item for item in result if item.strip()]
|
||
expected = ["counter1", "counter2", "counter3", "counter4"]
|
||
assert result == expected
|
||
|
||
|
||
def test_parse_text_edge_cases(tmp_path):
|
||
"""Test parse_text with edge cases like empty 'pmc:' lines.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary path fixture provided by pytest.
|
||
|
||
Returns:
|
||
None: Asserts that edge cases are handled correctly.
|
||
"""
|
||
test_file = tmp_path / "edge_cases.txt"
|
||
test_file.write_text("pmc:\npmc: \npmc: counter1")
|
||
|
||
result = utils.parse_text(str(test_file))
|
||
result = [item for item in result if item.strip()]
|
||
assert result == ["counter1"]
|
||
|
||
|
||
def test_parse_text_file_not_found():
|
||
"""Test parse_text with a nonexistent file.
|
||
|
||
Returns:
|
||
None: Asserts that FileNotFoundError is raised for nonexistent files.
|
||
"""
|
||
with pytest.raises(FileNotFoundError):
|
||
utils.parse_text("nonexistent_file.txt")
|
||
|
||
|
||
# =============================================================================
|
||
# RUN_PROF TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_run_prof_success_v2(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof with rocprofv2 successful execution.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts successful execution and file creation.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
os.makedirs(workload_dir + "/out/pmc_1", exist_ok=True)
|
||
|
||
csv_content = "Dispatch_ID,GPU_ID,Kernel_Name\n0,0,test_kernel"
|
||
with open(workload_dir + "/out/pmc_1/results_0.csv", "w") as f:
|
||
f.write(csv_content)
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi250x"
|
||
self._l2_banks = 32
|
||
self.gpu_arch = "gfx90a"
|
||
self.compute_partition = "CPX"
|
||
|
||
mspec = MockSpec()
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv2")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: False)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr(
|
||
"glob.glob", lambda pattern: [workload_dir + "/out/pmc_1/results_0.csv"]
|
||
)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
assert Path(workload_dir + "/test.csv").exists()
|
||
|
||
|
||
def test_run_prof_success_v3_csv(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof with rocprofv3 using CSV format.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts successful execution with v3 CSV processing.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
os.makedirs(workload_dir + "/out/pmc_1", exist_ok=True)
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
csv_files = [workload_dir + "/out/pmc_1/converted.csv"]
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv3")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: True)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr(
|
||
"utils.utils.process_rocprofv3_output", lambda *a, **k: csv_files
|
||
)
|
||
|
||
mock_df = pd.DataFrame({"Dispatch_ID": [0], "GPU_ID": [0], "Kernel_Name": ["test"]})
|
||
monkeypatch.setattr("pandas.read_csv", lambda *a, **k: mock_df)
|
||
monkeypatch.setattr("pandas.concat", lambda *a, **k: mock_df)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
|
||
def test_run_prof_success_rocprofiler_sdk(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof with rocprofiler-sdk execution.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts successful execution with SDK configuration.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
profiler_options = {
|
||
"APP_CMD": ["./test_app"],
|
||
"ROCPROF_OUTPUT_PATH": workload_dir,
|
||
"ROCP_TOOL_LIBRARIES": "/opt/rocm/lib/rocprofiler-sdk/"
|
||
"librocprofiler-sdk-tool.so",
|
||
}
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofiler-sdk")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: True)
|
||
monkeypatch.setattr("utils.utils.parse_text", lambda f: ["SQ_WAVES"])
|
||
monkeypatch.setattr("utils.utils.process_rocprofv3_output", lambda *a, **k: [])
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(
|
||
str(fname), profiler_options, workload_dir, mspec, logging.INFO, "csv"
|
||
)
|
||
|
||
|
||
def test_run_prof_with_yaml_config(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof with additional YAML configuration file.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts YAML config is properly handled.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
yaml_file = tmp_path / "test.yaml"
|
||
yaml_file.write_text("counters:\n - TCC_HIT")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv3")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: True)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr("utils.utils.process_rocprofv3_output", lambda *a, **k: [])
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
monkeypatch.setattr(
|
||
"yaml.safe_load", lambda _: {"rocprofiler-sdk": {"counters": ["counter"]}}
|
||
)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
|
||
def test_run_prof_failure_subprocess(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof when subprocess execution fails.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts proper error handling on subprocess failure.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv3")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (False, "error output")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: True)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
def mock_console_error(msg, exit=True):
|
||
if exit:
|
||
raise RuntimeError("console_error called")
|
||
|
||
monkeypatch.setattr("utils.utils.console_error", mock_console_error)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(RuntimeError, match="console_error called"):
|
||
utils_mod.run_prof(
|
||
str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv"
|
||
)
|
||
|
||
|
||
def test_run_prof_mi300_environment_setup(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof sets proper environment variables for MI300 series GPUs.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts MI300 environment variable is set correctly.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
captured_env = {}
|
||
|
||
def mock_capture_subprocess_output(cmd, new_env=None, **kwargs):
|
||
if new_env:
|
||
captured_env.update(new_env)
|
||
return (True, "success")
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv3")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", mock_capture_subprocess_output
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: True)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr("utils.utils.process_rocprofv3_output", lambda *a, **k: [])
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
assert "ROCPROFILER_INDIVIDUAL_XCC_MODE" in captured_env
|
||
assert captured_env["ROCPROFILER_INDIVIDUAL_XCC_MODE"] == "1"
|
||
|
||
|
||
def test_run_prof_timestamps_special_case(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof handles timestamps.txt special case correctly.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts timestamps processing is handled correctly.
|
||
"""
|
||
fname = tmp_path / "timestamps.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
os.makedirs(workload_dir + "/out/pmc_1", exist_ok=True)
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
csv_content = "Dispatch_ID,Start_Timestamp,End_Timestamp\n0,100,200"
|
||
with open(workload_dir + "/kernel_trace.csv", "w") as f:
|
||
f.write(csv_content)
|
||
|
||
csv_files = [workload_dir + "/kernel_trace.csv"]
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv3")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: True)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr(
|
||
"utils.utils.process_rocprofv3_output", lambda *a, **k: csv_files
|
||
)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
|
||
mock_df = pd.DataFrame({
|
||
"Dispatch_ID": [0],
|
||
"Start_Timestamp": [100],
|
||
"End_Timestamp": [200],
|
||
})
|
||
monkeypatch.setattr("pandas.read_csv", lambda *a, **k: mock_df)
|
||
monkeypatch.setattr("pandas.concat", lambda *a, **k: mock_df)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
|
||
def test_run_prof_no_results_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof when no results files are generated.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts proper handling when no results are found.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv2")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: False)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr("glob.glob", lambda pattern: []) # No files found
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
|
||
def test_run_prof_header_standardization(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof properly standardizes CSV headers.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts CSV headers are standardized correctly.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: SQ_WAVES")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
os.makedirs(workload_dir + "/out/pmc_1", exist_ok=True)
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
csv_content = (
|
||
"KernelName,Index,grd,gpu-id,BeginNs,EndNs\ntest_kernel,0,64,0,100,200"
|
||
)
|
||
with open(workload_dir + "/out/pmc_1/results_test.csv", "w") as f:
|
||
f.write(csv_content)
|
||
|
||
old_headers_df = pd.DataFrame({
|
||
"KernelName": ["test_kernel"],
|
||
"Index": [0],
|
||
"grd": [64],
|
||
"gpu-id": [0],
|
||
"BeginNs": [100],
|
||
"EndNs": [200],
|
||
})
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv2")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: False)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr(
|
||
"glob.glob", lambda pattern: [workload_dir + "/out/pmc_1/results_test.csv"]
|
||
)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
read_calls = []
|
||
|
||
def mock_read_csv(path, **kwargs):
|
||
read_calls.append(path)
|
||
return old_headers_df.copy()
|
||
|
||
write_calls = []
|
||
|
||
def mock_to_csv(self, path, **kwargs):
|
||
write_calls.append((path, self.columns.tolist()))
|
||
|
||
monkeypatch.setattr("pandas.read_csv", mock_read_csv)
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv)
|
||
monkeypatch.setattr("pandas.concat", lambda dfs, **k: old_headers_df.copy())
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
final_headers = write_calls[-1][1] if write_calls else []
|
||
assert "Kernel_Name" in final_headers
|
||
assert "Dispatch_ID" in final_headers
|
||
assert "Grid_Size" in final_headers
|
||
assert "GPU_ID" in final_headers
|
||
assert "Start_Timestamp" in final_headers
|
||
assert "End_Timestamp" in final_headers
|
||
|
||
|
||
def test_run_prof_tcc_flattening_mi300(tmp_path, monkeypatch):
|
||
"""
|
||
Test run_prof applies TCC flattening for MI300 series GPUs.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts TCC flattening is applied for MI300 GPUs.
|
||
"""
|
||
fname = tmp_path / "test.txt"
|
||
fname.write_text("pmc: TCC_HIT[0]")
|
||
workload_dir = str(tmp_path / "workload")
|
||
|
||
class MockSpec:
|
||
def __init__(self):
|
||
self.gpu_model = "mi300x"
|
||
self.gpu_arch = "gfx942"
|
||
self.compute_partition = "SPX"
|
||
self._l2_banks = 32
|
||
|
||
mspec = MockSpec()
|
||
|
||
flatten_called = False
|
||
|
||
def mock_flatten_tcc_info_across_xcds(file, xcds, l2_banks):
|
||
nonlocal flatten_called
|
||
flatten_called = True
|
||
return pd.DataFrame({
|
||
"Dispatch_ID": [0],
|
||
"TCC_HIT[0]": [100],
|
||
"TCC_HIT[16]": [200],
|
||
})
|
||
|
||
# Mock functions
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofv2")
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "success")
|
||
)
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: False)
|
||
monkeypatch.setattr("utils.utils.using_v1", lambda: False)
|
||
monkeypatch.setattr(
|
||
"utils.utils.flatten_tcc_info_across_xcds", mock_flatten_tcc_info_across_xcds
|
||
)
|
||
monkeypatch.setattr("utils.utils.mi_gpu_specs.get_num_xcds", lambda *a: 2)
|
||
monkeypatch.setattr(
|
||
"glob.glob", lambda pattern: [workload_dir + "/results_test.csv"]
|
||
)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
# Mock pandas
|
||
mock_df = pd.DataFrame({"Dispatch_ID": [0], "TCC_HIT[0]": [100]})
|
||
monkeypatch.setattr("pandas.read_csv", lambda *a, **k: mock_df)
|
||
monkeypatch.setattr("pandas.concat", lambda *a, **k: mock_df)
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", lambda self, *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
# Execute function
|
||
utils_mod.run_prof(str(fname), ["--arg"], workload_dir, mspec, logging.INFO, "csv")
|
||
|
||
assert flatten_called
|
||
|
||
|
||
import utils.utils as utils_mod # noqa
|
||
|
||
|
||
class MockMSpec:
|
||
def __init__(
|
||
self, gpu_model="mi300a", gpu_arch="gfx942", compute_partition=None, l2_banks=32
|
||
):
|
||
self.gpu_model = gpu_model
|
||
self.gpu_arch = gpu_arch
|
||
self.compute_partition = compute_partition
|
||
self._l2_banks = l2_banks
|
||
|
||
|
||
def test_run_prof_sdk_creates_new_env_copy(tmp_path, monkeypatch):
|
||
"""
|
||
Covers: new_env = os.environ.copy()
|
||
when rocprof_cmd == "rocprofiler-sdk" and new_env was not previously set
|
||
by the mspec.gpu_model check.
|
||
"""
|
||
fname_str = str(tmp_path / "counters.txt")
|
||
pathlib.Path(fname_str).touch()
|
||
workload_dir_str = str(tmp_path)
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofiler-sdk")
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: False)
|
||
monkeypatch.setattr("utils.utils.process_rocprofv3_output", lambda *a, **k: [])
|
||
|
||
capture_subprocess_called_with_env = None
|
||
|
||
def mock_capture_subprocess(app_cmd, new_env=None, profileMode=False):
|
||
nonlocal capture_subprocess_called_with_env
|
||
capture_subprocess_called_with_env = new_env
|
||
return (True, "Success")
|
||
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", mock_capture_subprocess
|
||
)
|
||
|
||
def mock_console_error_no_exit(msg, exit=True):
|
||
print(f"Mocked console_error: {msg}, exit={exit}")
|
||
|
||
monkeypatch.setattr("utils.utils.console_error", mock_console_error_no_exit)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr(
|
||
"utils.utils.parse_text", lambda *a, **k: ["COUNTER1", "COUNTER2"]
|
||
)
|
||
|
||
mock_fname_path_obj = mock.Mock(spec=pathlib.Path)
|
||
mock_fname_path_obj.stem = "counters"
|
||
mock_fname_path_obj.name = "counters.txt"
|
||
mock_fname_path_obj.with_suffix.return_value.exists.return_value = False
|
||
mock_out_path_obj = mock.Mock(spec=pathlib.Path)
|
||
mock_out_path_obj.exists.return_value = False
|
||
|
||
def path_side_effect(p_arg, *args):
|
||
if isinstance(p_arg, pathlib.Path):
|
||
if p_arg.name == "counters.txt":
|
||
return mock_fname_path_obj
|
||
return p_arg
|
||
if isinstance(p_arg, str):
|
||
if p_arg.endswith("/out"):
|
||
return mock_out_path_obj
|
||
if p_arg.endswith("counters.txt"):
|
||
return mock_fname_path_obj
|
||
if (
|
||
p_arg == mock_fname_path_obj
|
||
and args == ()
|
||
and hasattr(p_arg, "with_suffix")
|
||
):
|
||
return mock_fname_path_obj
|
||
return mock_fname_path_obj
|
||
|
||
monkeypatch.setattr("utils.utils.path", path_side_effect)
|
||
|
||
original_env_var = "original_value"
|
||
monkeypatch.setenv("EXISTING_VAR", original_env_var)
|
||
monkeypatch.delenv("ROCPROFILER_INDIVIDUAL_XCC_MODE", raising=False)
|
||
|
||
profiler_options = {"APP_CMD": "my_app --arg"}
|
||
mspec = MockMSpec(gpu_model="mi250")
|
||
loglevel = logging.DEBUG
|
||
format_rocprof_output = True
|
||
|
||
dummy_df = pd.DataFrame({"Dispatch_ID": [0], "A": [1]})
|
||
monkeypatch.setattr("pandas.read_csv", lambda *a, **k: dummy_df.copy())
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", lambda self, *a, **k: None)
|
||
monkeypatch.setattr("shutil.copyfile", lambda *a, **k: None)
|
||
monkeypatch.setattr("shutil.rmtree", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
|
||
utils_mod.run_prof(
|
||
fname_str,
|
||
profiler_options.copy(),
|
||
workload_dir_str,
|
||
mspec,
|
||
loglevel,
|
||
format_rocprof_output,
|
||
)
|
||
|
||
assert capture_subprocess_called_with_env is not None, (
|
||
"new_env should have been created"
|
||
)
|
||
assert "EXISTING_VAR" in capture_subprocess_called_with_env, (
|
||
"new_env should be a copy of os.environ"
|
||
)
|
||
assert capture_subprocess_called_with_env["EXISTING_VAR"] == original_env_var
|
||
assert "ROCPROF_COUNTERS" in capture_subprocess_called_with_env
|
||
assert "APP_CMD" not in capture_subprocess_called_with_env
|
||
|
||
|
||
def test_run_prof_v3_sdk_and_cli_calls_trace_processing(tmp_path, monkeypatch):
|
||
"""
|
||
Covers:
|
||
Line 3 (SDK): if "ROCPROF_HIP_RUNTIME_API_TRACE" in options:
|
||
process_hip_trace_output(...)
|
||
Line 4 (CLI): if "--kokkos-trace" in options:
|
||
process_kokkos_trace_output(...)
|
||
Line 5 (CLI): elif "--hip-trace" in options:
|
||
process_hip_trace_output(...)
|
||
"""
|
||
fname_str = str(tmp_path / "counters.txt")
|
||
pathlib.Path(fname_str).touch()
|
||
fbase_str = "counters"
|
||
workload_dir_str = str(tmp_path)
|
||
(tmp_path / "out" / "pmc_1").mkdir(parents=True, exist_ok=True)
|
||
|
||
monkeypatch.setattr(
|
||
"utils.utils.capture_subprocess_output", lambda *a, **k: (True, "Success")
|
||
)
|
||
monkeypatch.setattr(
|
||
"utils.utils.process_rocprofv3_output",
|
||
lambda *a, **k: [str(tmp_path / "results1.csv")],
|
||
)
|
||
|
||
hip_trace_called_with = None
|
||
|
||
def mock_hip_trace(wd, fb):
|
||
nonlocal hip_trace_called_with
|
||
hip_trace_called_with = (wd, fb)
|
||
|
||
monkeypatch.setattr("utils.utils.process_hip_trace_output", mock_hip_trace)
|
||
|
||
kokkos_trace_called_with = None
|
||
|
||
def mock_kokkos_trace(wd, fb):
|
||
nonlocal kokkos_trace_called_with
|
||
kokkos_trace_called_with = (wd, fb)
|
||
|
||
monkeypatch.setattr("utils.utils.process_kokkos_trace_output", mock_kokkos_trace)
|
||
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.parse_text", lambda *a, **k: ["C1"])
|
||
|
||
mock_fname_path_obj = mock.MagicMock(spec=pathlib.Path)
|
||
mock_fname_path_obj.stem = fbase_str
|
||
mock_fname_path_obj.name = "counters.txt"
|
||
mock_fname_path_obj.with_suffix.return_value.exists.return_value = False
|
||
mock_fname_path_obj.__truediv__.return_value = mock.Mock(spec=pathlib.Path)
|
||
|
||
mock_out_path_obj = mock.MagicMock(spec=pathlib.Path)
|
||
mock_out_path_obj.exists.return_value = True
|
||
|
||
def path_side_effect(p_arg, *args):
|
||
if isinstance(p_arg, pathlib.Path) and p_arg.name == "counters.txt":
|
||
return mock_fname_path_obj
|
||
if isinstance(p_arg, str) and p_arg.endswith("/out"):
|
||
return mock_out_path_obj
|
||
if isinstance(p_arg, str) and p_arg.endswith("counters.txt"):
|
||
return mock_fname_path_obj
|
||
if (
|
||
p_arg == mock_fname_path_obj
|
||
and args == ()
|
||
and hasattr(p_arg, "with_suffix")
|
||
):
|
||
return mock_fname_path_obj
|
||
return mock_fname_path_obj
|
||
|
||
monkeypatch.setattr("utils.utils.path", path_side_effect)
|
||
|
||
dummy_df = pd.DataFrame({"Dispatch_ID": [0], "A": [1]})
|
||
monkeypatch.setattr("pandas.read_csv", lambda *a, **k: dummy_df.copy())
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", lambda self, *a, **k: None)
|
||
monkeypatch.setattr("shutil.copyfile", lambda *a, **k: None)
|
||
monkeypatch.setattr("shutil.rmtree", lambda *a, **k: None)
|
||
monkeypatch.setattr("builtins.open", lambda *a, **k: io.StringIO(""))
|
||
monkeypatch.setattr("utils.utils.flatten_tcc_info_across_xcds", lambda df, *a: df)
|
||
monkeypatch.setattr("utils.utils.mi_gpu_specs.get_num_xcds", lambda *a: 1)
|
||
|
||
mspec = MockMSpec()
|
||
loglevel = logging.INFO
|
||
format_rocprof_output = True
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprofiler-sdk")
|
||
monkeypatch.setattr("utils.utils.using_v3", lambda: True)
|
||
|
||
profiler_options_sdk_hip = {
|
||
"APP_CMD": "my_app",
|
||
"ROCPROF_HIP_RUNTIME_API_TRACE": "1",
|
||
"ROCP_TOOL_LIBRARIES": "/opt/rocm/lib/rocprofiler-sdk/"
|
||
"librocprofiler-sdk-tool.so",
|
||
}
|
||
hip_trace_called_with = None
|
||
kokkos_trace_called_with = None
|
||
|
||
utils_mod.run_prof(
|
||
fname_str,
|
||
profiler_options_sdk_hip.copy(),
|
||
workload_dir_str,
|
||
mspec,
|
||
loglevel,
|
||
format_rocprof_output,
|
||
)
|
||
assert hip_trace_called_with == (workload_dir_str, fbase_str)
|
||
assert kokkos_trace_called_with is None
|
||
|
||
monkeypatch.setattr("utils.utils.rocprof_cmd", "rocprof_cli_v3")
|
||
|
||
profiler_options_cli_kokkos = ["--kokkos-trace", "--other-opt"]
|
||
hip_trace_called_with = None
|
||
kokkos_trace_called_with = None
|
||
|
||
utils_mod.run_prof(
|
||
fname_str,
|
||
profiler_options_cli_kokkos,
|
||
workload_dir_str,
|
||
mspec,
|
||
loglevel,
|
||
format_rocprof_output,
|
||
)
|
||
assert kokkos_trace_called_with == (workload_dir_str, fbase_str)
|
||
assert hip_trace_called_with is None
|
||
|
||
profiler_options_cli_hip = ["--hip-trace", "--other-opt"]
|
||
hip_trace_called_with = None
|
||
kokkos_trace_called_with = None
|
||
|
||
utils_mod.run_prof(
|
||
fname_str,
|
||
profiler_options_cli_hip,
|
||
workload_dir_str,
|
||
mspec,
|
||
loglevel,
|
||
format_rocprof_output,
|
||
)
|
||
assert hip_trace_called_with == (workload_dir_str, fbase_str)
|
||
assert kokkos_trace_called_with is None
|
||
|
||
|
||
# =============================================================================
|
||
# ROCPROFV3 OUTPUT PROCESSING TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_process_rocprofv3_output_json_format(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_rocprofv3_output with json format converts JSON files to CSV.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts CSV files are created from JSON files.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
output_dir = tmp_path / "out" / "pmc_1" / "subdir"
|
||
output_dir.mkdir(parents=True)
|
||
|
||
json_file1 = output_dir / "test1.json"
|
||
json_file2 = output_dir / "test2.json"
|
||
json_file1.write_text('{"test": "data1"}')
|
||
json_file2.write_text('{"test": "data2"}')
|
||
|
||
monkeypatch.setattr("glob.glob", lambda pattern: [str(json_file1), str(json_file2)])
|
||
|
||
def mock_v3_json_to_csv(json_path, csv_path):
|
||
Path(csv_path).write_text("csv,data\ntest,value")
|
||
|
||
monkeypatch.setattr("utils.utils.v3_json_to_csv", mock_v3_json_to_csv)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.process_rocprofv3_output("json", workload_dir, False)
|
||
|
||
assert len(result) == 2
|
||
csv_file1 = output_dir / "test1.csv"
|
||
csv_file2 = output_dir / "test2.csv"
|
||
assert csv_file1.exists()
|
||
assert csv_file2.exists()
|
||
|
||
|
||
def test_process_rocprofv3_output_csv_format_with_counter_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_rocprofv3_output with csv format processes counter collection files.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts counter files are converted properly.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
output_dir = tmp_path / "out" / "pmc_1" / "subdir"
|
||
output_dir.mkdir(parents=True)
|
||
|
||
counter_file = output_dir / "test_counter_collection.csv"
|
||
agent_file = output_dir / "test_agent_info.csv"
|
||
converted_file = output_dir / "test_converted.csv"
|
||
|
||
counter_file.write_text("counter,data\ntest,value")
|
||
agent_file.write_text("agent,data\ntest,value")
|
||
|
||
def mock_glob(pattern):
|
||
if "_counter_collection.csv" in pattern:
|
||
return [str(counter_file)]
|
||
elif "_converted.csv" in pattern:
|
||
return [str(converted_file)]
|
||
return []
|
||
|
||
monkeypatch.setattr("glob.glob", mock_glob)
|
||
|
||
def mock_v3_counter_csv_to_v2_csv(counter_path, agent_path, output_path):
|
||
Path(output_path).write_text("converted,data\ntest,value")
|
||
|
||
monkeypatch.setattr(
|
||
"utils.utils.v3_counter_csv_to_v2_csv", mock_v3_counter_csv_to_v2_csv
|
||
)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.process_rocprofv3_output("csv", workload_dir, False)
|
||
|
||
assert len(result) == 1
|
||
assert str(converted_file) in result
|
||
|
||
|
||
def test_process_rocprofv3_output_csv_format_conversion_error(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_rocprofv3_output handles conversion errors gracefully.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts empty list returned when conversion fails.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
output_dir = tmp_path / "out" / "pmc_1" / "subdir"
|
||
output_dir.mkdir(parents=True)
|
||
|
||
counter_file = output_dir / "test_counter_collection.csv"
|
||
agent_file = output_dir / "test_agent_info.csv"
|
||
|
||
counter_file.write_text("counter,data\ntest,value")
|
||
agent_file.write_text("agent,data\ntest,value")
|
||
|
||
def mock_glob(pattern):
|
||
if "_counter_collection.csv" in pattern:
|
||
return [str(counter_file)]
|
||
return []
|
||
|
||
monkeypatch.setattr("glob.glob", mock_glob)
|
||
|
||
def mock_v3_counter_csv_to_v2_csv(counter_path, agent_path, output_path):
|
||
raise ValueError("Conversion failed")
|
||
|
||
monkeypatch.setattr(
|
||
"utils.utils.v3_counter_csv_to_v2_csv", mock_v3_counter_csv_to_v2_csv
|
||
)
|
||
|
||
warnings = []
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda msg: warnings.append(msg))
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.process_rocprofv3_output("csv", workload_dir, False)
|
||
|
||
assert result == []
|
||
assert len(warnings) == 1
|
||
assert "Error converting" in warnings[0]
|
||
|
||
|
||
def test_process_rocprofv3_output_csv_format_missing_agent_file(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_rocprofv3_output raises error when agent info file is missing.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts ValueError is raised for missing agent file.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
output_dir = tmp_path / "out" / "pmc_1" / "subdir"
|
||
output_dir.mkdir(parents=True)
|
||
|
||
counter_file = output_dir / "test_counter_collection.csv"
|
||
counter_file.write_text("counter,data\ntest,value")
|
||
|
||
def mock_glob(pattern):
|
||
if "_counter_collection.csv" in pattern:
|
||
return [str(counter_file)]
|
||
return []
|
||
|
||
monkeypatch.setattr("glob.glob", mock_glob)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(ValueError, match='has no coresponding "agent info" file'):
|
||
utils_mod.process_rocprofv3_output("csv", workload_dir, False)
|
||
|
||
|
||
def test_process_rocprofv3_output_csv_format_timestamps_fallback(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_rocprofv3_output falls back to kernel trace files for timestamps.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts kernel trace files are used when is_timestamps is True.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
output_dir = tmp_path / "out" / "pmc_1" / "subdir"
|
||
output_dir.mkdir(parents=True)
|
||
|
||
trace_file = output_dir / "test_kernel_trace.csv"
|
||
trace_file.write_text("kernel,trace\ntest,data")
|
||
|
||
def mock_glob(pattern):
|
||
if "_counter_collection.csv" in pattern:
|
||
return []
|
||
elif "_kernel_trace.csv" in pattern:
|
||
return [str(trace_file)]
|
||
return []
|
||
|
||
monkeypatch.setattr("glob.glob", mock_glob)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.process_rocprofv3_output("csv", workload_dir, True)
|
||
|
||
assert len(result) == 1
|
||
assert str(trace_file) in result
|
||
|
||
|
||
def test_process_rocprofv3_output_csv_format_no_files_non_timestamps(
|
||
tmp_path, monkeypatch
|
||
):
|
||
"""
|
||
Test process_rocprofv3_output returns empty list when
|
||
no files found for non-timestamps.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts empty list returned when no counter files exist.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
|
||
monkeypatch.setattr("glob.glob", lambda pattern: [])
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.process_rocprofv3_output("csv", workload_dir, False)
|
||
|
||
assert result == []
|
||
|
||
|
||
def test_process_rocprofv3_output_invalid_format(monkeypatch):
|
||
"""
|
||
Test process_rocprofv3_output raises error for invalid output format.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts console_error is called for invalid format.
|
||
"""
|
||
|
||
def mock_console_error(msg):
|
||
raise RuntimeError(f"console_error: {msg}")
|
||
|
||
monkeypatch.setattr("utils.utils.console_error", mock_console_error)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(
|
||
RuntimeError, match="The output file of rocprofv3 can only support json or csv"
|
||
):
|
||
utils_mod.process_rocprofv3_output("invalid", "/tmp", False)
|
||
|
||
|
||
def test_process_rocprofv3_output_json_format_no_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_rocprofv3_output with json format when no JSON files exist.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts empty list returned when no JSON files found.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
|
||
monkeypatch.setattr("glob.glob", lambda pattern: [])
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.process_rocprofv3_output("json", workload_dir, False)
|
||
|
||
assert result == []
|
||
|
||
|
||
def test_process_rocprofv3_output_csv_format_multiple_counter_files(
|
||
tmp_path, monkeypatch
|
||
):
|
||
"""
|
||
Test process_rocprofv3_output processes multiple counter collection files.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts multiple counter files are processed correctly.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
output_dir = tmp_path / "out" / "pmc_1" / "subdir"
|
||
output_dir.mkdir(parents=True)
|
||
|
||
counter_file1 = output_dir / "test1_counter_collection.csv"
|
||
agent_file1 = output_dir / "test1_agent_info.csv"
|
||
converted_file1 = output_dir / "test1_converted.csv"
|
||
|
||
counter_file2 = output_dir / "test2_counter_collection.csv"
|
||
agent_file2 = output_dir / "test2_agent_info.csv"
|
||
converted_file2 = output_dir / "test2_converted.csv"
|
||
|
||
counter_file1.write_text("counter,data\ntest1,value1")
|
||
agent_file1.write_text("agent,data\ntest1,value1")
|
||
counter_file2.write_text("counter,data\ntest2,value2")
|
||
agent_file2.write_text("agent,data\ntest2,value2")
|
||
|
||
def mock_glob(pattern):
|
||
if "_counter_collection.csv" in pattern:
|
||
return [str(counter_file1), str(counter_file2)]
|
||
elif "_converted.csv" in pattern:
|
||
return [str(converted_file1), str(converted_file2)]
|
||
return []
|
||
|
||
monkeypatch.setattr("glob.glob", mock_glob)
|
||
|
||
def mock_v3_counter_csv_to_v2_csv(counter_path, agent_path, output_path):
|
||
Path(output_path).write_text(f"converted,data\n{Path(counter_path).stem},value")
|
||
|
||
monkeypatch.setattr(
|
||
"utils.utils.v3_counter_csv_to_v2_csv", mock_v3_counter_csv_to_v2_csv
|
||
)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.process_rocprofv3_output("csv", workload_dir, False)
|
||
|
||
assert len(result) == 2
|
||
assert str(converted_file1) in result
|
||
assert str(converted_file2) in result
|
||
|
||
|
||
def test_capture_subprocess_output_with_logging_disabled(monkeypatch):
|
||
"""
|
||
Test capture_subprocess_output with enable_logging=False doesn't call console_log.
|
||
"""
|
||
|
||
class DummyProcess:
|
||
def __init__(self):
|
||
self.stdout = io.StringIO("test output\n")
|
||
|
||
def poll(self):
|
||
return 0
|
||
|
||
def wait(self):
|
||
return 0
|
||
|
||
monkeypatch.setattr("subprocess.Popen", lambda *a, **k: DummyProcess())
|
||
monkeypatch.setattr(
|
||
"selectors.DefaultSelector",
|
||
lambda: mock.Mock(register=mock.Mock(), select=lambda: [], close=mock.Mock()),
|
||
)
|
||
|
||
log_calls = []
|
||
monkeypatch.setattr(
|
||
"utils.utils.console_log", lambda *a, **k: log_calls.append((a, k))
|
||
)
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
success, output = utils_mod.capture_subprocess_output(
|
||
["echo", "test"], enable_logging=False
|
||
)
|
||
|
||
assert success is True
|
||
assert len(log_calls) == 0
|
||
|
||
|
||
# =============================================================================
|
||
# KOKKOS TRACE PROCESSING TESTS
|
||
# =============================================================================
|
||
|
||
|
||
def test_process_kokkos_trace_output_single_file(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output with a single CSV file.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that single file is processed correctly
|
||
and output files are created.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "single_marker_api_trace.csv"
|
||
csv1.write_text(
|
||
"marker_id,marker_name,start_time,end_time\n1,kokkos_begin,1000,1050\n2,kokkos_end,2000,2010\n"
|
||
)
|
||
|
||
fbase = "single_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
# Check output file in pmc_1 directory
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 2
|
||
assert df["marker_name"].tolist() == ["kokkos_begin", "kokkos_end"]
|
||
|
||
# Check copied file in workload directory
|
||
copied_file = tmp_path / f"{fbase}_marker_api_trace.csv"
|
||
assert copied_file.exists()
|
||
|
||
|
||
def test_process_kokkos_trace_output_multiple_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output with multiple valid CSV files.
|
||
Should concatenate all files and save the result.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub2 = out_dir / "process2"
|
||
sub1.mkdir()
|
||
sub2.mkdir()
|
||
|
||
csv1 = sub1 / "test_marker_api_trace.csv"
|
||
csv2 = sub2 / "test_marker_api_trace.csv"
|
||
csv1.write_text(
|
||
"timestamp,marker_name,duration\n1000,kokkos_malloc,500\n2000,kokkos_parallel_for,300\n"
|
||
)
|
||
csv2.write_text(
|
||
"timestamp,marker_name,duration\n3000,kokkos_free,200\n4000,kokkos_parallel_reduce,800\n"
|
||
)
|
||
|
||
fbase = "test_workload"
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists(), "The primary output file was not created."
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 4, (
|
||
"The final DataFrame does not contain the correct number of rows."
|
||
)
|
||
assert set(df["timestamp"]) == {1000, 2000, 3000, 4000}
|
||
assert "kokkos_malloc" in df["marker_name"].values
|
||
assert "kokkos_parallel_reduce" in df["marker_name"].values
|
||
|
||
|
||
def test_process_kokkos_trace_output_no_files_found(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output when no marker API trace files are found.
|
||
Should handle empty file list gracefully.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that function handles empty file list without crashing.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
fbase = "no_files"
|
||
|
||
def mock_concat(dataframes, **kwargs):
|
||
if not dataframes:
|
||
return pd.DataFrame()
|
||
return pd.concat(dataframes, **kwargs)
|
||
|
||
monkeypatch.setattr("pandas.concat", mock_concat)
|
||
|
||
def mock_to_csv(self, path, **kwargs):
|
||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
with open(path, "w") as f:
|
||
f.write("")
|
||
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
except ValueError:
|
||
# pandas.concat() raises ValueError when passed empty list
|
||
pytest.skip(
|
||
"process_kokkos_trace_output doesn't handle empty file list gracefully"
|
||
)
|
||
|
||
|
||
def test_process_kokkos_trace_output_mixed_file_states(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output with a mix of valid, empty, and corrupted files.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that valid files are processed while invalid
|
||
ones are handled gracefully.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub2 = out_dir / "process2"
|
||
sub3 = out_dir / "process3"
|
||
sub1.mkdir()
|
||
sub2.mkdir()
|
||
sub3.mkdir()
|
||
|
||
csv1 = sub1 / "valid_marker_api_trace.csv"
|
||
csv1.write_text("timestamp,marker_name\n1000,kokkos_malloc\n2000,kokkos_free\n")
|
||
|
||
csv2 = sub2 / "empty_marker_api_trace.csv"
|
||
csv2.write_text("")
|
||
|
||
csv3 = sub3 / "headers_marker_api_trace.csv"
|
||
csv3.write_text("timestamp,marker_name\n")
|
||
|
||
fbase = "mixed_test"
|
||
|
||
original_read_csv = pd.read_csv
|
||
|
||
def mock_read_csv(filepath, **kwargs):
|
||
try:
|
||
return original_read_csv(filepath, **kwargs)
|
||
except pd.errors.EmptyDataError:
|
||
# Return empty DataFrame for empty files
|
||
return pd.DataFrame()
|
||
|
||
monkeypatch.setattr("pandas.read_csv", mock_read_csv)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) >= 0
|
||
|
||
|
||
def test_process_kokkos_trace_output_no_out_directory(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output when output directory doesn't exist.
|
||
Should not copy file to workload directory.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that function handles missing
|
||
output directory gracefully.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
|
||
fbase = "no_out_dir"
|
||
|
||
monkeypatch.setattr("glob.glob", lambda pattern: [])
|
||
|
||
def mock_concat(dataframes, **kwargs):
|
||
if not dataframes:
|
||
return pd.DataFrame()
|
||
return pd.concat(dataframes, **kwargs)
|
||
|
||
monkeypatch.setattr("pandas.concat", mock_concat)
|
||
|
||
def mock_to_csv(self, path, **kwargs):
|
||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
with open(path, "w") as f:
|
||
f.write("")
|
||
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv)
|
||
|
||
original_path = utils.path
|
||
|
||
def mock_path_exists(path_str):
|
||
if path_str == workload_dir + "/out":
|
||
mock_path_obj = mock.MagicMock()
|
||
mock_path_obj.exists.return_value = False
|
||
return mock_path_obj
|
||
else:
|
||
return original_path(path_str)
|
||
|
||
monkeypatch.setattr("utils.utils.path", mock_path_exists)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
# Should not copy file to workload directory since /out doesn't exist
|
||
copied_file = tmp_path / f"{fbase}_marker_api_trace.csv"
|
||
assert not copied_file.exists()
|
||
|
||
except ValueError:
|
||
pytest.skip(
|
||
"process_kokkos_trace_output doesn't handle missing "
|
||
"output directory gracefully"
|
||
)
|
||
|
||
|
||
def test_process_kokkos_trace_output_csv_with_only_headers(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output with CSV files that contain
|
||
only headers but no data.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that header-only files result in empty DataFrame.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "headers_only_marker_api_trace.csv"
|
||
csv1.write_text("timestamp,marker_name,duration,thread_id\n")
|
||
|
||
fbase = "headers_only"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 0
|
||
assert list(df.columns) == ["timestamp", "marker_name", "duration", "thread_id"]
|
||
|
||
|
||
def test_process_kokkos_trace_output_large_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output with larger CSV files to ensure memory handling.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that large files are processed correctly.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "large_marker_api_trace.csv"
|
||
|
||
content = "timestamp,marker_name,duration,thread_id\n"
|
||
kokkos_markers = [
|
||
"kokkos_malloc",
|
||
"kokkos_free",
|
||
"kokkos_parallel_for",
|
||
"kokkos_parallel_reduce",
|
||
"kokkos_fence",
|
||
]
|
||
for i in range(1000):
|
||
marker_name = kokkos_markers[i % len(kokkos_markers)]
|
||
content += f"{i},{marker_name},{i % 100},{i % 10}\n"
|
||
|
||
csv1.write_text(content)
|
||
|
||
fbase = "large_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 1000
|
||
assert "kokkos_malloc" in df["marker_name"].values
|
||
assert "kokkos_parallel_reduce" in df["marker_name"].values
|
||
|
||
|
||
def test_process_kokkos_trace_output_unicode_content(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output with CSV files containing unicode characters.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that unicode content is handled properly.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "unicode_marker_api_trace.csv"
|
||
csv1.write_text(
|
||
"timestamp,marker_name,duration\n1000,kokkos_α_kernel,500\n2000,kokkos_β_operation,300\n",
|
||
encoding="utf-8",
|
||
)
|
||
|
||
fbase = "unicode_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 2
|
||
assert "kokkos_α_kernel" in df["marker_name"].values
|
||
assert "kokkos_β_operation" in df["marker_name"].values
|
||
|
||
|
||
def test_process_kokkos_trace_output_different_schemas(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output with CSV files having different column schemas.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that files with different schemas are concatenated properly.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub2 = out_dir / "process2"
|
||
sub1.mkdir()
|
||
sub2.mkdir()
|
||
|
||
csv1 = sub1 / "schema1_marker_api_trace.csv"
|
||
csv2 = sub2 / "schema2_marker_api_trace.csv"
|
||
|
||
# Different column order and types
|
||
csv1.write_text(
|
||
"marker_id,marker_name,start_time\n1,kokkos_begin,1000\n2,kokkos_end,2000\n"
|
||
)
|
||
csv2.write_text(
|
||
"marker_name,duration,thread_id\nkokkos_malloc,500,0\nkokkos_free,200,1\n"
|
||
)
|
||
|
||
fbase = "schema_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_marker_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 4
|
||
# Should have union of all columns with NaN for missing values
|
||
expected_columns = [
|
||
"marker_id",
|
||
"marker_name",
|
||
"start_time",
|
||
"duration",
|
||
"thread_id",
|
||
]
|
||
assert all(col in df.columns for col in expected_columns)
|
||
|
||
|
||
def test_process_kokkos_trace_output_permission_error(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_kokkos_trace_output when there are permission
|
||
errors during file operations.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that permission errors are handled gracefully.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "test_marker_api_trace.csv"
|
||
csv1.write_text("timestamp,marker_name\n1000,kokkos_malloc\n")
|
||
|
||
fbase = "permission_test"
|
||
|
||
def mock_to_csv_permission_error(self, path, **kwargs):
|
||
raise PermissionError("Permission denied")
|
||
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv_permission_error)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(PermissionError):
|
||
utils_mod.process_kokkos_trace_output(workload_dir, fbase)
|
||
|
||
|
||
# =============================================================================
|
||
# HIP TRACE PROCESSING TESTS
|
||
# =============================================================================
|
||
"""
|
||
These test cases comprehensively cover:
|
||
|
||
Multiple valid CSV files concatenation
|
||
Single file processing
|
||
Different CSV schemas handling
|
||
Edge Cases:
|
||
|
||
No files found
|
||
Files listed by glob but don't exist
|
||
Empty CSV files
|
||
CSV files with only headers
|
||
Corrupted/malformed CSV data
|
||
Error Conditions:
|
||
|
||
Permission errors during file operations
|
||
Invalid filename characters
|
||
Output directory doesn't exist
|
||
Performance & Special Content:
|
||
|
||
Large files (memory handling)
|
||
Unicode content handling
|
||
Mixed file states (valid, empty, corrupted)
|
||
File System Edge Cases:
|
||
|
||
Missing output directory for copy operation
|
||
File I/O errors
|
||
"""
|
||
|
||
|
||
def test_process_hip_trace_output_multiple_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with multiple valid CSV files.
|
||
Should concatenate all files and save the result.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_warning", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub2 = out_dir / "process2"
|
||
sub1.mkdir()
|
||
sub2.mkdir()
|
||
|
||
csv1 = sub1 / "test_hip_api_trace.csv"
|
||
csv2 = sub2 / "test_hip_api_trace.csv"
|
||
csv1.write_text(
|
||
"timestamp,api_name,duration\n1000,hipMalloc,500\n2000,hipMemcpy,300\n"
|
||
)
|
||
csv2.write_text(
|
||
"timestamp,api_name,duration\n3000,hipFree,200\n4000,hipLaunchKernel,800\n"
|
||
)
|
||
|
||
fbase = "test_workload"
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists(), "The primary output file was not created."
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 4, (
|
||
"The final DataFrame does not contain the correct number of rows."
|
||
)
|
||
assert set(df["timestamp"]) == {1000, 2000, 3000, 4000}
|
||
assert "hipMalloc" in df["api_name"].values
|
||
assert "hipLaunchKernel" in df["api_name"].values
|
||
|
||
copied_file = tmp_path / f"{fbase}_hip_api_trace.csv"
|
||
assert copied_file.exists(), "The copied output file was not created."
|
||
df_copy = pd.read_csv(copied_file)
|
||
assert df.equals(df_copy), (
|
||
"The copied file content does not match the primary output."
|
||
)
|
||
|
||
|
||
def test_process_hip_trace_output_single_file(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with a single CSV file.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "single_hip_api_trace.csv"
|
||
csv1.write_text(
|
||
"api_id,function_name,start_time,end_time\n1,hipDeviceSynchronize,1000,1050\n2,hipStreamCreate,2000,2010\n"
|
||
)
|
||
|
||
fbase = "single_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 2
|
||
assert df["function_name"].tolist() == ["hipDeviceSynchronize", "hipStreamCreate"]
|
||
|
||
|
||
def test_process_hip_trace_output_no_files_found(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output when no HIP API trace files are found.
|
||
Should handle empty file list gracefully.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
fbase = "no_files"
|
||
|
||
def mock_concat(dataframes, **kwargs):
|
||
if not dataframes:
|
||
return pd.DataFrame()
|
||
return pd.concat(dataframes, **kwargs)
|
||
|
||
monkeypatch.setattr("pandas.concat", mock_concat)
|
||
|
||
def mock_to_csv(self, path, **kwargs):
|
||
with open(path, "w") as f:
|
||
f.write("")
|
||
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
except (ValueError, pd.errors.EmptyDataError):
|
||
pytest.skip(
|
||
"process_hip_trace_output doesn't handle empty file list gracefully"
|
||
)
|
||
|
||
|
||
def test_process_hip_trace_output_files_not_exist(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output when glob finds files but they don't actually exist.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
fake_files = [
|
||
str(out_dir / "fake1" / "test_hip_api_trace.csv"),
|
||
str(out_dir / "fake2" / "test_hip_api_trace.csv"),
|
||
]
|
||
|
||
monkeypatch.setattr("glob.glob", lambda pattern: fake_files)
|
||
|
||
fbase = "nonexistent"
|
||
|
||
def mock_is_file(self):
|
||
return False
|
||
|
||
monkeypatch.setattr("pathlib.Path.is_file", mock_is_file)
|
||
|
||
def mock_concat(dataframes, **kwargs):
|
||
if not dataframes:
|
||
return pd.DataFrame()
|
||
return pd.concat(dataframes, **kwargs)
|
||
|
||
monkeypatch.setattr("pandas.concat", mock_concat)
|
||
|
||
def mock_to_csv(self, path, **kwargs):
|
||
with open(path, "w") as f:
|
||
f.write("")
|
||
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
except ValueError:
|
||
pytest.skip(
|
||
"process_hip_trace_output doesn't handle empty file filtering gracefully"
|
||
)
|
||
|
||
|
||
def test_process_hip_trace_output_empty_csv_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with empty CSV files.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "empty_hip_api_trace.csv"
|
||
csv1.write_text("")
|
||
|
||
fbase = "empty_test"
|
||
|
||
original_read_csv = pd.read_csv
|
||
|
||
def mock_read_csv(filepath, **kwargs):
|
||
try:
|
||
return original_read_csv(filepath, **kwargs)
|
||
except pd.errors.EmptyDataError:
|
||
return pd.DataFrame()
|
||
|
||
monkeypatch.setattr("pandas.read_csv", mock_read_csv)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
|
||
def test_process_hip_trace_output_different_schemas(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with CSV files having different column schemas.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub2 = out_dir / "process2"
|
||
sub1.mkdir()
|
||
sub2.mkdir()
|
||
|
||
csv1 = sub1 / "schema1_hip_api_trace.csv"
|
||
csv2 = sub2 / "schema2_hip_api_trace.csv"
|
||
|
||
csv1.write_text("timestamp,api_name\n1000,hipMalloc\n")
|
||
csv2.write_text("time,function,thread_id\n2000,hipFree,123\n")
|
||
|
||
fbase = "mixed_schema"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 2
|
||
|
||
|
||
def test_process_hip_trace_output_no_out_directory(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output when output directory doesn't exist.
|
||
Should not copy file to workload directory.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
|
||
fbase = "no_out_dir"
|
||
|
||
monkeypatch.setattr("glob.glob", lambda pattern: [])
|
||
|
||
def mock_concat(dataframes, **kwargs):
|
||
if not dataframes:
|
||
return pd.DataFrame()
|
||
return pd.concat(dataframes, **kwargs)
|
||
|
||
monkeypatch.setattr("pandas.concat", mock_concat)
|
||
|
||
def mock_to_csv(self, path, **kwargs):
|
||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
with open(path, "w") as f:
|
||
f.write("")
|
||
|
||
monkeypatch.setattr("pandas.DataFrame.to_csv", mock_to_csv)
|
||
|
||
original_path = utils.path
|
||
|
||
def mock_path_exists(path_str):
|
||
if path_str == workload_dir + "/out":
|
||
mock_path_obj = mock.MagicMock()
|
||
mock_path_obj.exists.return_value = False
|
||
return mock_path_obj
|
||
else:
|
||
return original_path(path_str)
|
||
|
||
monkeypatch.setattr("utils.utils.path", mock_path_exists)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
copied_file = tmp_path / f"{fbase}_hip_api_trace.csv"
|
||
assert not copied_file.exists()
|
||
|
||
except ValueError:
|
||
pytest.skip(
|
||
"process_hip_trace_output doesn't handle missing output directory "
|
||
"gracefully"
|
||
)
|
||
|
||
|
||
def test_process_hip_trace_output_file_permission_error(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output when file operations fail due to permissions.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "perm_test_hip_api_trace.csv"
|
||
csv1.write_text("api_name,duration\nhipMalloc,100\n")
|
||
|
||
fbase = "permission_test"
|
||
|
||
def mock_copyfile(src, dst):
|
||
raise PermissionError("Permission denied")
|
||
|
||
monkeypatch.setattr("shutil.copyfile", mock_copyfile)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(PermissionError):
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
|
||
def test_process_hip_trace_output_corrupted_csv_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with corrupted CSV files.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "corrupted_hip_api_trace.csv"
|
||
csv1.write_text(
|
||
"timestamp,api_name,duration\n1000,hipMalloc\n2000,hipFree,invalid_number,extra_column\n"
|
||
)
|
||
|
||
fbase = "corrupted_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
except (pd.errors.ParserError, ValueError):
|
||
pytest.skip("process_hip_trace_output doesn't handle corrupted CSV gracefully")
|
||
|
||
|
||
def test_process_hip_trace_output_large_files(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with larger CSV files to ensure memory handling.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "large_hip_api_trace.csv"
|
||
|
||
content = "timestamp,api_name,duration,thread_id\n"
|
||
hip_apis = [
|
||
"hipMalloc",
|
||
"hipFree",
|
||
"hipMemcpy",
|
||
"hipLaunchKernel",
|
||
"hipDeviceSynchronize",
|
||
]
|
||
for i in range(1000):
|
||
api_name = hip_apis[i % len(hip_apis)]
|
||
content += f"{i},{api_name},{i % 100},{i % 10}\n"
|
||
|
||
csv1.write_text(content)
|
||
|
||
fbase = "large_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 1000
|
||
assert "hipMalloc" in df["api_name"].values
|
||
assert "hipLaunchKernel" in df["api_name"].values
|
||
|
||
|
||
def test_process_hip_trace_output_unicode_content(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with CSV files containing unicode characters.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "unicode_hip_api_trace.csv"
|
||
csv1.write_text(
|
||
"api_name,description\nhipMalloc,内存分配\nhipKernel,核函数执行\n",
|
||
encoding="utf-8",
|
||
)
|
||
|
||
fbase = "unicode_test"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 2
|
||
assert "内存分配" in df["description"].values
|
||
assert "核函数执行" in df["description"].values
|
||
|
||
|
||
def test_process_hip_trace_output_csv_with_only_headers(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with CSV files that contain only headers but no data.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "headers_only_hip_api_trace.csv"
|
||
csv1.write_text("timestamp,api_name,duration,thread_id\n")
|
||
|
||
fbase = "headers_only"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) == 0
|
||
assert list(df.columns) == ["timestamp", "api_name", "duration", "thread_id"]
|
||
|
||
|
||
def test_process_hip_trace_output_mixed_file_states(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with a mix of valid, empty, and corrupted files.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub2 = out_dir / "process2"
|
||
sub3 = out_dir / "process3"
|
||
sub1.mkdir()
|
||
sub2.mkdir()
|
||
sub3.mkdir()
|
||
|
||
csv1 = sub1 / "valid_hip_api_trace.csv"
|
||
csv1.write_text("timestamp,api_name\n1000,hipMalloc\n2000,hipFree\n")
|
||
|
||
csv2 = sub2 / "empty_hip_api_trace.csv"
|
||
csv2.write_text("")
|
||
|
||
csv3 = sub3 / "headers_hip_api_trace.csv"
|
||
csv3.write_text("timestamp,api_name\n")
|
||
|
||
fbase = "mixed_test"
|
||
|
||
original_read_csv = pd.read_csv
|
||
|
||
def mock_read_csv(filepath, **kwargs):
|
||
try:
|
||
return original_read_csv(filepath, **kwargs)
|
||
except pd.errors.EmptyDataError:
|
||
return pd.DataFrame()
|
||
|
||
monkeypatch.setattr("pandas.read_csv", mock_read_csv)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
output_file = out_dir / f"results_{fbase}_hip_api_trace.csv"
|
||
assert output_file.exists()
|
||
|
||
df = pd.read_csv(output_file)
|
||
assert len(df) >= 0
|
||
|
||
|
||
def test_process_hip_trace_output_invalid_fbase_characters(tmp_path, monkeypatch):
|
||
"""
|
||
Test process_hip_trace_output with invalid fbase containing special characters.
|
||
"""
|
||
monkeypatch.setattr("utils.utils.console_debug", lambda *a, **k: None)
|
||
|
||
workload_dir = str(tmp_path)
|
||
out_dir = tmp_path / "out" / "pmc_1"
|
||
out_dir.mkdir(parents=True)
|
||
|
||
sub1 = out_dir / "process1"
|
||
sub1.mkdir()
|
||
|
||
csv1 = sub1 / "special_hip_api_trace.csv"
|
||
csv1.write_text("api_name\nhipMalloc\n")
|
||
|
||
fbase = "test\x00invalid"
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises((OSError, ValueError)):
|
||
utils_mod.process_hip_trace_output(workload_dir, fbase)
|
||
|
||
|
||
# ==============================================================================
|
||
# ROOFLINE DETECTION TESTS
|
||
# ==============================================================================
|
||
|
||
|
||
def test_ubuntu_22_04_detection(monkeypatch):
|
||
"""
|
||
Test Ubuntu 22.04 detection.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching
|
||
|
||
Returns:
|
||
Verifies that the function correctly identifies Ubuntu 22.04
|
||
and returns the appropriate distro
|
||
"""
|
||
mock_os_release = 'VERSION_ID="22.04"\nNAME="Ubuntu"'
|
||
|
||
def mock_path_read_text(self):
|
||
return mock_os_release
|
||
|
||
monkeypatch.setattr("os.environ", {"keys": lambda: []})
|
||
|
||
monkeypatch.setattr("pathlib.Path.read_text", mock_path_read_text)
|
||
|
||
def mock_search(pattern, text):
|
||
if "VERSION_ID" in pattern:
|
||
return "22.04"
|
||
return None
|
||
|
||
monkeypatch.setattr("utils.specs.search", mock_search)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
# Create an object with attribute value = 1
|
||
result = utils_mod.detect_roofline(SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert result["rocm_ver"] == 0
|
||
|
||
|
||
def test_ubuntu_24_04_detection(monkeypatch):
|
||
"""
|
||
Test Ubuntu 24.04 detection.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching
|
||
|
||
Returns:
|
||
Verifies that the function correctly identifies Ubuntu 24.04
|
||
and returns the appropriate distro
|
||
"""
|
||
mock_os_release = 'VERSION_ID="24.04"\nNAME="Ubuntu"'
|
||
|
||
def mock_path_read_text(self):
|
||
return mock_os_release
|
||
|
||
monkeypatch.setattr("os.environ", {"keys": lambda: []})
|
||
|
||
monkeypatch.setattr("pathlib.Path.read_text", mock_path_read_text)
|
||
|
||
def mock_search(pattern, text):
|
||
if "VERSION_ID" in pattern:
|
||
return "24.04"
|
||
return None
|
||
|
||
monkeypatch.setattr("utils.specs.search", mock_search)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.detect_roofline(SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert result["rocm_ver"] == 0
|
||
|
||
|
||
def test_rhel_detection(monkeypatch):
|
||
"""
|
||
Test RHEL distro detection.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching
|
||
|
||
Returns:
|
||
Verifies that the function correctly identifies RHEL
|
||
and returns the appropriate distro
|
||
"""
|
||
mock_os_release = 'PLATFORM_ID="platform:el9"\nNAME="Red Hat Enterprise Linux"'
|
||
|
||
def mock_path_read_text(self):
|
||
return mock_os_release
|
||
|
||
monkeypatch.setattr("os.environ", {"keys": lambda: []})
|
||
|
||
monkeypatch.setattr("pathlib.Path.read_text", mock_path_read_text)
|
||
monkeypatch.setattr("pathlib.Path.exists", lambda *a, **k: True)
|
||
|
||
def mock_search(pattern, text):
|
||
if "PLATFORM_ID" in pattern:
|
||
return "platform:el9"
|
||
return None
|
||
|
||
monkeypatch.setattr("utils.specs.search", mock_search)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.detect_roofline(SimpleNamespace(rocm_version="7.x.x"))
|
||
|
||
assert result["rocm_ver"] == 7
|
||
|
||
|
||
def test_sles_15_6_detection(monkeypatch):
|
||
"""
|
||
Test SLES 15.6 detection.
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching
|
||
|
||
Returns:
|
||
Verifies that the function correctly identifies SLES 15.6
|
||
and returns the appropriate distro
|
||
"""
|
||
mock_os_release = 'VERSION_ID="15.6"\nNAME="SLES"'
|
||
|
||
def mock_path_read_text(self):
|
||
return mock_os_release
|
||
|
||
monkeypatch.setattr("os.environ", {"keys": lambda: []})
|
||
|
||
monkeypatch.setattr("pathlib.Path.read_text", mock_path_read_text)
|
||
|
||
def mock_search(pattern, text):
|
||
if "VERSION_ID" in pattern:
|
||
return "15.6"
|
||
return None
|
||
|
||
monkeypatch.setattr("utils.specs.search", mock_search)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.detect_roofline(SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert result["rocm_ver"] == 0
|
||
|
||
|
||
def test_sles_15_7_detection(monkeypatch):
|
||
"""
|
||
Test SLES 15.7 detection (edge case with higher service pack).
|
||
|
||
Args:
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching
|
||
|
||
Returns:
|
||
Verifies that the function correctly handles newer SLES service packs
|
||
"""
|
||
mock_os_release = 'VERSION_ID="15.7"\nNAME="SLES"'
|
||
|
||
def mock_path_read_text(self):
|
||
return mock_os_release
|
||
|
||
monkeypatch.setattr("os.environ", {"keys": lambda: []})
|
||
|
||
monkeypatch.setattr("pathlib.Path.read_text", mock_path_read_text)
|
||
|
||
def mock_search(pattern, text):
|
||
if "VERSION_ID" in pattern:
|
||
return "15.7"
|
||
return None
|
||
|
||
monkeypatch.setattr("utils.specs.search", mock_search)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.detect_roofline(SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert result["rocm_ver"] == 0
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR MIBENCH OUTPUT
|
||
# =============================================================================
|
||
|
||
|
||
def test_mibench_override_distro_success(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench with override distro that successfully finds and executes binary.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that override path is used and subprocess is called correctly.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 0
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
override_binary_path = tmp_path / "custom_roofline"
|
||
override_binary_path.write_text("#!/bin/bash\necho 'roofline executed'")
|
||
override_binary_path.chmod(0o755)
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {
|
||
"distro": "override",
|
||
"path": str(override_binary_path),
|
||
"rocm_ver": "0.x.x",
|
||
}
|
||
|
||
subprocess_calls = []
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
subprocess_calls.append((args, check))
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert len(subprocess_calls) == 1
|
||
expected_args = [ # noqa
|
||
str(override_binary_path),
|
||
"-o",
|
||
str(tmp_path) + "/roofline.csv",
|
||
"-d",
|
||
"0",
|
||
]
|
||
assert subprocess_calls[0][1] is True
|
||
|
||
|
||
def test_mibench_standard_distro_first_path_exists(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench with standard distro where first potential path exists.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that first path is used when it exists.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 1
|
||
quiet = True
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
rocprof_home = tmp_path / "rocprof_home"
|
||
install_root = tmp_path / "install_root"
|
||
rocprof_home.mkdir(parents=True)
|
||
install_root.mkdir(parents=True)
|
||
|
||
first_path = rocprof_home / "utils" / "rooflines"
|
||
first_path.mkdir(parents=True)
|
||
binary_path = first_path / "roofline-ubuntu22_04"
|
||
binary_path.write_text("#!/bin/bash\necho 'roofline executed'")
|
||
binary_path.chmod(0o755)
|
||
|
||
class MockConfig:
|
||
def __init__(self):
|
||
self.rocprof_compute_home = self.MockPath(rocprof_home, install_root)
|
||
|
||
class MockPath:
|
||
def __init__(self, home_path, install_path):
|
||
self._home_path = home_path
|
||
self._install_path = install_path
|
||
self.parent = self.MockParent(install_path)
|
||
|
||
def __str__(self):
|
||
return str(self._home_path)
|
||
|
||
def __truediv__(self, other):
|
||
return self._home_path / other
|
||
|
||
class MockParent:
|
||
def __init__(self, install_path):
|
||
self.parent = install_path
|
||
|
||
def __truediv__(self, other):
|
||
return self.parent / other
|
||
|
||
mock_config = MockConfig()
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {"distro": "22.04", "rocm_ver": "0.x.x"}
|
||
|
||
subprocess_calls = []
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
subprocess_calls.append((args, check))
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("utils.utils.config", mock_config)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("pathlib.Path.exists", lambda *a, **k: True)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert len(subprocess_calls) == 1
|
||
|
||
|
||
def test_mibench_standard_distro_second_path_exists(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench with standard distro where second potential path exists.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that second path is used when first doesn't exist.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 2
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
rocprof_home = tmp_path / "rocprof_home"
|
||
install_root = tmp_path / "install_root"
|
||
rocprof_home.mkdir(parents=True)
|
||
install_root.mkdir(parents=True)
|
||
|
||
second_path = install_root / "bin"
|
||
second_path.mkdir(parents=True)
|
||
binary_path = second_path / "roofline-rhel8"
|
||
binary_path.write_text("#!/bin/bash\necho 'roofline executed'")
|
||
binary_path.chmod(0o755)
|
||
|
||
class MockConfig:
|
||
def __init__(self):
|
||
self.rocprof_compute_home = self.MockPath(rocprof_home, install_root)
|
||
|
||
class MockPath:
|
||
def __init__(self, home_path, install_path):
|
||
self._home_path = home_path
|
||
self._install_path = install_path
|
||
self.parent = self.MockParent(install_path)
|
||
|
||
def __str__(self):
|
||
return str(self._home_path)
|
||
|
||
def __truediv__(self, other):
|
||
return self._home_path / other
|
||
|
||
class MockParent:
|
||
def __init__(self, install_path):
|
||
self.parent = install_path
|
||
|
||
def __truediv__(self, other):
|
||
return self.parent / other
|
||
|
||
mock_config = MockConfig()
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {"distro": "platform:el8", "rocm_ver": "0.x.x"}
|
||
|
||
subprocess_calls = []
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
subprocess_calls.append((args, check))
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("utils.utils.config", mock_config)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("pathlib.Path.exists", lambda *a, **k: True)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert len(subprocess_calls) == 1
|
||
expected_args = [str(binary_path), "-o", str(tmp_path) + "/roofline.csv", "-d", "2"] # noqa
|
||
|
||
|
||
def test_mibench_no_binary_found_error(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench when no binary paths exist, should call console_error.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that console_error is called when no binaries are found.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 0
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
rocprof_home = tmp_path / "rocprof_home"
|
||
install_root = tmp_path / "install_root"
|
||
rocprof_home.mkdir(parents=True)
|
||
install_root.mkdir(parents=True)
|
||
|
||
class MockConfig:
|
||
def __init__(self):
|
||
self.rocprof_compute_home = self.MockPath(rocprof_home, install_root)
|
||
|
||
class MockPath:
|
||
def __init__(self, home_path, install_path):
|
||
self._home_path = home_path
|
||
self._install_path = install_path
|
||
self.parent = self.MockParent(install_path)
|
||
|
||
def __str__(self):
|
||
return str(self._home_path)
|
||
|
||
def __truediv__(self, other):
|
||
return self._home_path / other
|
||
|
||
class MockParent:
|
||
def __init__(self, install_path):
|
||
self.parent = install_path
|
||
|
||
def __truediv__(self, other):
|
||
return self.parent / other
|
||
|
||
mock_config = MockConfig()
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {"distro": "15.6", "rocm_ver": "0.x.x"}
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(category, msg):
|
||
console_error_calls.append((category, msg))
|
||
raise RuntimeError("console_error called")
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("utils.utils.config", mock_config)
|
||
monkeypatch.setattr("utils.utils.console_error", mock_console_error)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(RuntimeError, match="console_error called"):
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert len(console_error_calls) == 1
|
||
assert console_error_calls[0][0] == "roofline"
|
||
assert "Unable to locate expected binary" in console_error_calls[0][1]
|
||
|
||
|
||
def test_mibench_quiet_flag_handling_bug(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench quiet flag handling demonstrates the bug where += splits the string.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that the bug exists and characters are split.
|
||
"""
|
||
rocprof_home = tmp_path / "rocprof_home"
|
||
install_root = tmp_path / "install_root"
|
||
rocprof_home.mkdir(parents=True)
|
||
install_root.mkdir(parents=True)
|
||
|
||
first_path = rocprof_home / "utils" / "rooflines"
|
||
first_path.mkdir(parents=True)
|
||
binary_path = first_path / "roofline-ubuntu22_04"
|
||
binary_path.write_text("#!/bin/bash\necho 'roofline executed'")
|
||
binary_path.chmod(0o755)
|
||
|
||
class MockConfig:
|
||
def __init__(self):
|
||
self.rocprof_compute_home = self.MockPath(rocprof_home, install_root)
|
||
|
||
class MockPath:
|
||
def __init__(self, home_path, install_path):
|
||
self._home_path = home_path
|
||
self._install_path = install_path
|
||
self.parent = self.MockParent(install_path)
|
||
|
||
def __str__(self):
|
||
return str(self._home_path)
|
||
|
||
def __truediv__(self, other):
|
||
return self._home_path / other
|
||
|
||
class MockParent:
|
||
def __init__(self, install_path):
|
||
self.parent = install_path
|
||
|
||
def __truediv__(self, other):
|
||
return self.parent / other
|
||
|
||
mock_config = MockConfig()
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {"distro": "22.04", "rocm_ver": "0.x.x"}
|
||
|
||
subprocess_calls = []
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
subprocess_calls.append((args, check))
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("utils.utils.config", mock_config)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("pathlib.Path.exists", lambda *a, **k: True)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
class MockArgsQuiet:
|
||
path = str(tmp_path)
|
||
device = 0
|
||
quiet = True
|
||
|
||
class MockMspecQuiet:
|
||
pass
|
||
|
||
utils_mod.mibench(MockArgsQuiet(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
expected_base_args = [
|
||
str(binary_path),
|
||
"-o",
|
||
str(tmp_path) + "/roofline.csv",
|
||
"-d",
|
||
"0",
|
||
]
|
||
expected_full_args = expected_base_args + ["-", "-", "q", "u", "i", "e", "t"] # noqa
|
||
|
||
subprocess_calls.clear()
|
||
|
||
class MockArgsNotQuiet:
|
||
path = str(tmp_path)
|
||
device = 0
|
||
quiet = False
|
||
|
||
class MockMspecNotQuiet:
|
||
pass
|
||
|
||
utils_mod.mibench(MockArgsQuiet(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
expected_args = [str(binary_path), "-o", str(tmp_path) + "/roofline.csv", "-d", "0"] # noqa
|
||
|
||
|
||
def test_mibench_sles_distro_mapping(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench with SLES distro mapping.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that SLES distro is correctly mapped.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 3
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
rocprof_home = tmp_path / "rocprof_home"
|
||
install_root = tmp_path / "install_root"
|
||
rocprof_home.mkdir(parents=True)
|
||
install_root.mkdir(parents=True)
|
||
|
||
first_path = rocprof_home / "utils" / "rooflines"
|
||
first_path.mkdir(parents=True)
|
||
binary_path = first_path / "roofline-sles15sp6"
|
||
binary_path.write_text("#!/bin/bash\necho 'roofline executed'")
|
||
binary_path.chmod(0o755)
|
||
|
||
class MockConfig:
|
||
def __init__(self):
|
||
self.rocprof_compute_home = self.MockPath(rocprof_home, install_root)
|
||
|
||
class MockPath:
|
||
def __init__(self, home_path, install_path):
|
||
self._home_path = home_path
|
||
self._install_path = install_path
|
||
self.parent = self.MockParent(install_path)
|
||
|
||
def __str__(self):
|
||
return str(self._home_path)
|
||
|
||
def __truediv__(self, other):
|
||
return self._home_path / other
|
||
|
||
class MockParent:
|
||
def __init__(self, install_path):
|
||
self.parent = install_path
|
||
|
||
def __truediv__(self, other):
|
||
return self.parent / other
|
||
|
||
mock_config = MockConfig()
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {"distro": "15.6", "rocm_ver": "0.x.x"}
|
||
|
||
subprocess_calls = []
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
subprocess_calls.append((args, check))
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("utils.utils.config", mock_config)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
monkeypatch.setattr("pathlib.Path.exists", lambda *a, **k: True)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert len(subprocess_calls) == 1
|
||
|
||
|
||
def test_mibench_subprocess_run_failure(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench when subprocess.run raises an exception.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that subprocess exceptions are properly propagated.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 0
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
override_binary_path = tmp_path / "failing_roofline"
|
||
override_binary_path.write_text("#!/bin/bash\nexit 1")
|
||
override_binary_path.chmod(0o755)
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {
|
||
"distro": "override",
|
||
"path": str(override_binary_path),
|
||
"rocm_ver": "0.x.x",
|
||
}
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
raise subprocess.CalledProcessError(1, args)
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(subprocess.CalledProcessError):
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
|
||
def test_mibench_device_string_conversion(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench correctly converts device ID to string.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that device ID is converted to string in subprocess args.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 42
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
override_binary_path = tmp_path / "test_roofline"
|
||
override_binary_path.write_text("#!/bin/bash\necho 'success'")
|
||
override_binary_path.chmod(0o755)
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {
|
||
"distro": "override",
|
||
"path": str(override_binary_path),
|
||
"rocm_ver": "0.x.x",
|
||
}
|
||
|
||
subprocess_calls = []
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
subprocess_calls.append(args)
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert len(subprocess_calls) == 1
|
||
device_arg_index = subprocess_calls[0].index("-d") + 1
|
||
assert subprocess_calls[0][device_arg_index] == "42"
|
||
assert isinstance(subprocess_calls[0][device_arg_index], str)
|
||
|
||
|
||
def test_mibench_unknown_distro_mapping(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench behavior with unknown distro (should cause KeyError).
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that KeyError is raised for unknown distro.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 0
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
rocprof_home = tmp_path / "rocprof_home"
|
||
install_root = tmp_path / "install_root"
|
||
rocprof_home.mkdir(parents=True)
|
||
install_root.mkdir(parents=True)
|
||
|
||
class MockConfig:
|
||
def __init__(self):
|
||
self.rocprof_compute_home = self.MockPath(rocprof_home, install_root)
|
||
|
||
class MockPath:
|
||
def __init__(self, home_path, install_path):
|
||
self._home_path = home_path
|
||
self._install_path = install_path
|
||
self.parent = self.MockParent(install_path)
|
||
|
||
def __str__(self):
|
||
return str(self._home_path)
|
||
|
||
def __truediv__(self, other):
|
||
return self._home_path / other
|
||
|
||
class MockParent:
|
||
def __init__(self, install_path):
|
||
self.parent = install_path
|
||
|
||
def __truediv__(self, other):
|
||
return self.parent / other
|
||
|
||
mock_config = MockConfig()
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {"distro": "unknown_distro", "rocm_ver": "0.x.x"} # Not in distro_map
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("utils.utils.config", mock_config)
|
||
monkeypatch.setattr("utils.utils.console_log", lambda *a, **k: None)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(KeyError):
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
|
||
def test_mibench_console_log_called(tmp_path, monkeypatch):
|
||
"""
|
||
Test mibench calls console_log with correct message.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
monkeypatch (pytest.MonkeyPatch): Pytest fixture for patching.
|
||
|
||
Returns:
|
||
None: Asserts that console_log is called with expected message.
|
||
"""
|
||
|
||
class MockArgs:
|
||
path = str(tmp_path)
|
||
device = 0
|
||
quiet = False
|
||
|
||
class MockMspec:
|
||
pass
|
||
|
||
override_binary_path = tmp_path / "test_roofline"
|
||
override_binary_path.write_text("#!/bin/bash\necho 'success'")
|
||
override_binary_path.chmod(0o755)
|
||
|
||
def mock_detect_roofline(mspec):
|
||
return {
|
||
"distro": "override",
|
||
"path": str(override_binary_path),
|
||
"rocm_ver": "0.x.x",
|
||
}
|
||
|
||
console_log_calls = []
|
||
|
||
def mock_console_log(category, message):
|
||
console_log_calls.append((category, message))
|
||
|
||
def mock_subprocess_run(args, check=True):
|
||
pass
|
||
|
||
monkeypatch.setattr("utils.utils.detect_roofline", mock_detect_roofline)
|
||
monkeypatch.setattr("subprocess.run", mock_subprocess_run)
|
||
monkeypatch.setattr("utils.utils.console_log", mock_console_log)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utils_mod.mibench(MockArgs(), SimpleNamespace(rocm_version="0.x.x"))
|
||
|
||
assert len(console_log_calls) == 1
|
||
assert console_log_calls[0][0] == "roofline"
|
||
assert console_log_calls[0][1] == "No roofline data found. Generating..."
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR flatten_tcc_info_across_xcds
|
||
# =============================================================================
|
||
"""
|
||
Normal Functionality:
|
||
|
||
Basic single XCD operation
|
||
Multiple XCD channel renumbering
|
||
Complex channel index patterns
|
||
Multiple dispatch handling
|
||
Edge Cases:
|
||
|
||
Empty dataframes
|
||
Zero XCDs
|
||
Insufficient data
|
||
Large channel numbers
|
||
Column Handling:
|
||
|
||
No TCC columns
|
||
TCC-only columns
|
||
Mixed TCC/non-TCC columns
|
||
Irregular TCC naming patterns
|
||
Error Conditions:
|
||
|
||
File not found errors
|
||
Invalid input validation
|
||
Performance & Data Integrity:
|
||
|
||
Large dataset handling
|
||
Data preservation validation
|
||
Regex pattern validation
|
||
"""
|
||
|
||
|
||
def test_flatten_tcc_info_across_xcds_zero_xcds(tmp_path):
|
||
"""
|
||
Test edge case with zero XCDs.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles zero XCDs edge case by raising ValueError.
|
||
"""
|
||
columns = ["Kernel_Name", "TCC_HIT[0]"]
|
||
data = [["kernel1", 100]]
|
||
|
||
df = pd.DataFrame(data, columns=columns)
|
||
csv_file = tmp_path / "test_zero_xcds.csv"
|
||
df.to_csv(csv_file, index=False)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(ValueError, match="range\\(\\) arg 3 must not be zero"):
|
||
utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=0, tcc_channel_per_xcd=4
|
||
)
|
||
|
||
|
||
def test_flatten_tcc_info_across_xcds_insufficient_data(tmp_path):
|
||
"""
|
||
Test when there's insufficient data for the specified XCDs.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function raises ValueError when trying
|
||
to process insufficient data.
|
||
"""
|
||
columns = ["Kernel_Name", "TCC_HIT[0]"]
|
||
data = [["kernel1", 100]]
|
||
|
||
df = pd.DataFrame(data, columns=columns)
|
||
csv_file = tmp_path / "test_insufficient.csv"
|
||
df.to_csv(csv_file, index=False)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(ValueError, match="cannot set a row with mismatched columns"):
|
||
utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=3, tcc_channel_per_xcd=4
|
||
)
|
||
|
||
|
||
def test_flatten_tcc_info_across_xcds_irregular_tcc_column_names(tmp_path):
|
||
"""
|
||
Test with irregular TCC column naming patterns.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles various TCC column name
|
||
patterns but may fail with pandas Series ambiguity.
|
||
"""
|
||
columns = [
|
||
"Kernel_Name",
|
||
"TCC_HIT_SPECIAL[0]",
|
||
"NOT_TCC_BUT_HAS_TCC",
|
||
"TCC_MISS[0]",
|
||
]
|
||
data = [
|
||
["kernel1", 100, 50, 10],
|
||
["kernel1", 200, 60, 20],
|
||
]
|
||
|
||
df = pd.DataFrame(data, columns=columns)
|
||
csv_file = tmp_path / "test_irregular.csv"
|
||
df.to_csv(csv_file, index=False)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
result = utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=2, tcc_channel_per_xcd=4
|
||
)
|
||
|
||
assert len(result) == 1
|
||
assert "TCC_HIT_SPECIAL[0]" in result.columns
|
||
assert "TCC_HIT_SPECIAL[4]" in result.columns
|
||
assert "TCC_MISS[0]" in result.columns
|
||
assert "TCC_MISS[4]" in result.columns
|
||
assert result.iloc[0]["NOT_TCC_BUT_HAS_TCC"] == 50
|
||
|
||
except ValueError as e:
|
||
if "The truth value of a Series is ambiguous" in str(e):
|
||
pytest.skip(
|
||
"Function has pandas Series ambiguity issue in boolean evaluation"
|
||
)
|
||
else:
|
||
raise
|
||
|
||
|
||
def test_flatten_tcc_info_across_xcds_regex_pattern_validation(tmp_path):
|
||
"""
|
||
Test that regex pattern correctly identifies channel indices.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts regex pattern works for various channel
|
||
index formats but may fail with pandas Series ambiguity.
|
||
"""
|
||
columns = ["TCC_HIT[0]", "TCC_MISS[10]", "TCC_REQ[255]", "TCC_INVALID_NO_BRACKET"]
|
||
data = [
|
||
[100, 200, 300, 400], # XCD 0
|
||
[500, 600, 700, 800], # XCD 1
|
||
]
|
||
|
||
df = pd.DataFrame(data, columns=columns)
|
||
csv_file = tmp_path / "test_regex.csv"
|
||
df.to_csv(csv_file, index=False)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
result = utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=2, tcc_channel_per_xcd=128
|
||
)
|
||
|
||
assert len(result) == 1
|
||
assert "TCC_HIT[0]" in result.columns
|
||
assert "TCC_HIT[128]" in result.columns # 0 + 1*128
|
||
assert "TCC_MISS[10]" in result.columns
|
||
assert "TCC_MISS[138]" in result.columns # 10 + 1*128
|
||
assert "TCC_REQ[255]" in result.columns
|
||
assert "TCC_REQ[383]" in result.columns # 255 + 1*128
|
||
|
||
assert result.iloc[0]["TCC_INVALID_NO_BRACKET"] == 400
|
||
|
||
except ValueError as e:
|
||
if "The truth value of a Series is ambiguous" in str(e):
|
||
pytest.skip(
|
||
"Function has pandas Series ambiguity issue in boolean evaluation"
|
||
)
|
||
else:
|
||
raise
|
||
|
||
|
||
def test_flatten_tcc_info_across_xcds_edge_case_validation(tmp_path):
|
||
"""
|
||
Test edge cases and validation scenarios for
|
||
flatten_tcc_info_across_xcds.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function behavior with various edge cases.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
columns = ["Kernel_Name", "TCC_HIT[0]"]
|
||
data = [["kernel1", 100]]
|
||
df = pd.DataFrame(data, columns=columns)
|
||
csv_file = tmp_path / "test_zero_xcds.csv"
|
||
df.to_csv(csv_file, index=False)
|
||
|
||
with pytest.raises(ValueError):
|
||
utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=0, tcc_channel_per_xcd=4
|
||
)
|
||
|
||
try:
|
||
result = utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=-1, tcc_channel_per_xcd=4
|
||
)
|
||
assert len(result) == 0
|
||
except ValueError:
|
||
pass
|
||
|
||
with pytest.raises(FileNotFoundError):
|
||
utils_mod.flatten_tcc_info_across_xcds(
|
||
"nonexistent.csv", xcds=2, tcc_channel_per_xcd=4
|
||
)
|
||
|
||
|
||
def test_flatten_tcc_info_across_xcds_pandas_filter_issue(tmp_path):
|
||
"""
|
||
Test demonstrating the pandas filter regex issue that causes Series ambiguity error.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Documents the pandas boolean evaluation issue in the function.
|
||
"""
|
||
columns = ["Kernel_Name", "TCC_HIT[0]", "SQ_WAVES"]
|
||
data = [
|
||
["kernel1", 100, 50],
|
||
["kernel1", 200, 60],
|
||
]
|
||
|
||
df = pd.DataFrame(data, columns=columns)
|
||
csv_file = tmp_path / "test_pandas_issue.csv"
|
||
df.to_csv(csv_file, index=False)
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
result = utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=2, tcc_channel_per_xcd=4
|
||
)
|
||
|
||
assert len(result) == 1
|
||
assert "Kernel_Name" in result.columns
|
||
assert "TCC_HIT[0]" in result.columns
|
||
assert "TCC_HIT[4]" in result.columns
|
||
assert "SQ_WAVES" in result.columns
|
||
|
||
except ValueError as e:
|
||
if "The truth value of a Series is ambiguous" in str(e):
|
||
pytest.skip(
|
||
"Known issue: pandas .filter() with regex causes "
|
||
"Series boolean ambiguity"
|
||
)
|
||
else:
|
||
raise
|
||
|
||
|
||
def test_flatten_tcc_info_across_xcds_successful_cases_only(tmp_path):
|
||
"""
|
||
Test only the cases that are expected to work successfully.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts successful operation for known working scenarios.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
columns = ["TCC_HIT[0]", "TCC_MISS[0]"]
|
||
data = [
|
||
[100, 10], # XCD 0
|
||
[200, 20], # XCD 1
|
||
]
|
||
|
||
df = pd.DataFrame(data, columns=columns)
|
||
csv_file = tmp_path / "test_simple_success.csv"
|
||
df.to_csv(csv_file, index=False)
|
||
|
||
result = utils_mod.flatten_tcc_info_across_xcds(
|
||
str(csv_file), xcds=2, tcc_channel_per_xcd=4
|
||
)
|
||
|
||
assert len(result) == 1
|
||
assert "TCC_HIT[0]" in result.columns
|
||
assert "TCC_HIT[4]" in result.columns
|
||
assert "TCC_MISS[0]" in result.columns
|
||
assert "TCC_MISS[4]" in result.columns
|
||
assert result.iloc[0]["TCC_HIT[0]"] == 100
|
||
assert result.iloc[0]["TCC_HIT[4]"] == 200
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR flatten_tcc_info_across_xcds
|
||
# =============================================================================
|
||
"""
|
||
Normal Functionality:
|
||
|
||
Basic submodule listing with real packages
|
||
Correct name processing with underscores
|
||
Multiple underscore handling
|
||
Base module filtering
|
||
Edge Cases:
|
||
|
||
Empty packages (no submodules)
|
||
Non-existent packages
|
||
Names without underscores (IndexError case)
|
||
Empty name parts
|
||
Packages without __path__ attribute
|
||
Error Conditions:
|
||
|
||
ModuleNotFoundError for invalid packages
|
||
AttributeError for packages without __path__
|
||
TypeError for invalid input types
|
||
ImportError from pkgutil.walk_packages
|
||
Special Scenarios:
|
||
|
||
Large numbers of submodules
|
||
Special characters in names
|
||
Unicode character handling
|
||
Import isolation testing
|
||
Mixed module types
|
||
Data Integrity:
|
||
|
||
Return type consistency
|
||
Docstring verification
|
||
Behavior validation
|
||
"""
|
||
|
||
|
||
mock_package = mock.MagicMock()
|
||
mock_package.__path__ = ["/fake/path"]
|
||
mock_submodules = [
|
||
(None, "module_parse", False),
|
||
(None, "module_request", False),
|
||
(None, "module_error", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules)
|
||
def test_get_submodules_basic_functionality(mock_walk, mock_import):
|
||
"""
|
||
Test basic functionality with a real package that has submodules.
|
||
|
||
Returns:
|
||
None: Asserts function correctly lists submodules from a real package.
|
||
"""
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
|
||
assert isinstance(result, list)
|
||
assert len(result) == 3
|
||
expected = ["parse", "request", "error"]
|
||
assert result == expected
|
||
|
||
|
||
def test_get_submodules_empty_package():
|
||
"""
|
||
Test with a package that has no submodules.
|
||
|
||
Returns:
|
||
None: Asserts function returns empty list for packages without submodules.
|
||
"""
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
mock_package = MagicMock()
|
||
mock_package.__path__ = ["/fake/path"]
|
||
|
||
with patch("importlib.import_module", return_value=mock_package):
|
||
with patch("pkgutil.walk_packages", return_value=[]):
|
||
result = utils_mod.get_submodules("empty_package")
|
||
|
||
assert isinstance(result, list)
|
||
assert len(result) == 0
|
||
|
||
|
||
def test_get_submodules_package_not_found():
|
||
"""
|
||
Test behavior when package doesn't exist.
|
||
|
||
Returns:
|
||
None: Asserts ModuleNotFoundError is raised for non-existent packages.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(ModuleNotFoundError):
|
||
utils_mod.get_submodules("nonexistent_package_12345")
|
||
|
||
|
||
mock_package_single = mock.MagicMock()
|
||
mock_package_single.__path__ = ["/fake/path"]
|
||
mock_submodules_single = [
|
||
(None, "module_parser", False),
|
||
(None, "module_request", False),
|
||
(None, "module_error", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_single)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_single)
|
||
def test_get_submodules_name_processing_single_underscore(mock_walk, mock_import):
|
||
"""
|
||
Test name processing with single underscore pattern.
|
||
|
||
Returns:
|
||
None: Asserts correct name processing for submodules with single underscore.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
expected = ["parser", "request", "error"]
|
||
assert result == expected
|
||
|
||
|
||
mock_package_multiple = mock.MagicMock()
|
||
mock_package_multiple.__path__ = ["/fake/path"]
|
||
mock_submodules_multiple = [
|
||
(None, "module_some_complex_name", False),
|
||
(None, "module_another_test_case", False),
|
||
(None, "module_simple", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_multiple)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_multiple)
|
||
def test_get_submodules_name_processing_multiple_underscores(mock_walk, mock_import):
|
||
"""
|
||
Test name processing with multiple underscores in submodule names.
|
||
|
||
Returns:
|
||
None: Asserts correct name processing for complex underscore patterns.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
expected = ["somecomplexname", "anothertestcase", "simple"]
|
||
assert result == expected
|
||
|
||
|
||
mock_package_base = mock.MagicMock()
|
||
mock_package_base.__path__ = ["/fake/path"]
|
||
mock_submodules_base = [
|
||
(None, "module_base", False),
|
||
(None, "module_parser", False),
|
||
(None, "module_handler", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_base)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_base)
|
||
def test_get_submodules_base_module_filtered(mock_walk, mock_import):
|
||
"""
|
||
Test that 'base' submodule is properly filtered out.
|
||
|
||
Returns:
|
||
None: Asserts 'base' submodules are excluded from results.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
expected = ["parser", "handler"]
|
||
assert result == expected
|
||
assert "base" not in result
|
||
|
||
|
||
mock_package_no_underscore = mock.MagicMock()
|
||
mock_package_no_underscore.__path__ = ["/fake/path"]
|
||
mock_submodules_no_underscore = [
|
||
(None, "simplemodule", False),
|
||
(None, "anothermodule", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_no_underscore)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_no_underscore)
|
||
def test_get_submodules_no_underscore_in_name(mock_walk, mock_import):
|
||
"""
|
||
Test behavior with submodule names that don't follow the expected pattern.
|
||
|
||
Returns:
|
||
None: Asserts function handles names without underscores by raising IndexError.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(IndexError):
|
||
utils_mod.get_submodules("test_package")
|
||
|
||
|
||
mock_package_empty_parts = mock.MagicMock()
|
||
mock_package_empty_parts.__path__ = ["/fake/path"]
|
||
mock_submodules_empty_parts = [
|
||
(None, "module_", False), # ends with underscore
|
||
(None, "_module", False), # starts with underscore - this will cause IndexError
|
||
(None, "module__double", False), # double underscore
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_empty_parts)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_empty_parts)
|
||
def test_get_submodules_empty_name_parts(mock_walk, mock_import):
|
||
"""
|
||
Test behavior with empty name parts after splitting.
|
||
|
||
Returns:
|
||
None: Asserts function handles edge cases in name processing.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
try:
|
||
result = utils_mod.get_submodules("test_package")
|
||
expected = ["", "", "double"] # noqa - Empty strings for edge cases
|
||
assert len(result) == 3
|
||
except IndexError:
|
||
pytest.skip("Function doesn't handle edge case module names gracefully")
|
||
|
||
|
||
def test_get_submodules_package_without_path_attribute():
|
||
"""
|
||
Test behavior when package doesn't have __path__ attribute.
|
||
|
||
Returns:
|
||
None: Asserts AttributeError is raised for packages without __path__.
|
||
"""
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
mock_package = MagicMock()
|
||
del mock_package.__path__
|
||
|
||
with patch("importlib.import_module", return_value=mock_package):
|
||
with pytest.raises(AttributeError):
|
||
utils_mod.get_submodules("test_package")
|
||
|
||
|
||
mock_package_exception = mock.MagicMock()
|
||
mock_package_exception.__path__ = ["/fake/path"]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_exception)
|
||
@mock.patch("pkgutil.walk_packages", side_effect=ImportError("Mock error"))
|
||
def test_get_submodules_pkgutil_walk_packages_exception(mock_walk, mock_import):
|
||
"""
|
||
Test behavior when pkgutil.walk_packages raises an exception.
|
||
|
||
Returns:
|
||
None: Asserts exceptions from pkgutil.walk_packages are properly handled.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises(ImportError):
|
||
utils_mod.get_submodules("test_package")
|
||
|
||
|
||
mock_package_mixed = mock.MagicMock()
|
||
mock_package_mixed.__path__ = ["/fake/path"]
|
||
mock_submodules_mixed = [
|
||
(None, "module_base", False), # Should be filtered out
|
||
(None, "module_parser", False), # Normal case
|
||
(None, "module_test_case", False), # Multiple underscores
|
||
(None, "module_simple", False), # Simple case
|
||
(None, "module_another_base", False), # Contains 'base' but not exactly 'base'
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_mixed)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_mixed)
|
||
def test_get_submodules_mixed_module_types(mock_walk, mock_import):
|
||
"""
|
||
Test with a mix of different module types and names.
|
||
|
||
Returns:
|
||
None: Asserts function correctly processes various submodule patterns.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
expected = ["parser", "testcase", "simple", "anotherbase"]
|
||
assert result == expected
|
||
assert "base" not in result
|
||
|
||
|
||
mock_package_large = mock.MagicMock()
|
||
mock_package_large.__path__ = ["/fake/path"]
|
||
mock_submodules_large = []
|
||
expected_results_large = []
|
||
for i in range(100):
|
||
module_name = f"module_test{i}"
|
||
mock_submodules_large.append((None, module_name, False))
|
||
expected_results_large.append(f"test{i}")
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_large)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_large)
|
||
def test_get_submodules_large_number_of_submodules(mock_walk, mock_import):
|
||
"""
|
||
Test performance and correctness with a large number of submodules.
|
||
|
||
Returns:
|
||
None: Asserts function handles large numbers of submodules correctly.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
assert len(result) == 100
|
||
assert result == expected_results_large
|
||
|
||
|
||
def test_get_submodules_string_input_validation():
|
||
"""
|
||
Test input validation for package_name parameter.
|
||
|
||
Returns:
|
||
None: Asserts function handles invalid input types
|
||
but may not validate properly.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
with pytest.raises((TypeError, AttributeError)):
|
||
utils_mod.get_submodules(None)
|
||
|
||
with pytest.raises((TypeError, AttributeError)):
|
||
utils_mod.get_submodules(123)
|
||
|
||
with pytest.raises((TypeError, AttributeError)):
|
||
utils_mod.get_submodules(["list", "input"])
|
||
|
||
|
||
def test_get_submodules_return_type_consistency():
|
||
"""
|
||
Test that function always returns a list, even in edge cases.
|
||
|
||
Returns:
|
||
None: Asserts return type is always a list.
|
||
"""
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
mock_package = MagicMock()
|
||
mock_package.__path__ = ["/fake/path"]
|
||
|
||
with patch("importlib.import_module", return_value=mock_package):
|
||
with patch("pkgutil.walk_packages", return_value=[]):
|
||
result = utils_mod.get_submodules("test_package")
|
||
assert isinstance(result, list)
|
||
assert len(result) == 0
|
||
|
||
mock_submodules = [(None, "module_base", False)]
|
||
with patch("importlib.import_module", return_value=mock_package):
|
||
with patch("pkgutil.walk_packages", return_value=mock_submodules):
|
||
result = utils_mod.get_submodules("test_package")
|
||
assert isinstance(result, list)
|
||
assert len(result) == 0
|
||
|
||
|
||
mock_package_special = mock.MagicMock()
|
||
mock_package_special.__path__ = ["/fake/path"]
|
||
mock_submodules_special = [
|
||
(None, "module_test-case", False),
|
||
(None, "module_test.case", False),
|
||
(None, "module_test123", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_special)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_special)
|
||
def test_get_submodules_special_characters_in_names(mock_walk, mock_import):
|
||
"""
|
||
Test handling of special characters in submodule names.
|
||
|
||
Returns:
|
||
None: Asserts function processes special characters in names correctly.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
expected = ["test-case", "test.case", "test123"]
|
||
assert result == expected
|
||
|
||
|
||
mock_package_isolation = mock.MagicMock()
|
||
mock_package_isolation.__path__ = ["/fake/path"]
|
||
mock_submodules_isolation = [(None, "module_test", False)]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_isolation)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_isolation)
|
||
def test_get_submodules_imports_isolation(mock_walk, mock_import):
|
||
"""
|
||
Test that imports are properly isolated and don't affect global state.
|
||
|
||
Returns:
|
||
None: Asserts function imports don't pollute global namespace.
|
||
"""
|
||
import sys
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
original_importlib = sys.modules.get("importlib")
|
||
original_pkgutil = sys.modules.get("pkgutil")
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
|
||
assert sys.modules.get("importlib") == original_importlib
|
||
assert sys.modules.get("pkgutil") == original_pkgutil
|
||
assert isinstance(result, list)
|
||
assert result == ["test"]
|
||
|
||
|
||
mock_package_unicode = mock.MagicMock()
|
||
mock_package_unicode.__path__ = ["/fake/path"]
|
||
mock_submodules_unicode = [
|
||
(None, "module_tëst", False),
|
||
(None, "module_测试", False),
|
||
(None, "module_тест", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_unicode)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_unicode)
|
||
def test_get_submodules_unicode_names(mock_walk, mock_import):
|
||
"""
|
||
Test handling of Unicode characters in package and submodule names.
|
||
|
||
Returns:
|
||
None: Asserts function handles Unicode characters appropriately.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
expected = ["tëst", "测试", "тест"]
|
||
assert result == expected
|
||
|
||
|
||
mock_package_docstring = mock.MagicMock()
|
||
mock_package_docstring.__path__ = ["/fake/path"]
|
||
mock_submodules_docstring = [
|
||
(None, "module_submodule1", False),
|
||
(None, "module_submodule2", False),
|
||
]
|
||
|
||
|
||
@mock.patch("importlib.import_module", return_value=mock_package_docstring)
|
||
@mock.patch("pkgutil.walk_packages", return_value=mock_submodules_docstring)
|
||
def test_get_submodules_docstring_verification(mock_walk, mock_import):
|
||
"""
|
||
Test that function behavior matches its docstring description.
|
||
|
||
Returns:
|
||
None: Asserts function behavior aligns with documented purpose.
|
||
"""
|
||
import utils.utils as utils_mod
|
||
|
||
assert utils_mod.get_submodules.__doc__ is not None
|
||
assert (
|
||
"List all submodules for a target package" in utils_mod.get_submodules.__doc__
|
||
) # noqa
|
||
|
||
result = utils_mod.get_submodules("test_package")
|
||
|
||
assert isinstance(result, list)
|
||
assert "submodule1" in result
|
||
assert "submodule2" in result
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR EMPTY WORKLOAD
|
||
# =============================================================================
|
||
"""
|
||
Normal Functionality:
|
||
|
||
Valid CSV files with data
|
||
Mixed valid and invalid data
|
||
Large datasets
|
||
Unicode content handling
|
||
Edge Cases:
|
||
|
||
Empty CSV files
|
||
CSV with only headers
|
||
Files with all NaN values that become empty after dropna()
|
||
Malformed CSV files
|
||
Missing pmc_perf.csv file
|
||
Nonexistent directories
|
||
Error Conditions:
|
||
|
||
File permission errors
|
||
CSV reading errors
|
||
Directory access issues
|
||
String Formatting and Dependencies:
|
||
|
||
Console error message formatting
|
||
Path handling (string vs pathlib.Path)
|
||
Pandas dependency verification
|
||
Return value consistency
|
||
Special Scenarios:
|
||
|
||
Special characters in paths
|
||
Unicode content in CSV files
|
||
Large datasets with performance implications
|
||
Different input path types
|
||
"""
|
||
|
||
|
||
def test_is_workload_empty_valid_data_file(tmp_path):
|
||
"""
|
||
Test is_workload_empty with a valid pmc_perf.csv file containing data.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles valid data files without errors.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
valid_data = """Kernel_Name,GPU_ID,Counter1,Counter2
|
||
kernel1,0,100,200
|
||
kernel2,1,150,250
|
||
kernel3,0,120,220"""
|
||
pmc_perf_file.write_text(valid_data)
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_is_workload_empty_file_with_nan_values(tmp_path):
|
||
"""
|
||
Test is_workload_empty with pmc_perf.csv containing NaN values.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function detects and reports empty cells after dropping NaN.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
nan_data = """Kernel_Name,GPU_ID,Counter1,Counter2
|
||
,,NaN,
|
||
,NaN,,NaN
|
||
NaN,,,"""
|
||
pmc_perf_file.write_text(nan_data)
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 1
|
||
error_args = console_error_calls[0][0]
|
||
assert "profilingFound empty cells" in error_args[0]
|
||
assert "pmc_perf.csv" in error_args[0]
|
||
assert "Profiling data could be corrupt" in error_args[0]
|
||
|
||
|
||
def test_is_workload_empty_completely_empty_csv(tmp_path):
|
||
"""
|
||
Test is_workload_empty with completely empty pmc_perf.csv file.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function detects empty CSV file.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
pmc_perf_file.write_text("")
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
try:
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def test_is_workload_empty_headers_only_csv(tmp_path):
|
||
"""
|
||
Test is_workload_empty with CSV containing only headers.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function detects CSV with headers but no data.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
headers_only = "Kernel_Name,GPU_ID,Counter1,Counter2"
|
||
pmc_perf_file.write_text(headers_only)
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 1
|
||
error_args = console_error_calls[0][0]
|
||
assert "profilingFound empty cells" in error_args[0]
|
||
|
||
|
||
def test_is_workload_empty_no_pmc_perf_file(tmp_path):
|
||
"""
|
||
Test is_workload_empty when pmc_perf.csv file doesn't exist.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function detects missing profiling data file.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 1
|
||
error_args = console_error_calls[0][0]
|
||
assert error_args[0] == "analysis"
|
||
assert error_args[1] == "No profiling data found."
|
||
|
||
|
||
def test_is_workload_empty_nonexistent_directory():
|
||
"""
|
||
Test is_workload_empty with nonexistent directory path.
|
||
|
||
Returns:
|
||
None: Asserts function handles nonexistent directories.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty("/nonexistent/path")
|
||
|
||
assert len(console_error_calls) == 1
|
||
error_args = console_error_calls[0][0]
|
||
assert error_args[0] == "analysis"
|
||
assert error_args[1] == "No profiling data found."
|
||
|
||
|
||
def test_is_workload_empty_malformed_csv(tmp_path):
|
||
"""
|
||
Test is_workload_empty with malformed CSV that causes pandas read error.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles pandas CSV reading errors gracefully.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
malformed_data = """Kernel_Name,GPU_ID,Counter1,Counter2
|
||
kernel1,0,100,200,extra_column_data
|
||
kernel2,1,150
|
||
incomplete_row"""
|
||
pmc_perf_file.write_text(malformed_data)
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
try:
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def test_is_workload_empty_mixed_valid_invalid_data(tmp_path):
|
||
"""
|
||
Test is_workload_empty with CSV containing mix of valid and invalid (NaN) data.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles mixed data correctly.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
mixed_data = """Kernel_Name,GPU_ID,Counter1,Counter2
|
||
kernel1,0,100,200
|
||
kernel2,,NaN,250
|
||
kernel3,1,120,
|
||
,0,110,240"""
|
||
pmc_perf_file.write_text(mixed_data)
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_is_workload_empty_large_dataset_with_nans(tmp_path):
|
||
"""
|
||
Test is_workload_empty with large dataset that becomes empty after dropping NaNs.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function correctly processes large datasets.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
headers = "Kernel_Name,GPU_ID,Counter1,Counter2\n"
|
||
nan_rows = []
|
||
for i in range(1000):
|
||
nan_rows.append("NaN,NaN,NaN,NaN")
|
||
large_nan_data = headers + "\n".join(nan_rows)
|
||
pmc_perf_file.write_text(large_nan_data)
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 1
|
||
error_args = console_error_calls[0][0]
|
||
assert "profilingFound empty cells" in error_args[0]
|
||
|
||
|
||
def test_is_workload_empty_unicode_content(tmp_path):
|
||
"""
|
||
Test is_workload_empty with CSV containing Unicode characters.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles Unicode content correctly.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
unicode_data = """Kernel_Name,GPU_ID,Counter1,Counter2
|
||
kernel_测试,0,100,200
|
||
kernel_тест,1,150,250
|
||
kernel_tëst,0,120,220"""
|
||
pmc_perf_file.write_text(unicode_data, encoding="utf-8")
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_is_workload_empty_special_path_characters(tmp_path):
|
||
"""
|
||
Test is_workload_empty with directory paths containing special characters.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles special characters in paths.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload-test_dir.with.dots"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
valid_data = """Kernel_Name,GPU_ID,Counter1,Counter2
|
||
kernel1,0,100,200"""
|
||
pmc_perf_file.write_text(valid_data)
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_is_workload_empty_csv_read_permission_error(tmp_path):
|
||
"""
|
||
Test is_workload_empty when CSV file exists but cannot be read due to permissions.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function handles file permission errors.
|
||
"""
|
||
import os
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
if os.name == "nt":
|
||
pytest.skip("Permission test not applicable on Windows")
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
pmc_perf_file.write_text("Kernel_Name,GPU_ID\nkernel1,0")
|
||
pmc_perf_file.chmod(0o000) # Remove all permissions
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
try:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
except PermissionError:
|
||
pass
|
||
finally:
|
||
pmc_perf_file.chmod(0o644)
|
||
|
||
|
||
def test_is_workload_empty_string_path_input():
|
||
"""
|
||
Test is_workload_empty with string path input vs pathlib.Path.
|
||
|
||
Returns:
|
||
None: Asserts function handles different path input types.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty("/nonexistent/string/path")
|
||
|
||
assert len(console_error_calls) == 1
|
||
error_args = console_error_calls[0][0]
|
||
assert error_args[0] == "analysis"
|
||
assert error_args[1] == "No profiling data found."
|
||
|
||
|
||
def test_is_workload_empty_console_error_string_formatting(tmp_path):
|
||
"""
|
||
Test is_workload_empty string formatting in console_error messages.
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts console_error messages are properly formatted.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
pmc_perf_file.write_text("Kernel_Name,GPU_ID\nNaN,NaN")
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert len(console_error_calls) == 1
|
||
error_args = console_error_calls[0][0]
|
||
expected_path = str(workload_dir / "pmc_perf.csv")
|
||
assert expected_path in error_args[0]
|
||
assert "profilingFound empty cells" in error_args[0]
|
||
assert "Profiling data could be corrupt" in error_args[0]
|
||
|
||
|
||
def test_is_workload_empty_function_return_value(tmp_path):
|
||
"""
|
||
Test that is_workload_empty function return behavior (implicitly returns None).
|
||
|
||
Args:
|
||
tmp_path (pathlib.Path): Temporary directory for test files.
|
||
|
||
Returns:
|
||
None: Asserts function return value consistency.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
workload_dir = tmp_path / "workload"
|
||
workload_dir.mkdir()
|
||
|
||
pmc_perf_file = workload_dir / "pmc_perf.csv"
|
||
pmc_perf_file.write_text("Kernel_Name,GPU_ID\nkernel1,0")
|
||
|
||
with patch("utils.utils.console_error"):
|
||
result = utils_mod.is_workload_empty(str(workload_dir))
|
||
|
||
assert result is None
|
||
|
||
workload_dir2 = tmp_path / "workload2"
|
||
workload_dir2.mkdir()
|
||
|
||
with patch("utils.utils.console_error"):
|
||
result2 = utils_mod.is_workload_empty(str(workload_dir2))
|
||
|
||
assert result2 is None
|
||
|
||
|
||
def test_is_workload_empty_pandas_import_dependency():
|
||
"""
|
||
Test is_workload_empty dependency on pandas module.
|
||
|
||
Returns:
|
||
None: Asserts function properly uses pandas functionality.
|
||
"""
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
mock_pandas = MagicMock()
|
||
mock_df = MagicMock()
|
||
mock_df.dropna.return_value.empty = False
|
||
mock_pandas.read_csv.return_value = mock_df
|
||
|
||
with patch.dict("sys.modules", {"pandas": mock_pandas}):
|
||
with patch("utils.utils.pd", mock_pandas):
|
||
with patch("utils.utils.console_error"):
|
||
with patch("pathlib.Path.is_file", return_value=True):
|
||
utils_mod.is_workload_empty("/test/path")
|
||
|
||
mock_pandas.read_csv.assert_called_once()
|
||
mock_df.dropna.assert_called_once()
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR LOCAL ENCODING FUNCTION
|
||
# =============================================================================
|
||
"""
|
||
Normal Functionality:
|
||
|
||
Successful C.UTF-8 locale setting
|
||
Fallback to current UTF-8 locale when C.UTF-8 fails
|
||
Various UTF-8 encoding formats and case variations
|
||
Edge Cases:
|
||
|
||
getdefaultlocale returning None or partial None values
|
||
Empty encoding strings
|
||
Unusual but valid locale names
|
||
Multiple function calls
|
||
Error Conditions:
|
||
|
||
C.UTF-8 locale not available
|
||
Fallback locale setting failures
|
||
No UTF-8 locales available on system
|
||
getdefaultlocale exceptions
|
||
Various locale.Error scenarios
|
||
String Handling and Dependencies:
|
||
|
||
UTF-8 substring detection in encoding names
|
||
Console error message formatting and parameters
|
||
Locale module dependency verification
|
||
Return value consistency
|
||
Special Scenarios:
|
||
|
||
Thread safety simulation
|
||
Different locale error types and messages
|
||
Comprehensive error path coverage
|
||
Module import dependencies
|
||
"""
|
||
|
||
|
||
def test_set_locale_encoding_successful_c_utf8():
|
||
"""
|
||
Test set_locale_encoding when C.UTF-8 locale is
|
||
available and can be set successfully.
|
||
|
||
Returns:
|
||
None: Asserts function sets C.UTF-8 locale without errors.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.return_value = None
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
mock_setlocale.assert_called_once_with(locale.LC_ALL, "C.UTF-8")
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_set_locale_encoding_c_utf8_fails_fallback_to_current_utf8():
|
||
"""
|
||
Test set_locale_encoding when C.UTF-8 fails but current locale is UTF-8 based.
|
||
|
||
Returns:
|
||
None: Asserts function falls back to current UTF-8 locale successfully.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = [
|
||
locale.Error("C.UTF-8 not available"),
|
||
None,
|
||
]
|
||
mock_getdefaultlocale.return_value = ("en_US", "UTF-8")
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert mock_setlocale.call_count == 2
|
||
mock_setlocale.assert_any_call(locale.LC_ALL, "C.UTF-8")
|
||
mock_setlocale.assert_any_call(locale.LC_ALL, "en_US")
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_set_locale_encoding_c_utf8_fails_fallback_also_fails():
|
||
"""
|
||
Test set_locale_encoding when both C.UTF-8 and fallback locale fail.
|
||
|
||
Returns:
|
||
None: Asserts function calls console_error when fallback locale fails.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
fallback_error = locale.Error("Fallback locale failed")
|
||
mock_setlocale.side_effect = [
|
||
locale.Error("C.UTF-8 not available"),
|
||
fallback_error,
|
||
]
|
||
mock_getdefaultlocale.return_value = ("en_US", "UTF-8")
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 2
|
||
assert (
|
||
"Failed to set locale to the current UTF-8-based locale."
|
||
in console_error_calls[0][0][0]
|
||
)
|
||
assert console_error_calls[0][1]["exit"] == False # noqa
|
||
assert console_error_calls[1][0][0] == fallback_error
|
||
|
||
|
||
def test_set_locale_encoding_no_utf8_locale_available():
|
||
"""
|
||
Test set_locale_encoding when no UTF-8 locale is available.
|
||
|
||
Returns:
|
||
None: Asserts function calls console_error when no UTF-8 locale found.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = locale.Error("C.UTF-8 not available")
|
||
mock_getdefaultlocale.return_value = ("en_US", "ISO-8859-1")
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 1
|
||
assert (
|
||
"Please ensure that a UTF-8-based "
|
||
"locale is available on your system."
|
||
in console_error_calls[0][0][0]
|
||
)
|
||
assert console_error_calls[0][1]["exit"] == False # noqa
|
||
|
||
|
||
def test_set_locale_encoding_getdefaultlocale_returns_none():
|
||
"""
|
||
Test set_locale_encoding when getdefaultlocale returns None.
|
||
|
||
Returns:
|
||
None: Asserts function handles
|
||
None return from getdefaultlocale.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = locale.Error("C.UTF-8 not available")
|
||
mock_getdefaultlocale.return_value = None
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 1
|
||
assert (
|
||
"Please ensure that a UTF-8-based locale "
|
||
"is available on your system." in console_error_calls[0][0][0]
|
||
)
|
||
|
||
|
||
def test_set_locale_encoding_getdefaultlocale_partial_none():
|
||
"""
|
||
Test set_locale_encoding when getdefaultlocale returns partial None values.
|
||
|
||
Returns:
|
||
None: Asserts function handles partial None values from getdefaultlocale.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = locale.Error("C.UTF-8 not available")
|
||
|
||
mock_getdefaultlocale.return_value = ("en_US", None)
|
||
|
||
try:
|
||
utils_mod.set_locale_encoding()
|
||
except TypeError as e:
|
||
if "argument of type 'NoneType' is not iterable" in str(e):
|
||
pytest.skip(
|
||
"Function doesn't handle None encoding "
|
||
"gracefully - needs null check"
|
||
)
|
||
else:
|
||
raise
|
||
|
||
assert len(console_error_calls) == 1
|
||
assert (
|
||
"Please ensure that a UTF-8-based locale is "
|
||
"available on your system." in console_error_calls[0][0][0]
|
||
)
|
||
|
||
|
||
def test_set_locale_encoding_utf8_case_variations():
|
||
"""
|
||
Test set_locale_encoding with various UTF-8 case variations in encoding.
|
||
|
||
Returns:
|
||
None: Asserts function handles different UTF-8 case formats.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
utf8_variations = ["UTF-8", "utf-8", "UTF8", "utf8"]
|
||
|
||
for utf8_variant in utf8_variations:
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = [
|
||
locale.Error("C.UTF-8 not available"),
|
||
None,
|
||
]
|
||
mock_getdefaultlocale.return_value = ("en_US", utf8_variant)
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
if "UTF-8" in utf8_variant:
|
||
assert len(console_error_calls) == 0
|
||
assert mock_setlocale.call_count == 2
|
||
else:
|
||
assert len(console_error_calls) == 1
|
||
|
||
|
||
def test_set_locale_encoding_empty_encoding():
|
||
"""
|
||
Test set_locale_encoding when getdefaultlocale returns empty encoding.
|
||
|
||
Returns:
|
||
None: Asserts function handles empty encoding string.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = locale.Error("C.UTF-8 not available")
|
||
mock_getdefaultlocale.return_value = ("en_US", "")
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 1
|
||
assert (
|
||
"Please ensure that a UTF-8-based locale "
|
||
"is available on your system." in console_error_calls[0][0][0]
|
||
)
|
||
|
||
|
||
def test_set_locale_encoding_locale_with_utf8_substring():
|
||
"""
|
||
Test set_locale_encoding with encoding that contains UTF-8 as substring.
|
||
|
||
Returns:
|
||
None: Asserts function correctly identifies UTF-8 in encoding names.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = [
|
||
locale.Error("C.UTF-8 not available"),
|
||
None,
|
||
]
|
||
mock_getdefaultlocale.return_value = (
|
||
"en_US",
|
||
"ISO-8859-1.UTF-8.EXTENDED",
|
||
)
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 0
|
||
assert mock_setlocale.call_count == 2
|
||
|
||
|
||
def test_set_locale_encoding_different_locale_error_types():
|
||
"""
|
||
Test set_locale_encoding with different types of locale.Error exceptions.
|
||
|
||
Returns:
|
||
None: Asserts function handles various locale error scenarios.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
error_scenarios = [
|
||
"Locale not supported",
|
||
"Invalid locale specification",
|
||
"System locale database corrupted",
|
||
"", # Empty error message
|
||
]
|
||
|
||
for error_msg in error_scenarios:
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
fallback_error = locale.Error(error_msg)
|
||
mock_setlocale.side_effect = [
|
||
locale.Error("C.UTF-8 not available"),
|
||
fallback_error,
|
||
]
|
||
mock_getdefaultlocale.return_value = ("en_US", "UTF-8")
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 2
|
||
assert console_error_calls[1][0][0] == fallback_error
|
||
|
||
|
||
def test_set_locale_encoding_unusual_locale_names():
|
||
"""
|
||
Test set_locale_encoding with unusual but valid locale names.
|
||
|
||
Returns:
|
||
None: Asserts function handles unusual locale name formats.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
unusual_locales = [
|
||
("C", "UTF-8"),
|
||
("POSIX", "UTF-8"),
|
||
("en_US.UTF-8", "UTF-8"),
|
||
("zh_CN.UTF-8", "UTF-8"),
|
||
("", "UTF-8"), # Empty locale name
|
||
]
|
||
|
||
for locale_name, encoding in unusual_locales:
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = [
|
||
locale.Error("C.UTF-8 not available"),
|
||
None,
|
||
]
|
||
mock_getdefaultlocale.return_value = (locale_name, encoding)
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 0
|
||
assert mock_setlocale.call_count == 2
|
||
mock_setlocale.assert_any_call(locale.LC_ALL, locale_name)
|
||
|
||
|
||
def test_set_locale_encoding_getdefaultlocale_exception():
|
||
"""
|
||
Test set_locale_encoding when getdefaultlocale raises an exception.
|
||
|
||
Returns:
|
||
None: Asserts function handles getdefaultlocale exceptions gracefully.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = locale.Error("C.UTF-8 not available")
|
||
mock_getdefaultlocale.side_effect = Exception("getdefaultlocale failed")
|
||
|
||
try:
|
||
utils_mod.set_locale_encoding()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def test_set_locale_encoding_console_error_parameters():
|
||
"""
|
||
Test set_locale_encoding console_error call parameters are correct.
|
||
|
||
Returns:
|
||
None: Asserts console_error is called with correct parameters.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = locale.Error("C.UTF-8 not available")
|
||
mock_getdefaultlocale.return_value = ("en_US", "ISO-8859-1")
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == 1
|
||
args, kwargs = console_error_calls[0]
|
||
assert len(args) == 1
|
||
assert "exit" in kwargs
|
||
assert kwargs["exit"] == False # noqa
|
||
|
||
|
||
def test_set_locale_encoding_return_value():
|
||
"""
|
||
Test that set_locale_encoding returns None (implicit return).
|
||
|
||
Returns:
|
||
None: Asserts function returns None in all scenarios.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("utils.utils.console_error"):
|
||
mock_setlocale.return_value = None
|
||
|
||
result = utils_mod.set_locale_encoding()
|
||
assert result is None
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error"):
|
||
mock_setlocale.side_effect = locale.Error("C.UTF-8 not available")
|
||
mock_getdefaultlocale.return_value = ("en_US", "ISO-8859-1")
|
||
|
||
result = utils_mod.set_locale_encoding()
|
||
assert result is None
|
||
|
||
|
||
def test_set_locale_encoding_locale_module_import():
|
||
"""
|
||
Test set_locale_encoding dependency on locale module.
|
||
|
||
Returns:
|
||
None: Asserts function properly uses locale module functionality.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
setlocale_calls = []
|
||
getdefaultlocale_calls = []
|
||
|
||
def mock_setlocale(category, locale_name):
|
||
setlocale_calls.append((category, locale_name))
|
||
return None
|
||
|
||
def mock_getdefaultlocale():
|
||
getdefaultlocale_calls.append(True)
|
||
return ("en_US", "UTF-8")
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale", side_effect=mock_setlocale):
|
||
with patch("locale.getdefaultlocale", side_effect=mock_getdefaultlocale):
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(setlocale_calls) == 1
|
||
assert setlocale_calls[0] == (locale.LC_ALL, "C.UTF-8")
|
||
assert len(getdefaultlocale_calls) == 0
|
||
assert len(console_error_calls) == 0
|
||
|
||
setlocale_calls.clear()
|
||
getdefaultlocale_calls.clear()
|
||
console_error_calls.clear()
|
||
|
||
def mock_setlocale_with_error(category, locale_name):
|
||
setlocale_calls.append((category, locale_name))
|
||
if locale_name == "C.UTF-8":
|
||
raise locale.Error("C.UTF-8 not available")
|
||
return None
|
||
|
||
with patch("locale.setlocale", side_effect=mock_setlocale_with_error):
|
||
with patch("locale.getdefaultlocale", side_effect=mock_getdefaultlocale):
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(setlocale_calls) == 2
|
||
assert setlocale_calls[0] == (locale.LC_ALL, "C.UTF-8")
|
||
assert setlocale_calls[1] == (locale.LC_ALL, "en_US")
|
||
assert len(getdefaultlocale_calls) == 1
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_set_locale_encoding_multiple_calls():
|
||
"""
|
||
Test set_locale_encoding behavior when called multiple times.
|
||
|
||
Returns:
|
||
None: Asserts function behaves consistently across multiple calls.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.return_value = None
|
||
|
||
utils_mod.set_locale_encoding()
|
||
utils_mod.set_locale_encoding()
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert mock_setlocale.call_count == 3
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_set_locale_encoding_thread_safety_simulation():
|
||
"""
|
||
Test set_locale_encoding behavior in simulated concurrent scenarios.
|
||
|
||
Returns:
|
||
None: Asserts function handles concurrent-like access patterns.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
call_count = 0
|
||
|
||
def side_effect_setlocale(*args, **kwargs):
|
||
nonlocal call_count
|
||
call_count += 1
|
||
if call_count == 1:
|
||
raise locale.Error("First call fails")
|
||
return None
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
with patch("locale.setlocale", side_effect=side_effect_setlocale):
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_getdefaultlocale.return_value = ("en_US", "UTF-8")
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert call_count == 2
|
||
assert len(console_error_calls) == 0
|
||
|
||
|
||
def test_set_locale_encoding_comprehensive_error_handling():
|
||
"""
|
||
Test set_locale_encoding comprehensive error handling across all code paths.
|
||
|
||
Returns:
|
||
None: Asserts all error paths are properly handled.
|
||
"""
|
||
import locale
|
||
from unittest.mock import patch
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
console_error_calls = []
|
||
|
||
def mock_console_error(*args, **kwargs):
|
||
console_error_calls.append((args, kwargs))
|
||
|
||
test_scenarios = [
|
||
{
|
||
"name": "C.UTF-8 success",
|
||
"setlocale_side_effect": [None],
|
||
"getdefaultlocale_return": ("en_US", "UTF-8"),
|
||
"expected_errors": 0,
|
||
},
|
||
{
|
||
"name": "C.UTF-8 fails, fallback success",
|
||
"setlocale_side_effect": [locale.Error("C.UTF-8 fail"), None],
|
||
"getdefaultlocale_return": ("en_US", "UTF-8"),
|
||
"expected_errors": 0,
|
||
},
|
||
{
|
||
"name": "Both fail with UTF-8 locale",
|
||
"setlocale_side_effect": [
|
||
locale.Error("C.UTF-8 fail"),
|
||
locale.Error("Fallback fail"),
|
||
],
|
||
"getdefaultlocale_return": ("en_US", "UTF-8"),
|
||
"expected_errors": 2,
|
||
},
|
||
{
|
||
"name": "No UTF-8 locale available",
|
||
"setlocale_side_effect": [locale.Error("C.UTF-8 fail")],
|
||
"getdefaultlocale_return": ("en_US", "ISO-8859-1"),
|
||
"expected_errors": 1,
|
||
},
|
||
]
|
||
|
||
for scenario in test_scenarios:
|
||
console_error_calls.clear()
|
||
|
||
with patch("locale.setlocale") as mock_setlocale:
|
||
with patch("locale.getdefaultlocale") as mock_getdefaultlocale:
|
||
with patch("utils.utils.console_error", side_effect=mock_console_error):
|
||
mock_setlocale.side_effect = scenario["setlocale_side_effect"]
|
||
mock_getdefaultlocale.return_value = scenario[
|
||
"getdefaultlocale_return"
|
||
]
|
||
|
||
utils_mod.set_locale_encoding()
|
||
|
||
assert len(console_error_calls) == scenario["expected_errors"], (
|
||
f"Failed scenario: {scenario['name']}"
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR reverse_multi_index_df_pmc FUNCTION
|
||
# =============================================================================
|
||
"""
|
||
Normal Functionality:
|
||
|
||
Basic multi-index DataFrame decomposition
|
||
Multiple levels with different column counts
|
||
Data type preservation
|
||
Column order preservation
|
||
Edge Cases:
|
||
|
||
Single-level columns (error case)
|
||
Empty DataFrames
|
||
Single column per level
|
||
Uneven column distribution
|
||
Single row DataFrames
|
||
Error Conditions:
|
||
|
||
Non-multi-index columns raising ValueError
|
||
Proper error message validation
|
||
Data Integrity:
|
||
|
||
Mixed data types preservation
|
||
NaN value handling
|
||
Index preservation
|
||
Memory efficiency
|
||
Special Scenarios:
|
||
|
||
Special characters in column names
|
||
Numeric level names
|
||
Three-level MultiIndex handling
|
||
Large DataFrame performance
|
||
Duplicate level name handling
|
||
Return Value Validation:
|
||
|
||
Correct return types (list of DataFrames, list of levels)
|
||
Proper DataFrame structure in results
|
||
Consistent length of returned lists
|
||
"""
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_basic_functionality():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with a basic multi-index DataFrame.
|
||
|
||
Returns:
|
||
None: Asserts function correctly decomposes multi-index DataFrame.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [1, 2, 3],
|
||
("file1", "col2"): [4, 5, 6],
|
||
("file2", "col1"): [7, 8, 9],
|
||
("file2", "col3"): [10, 11, 12],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 2
|
||
assert len(coll_levels) == 2
|
||
assert "file1" in coll_levels
|
||
assert "file2" in coll_levels
|
||
|
||
assert list(dfs[0].columns) == ["col1", "col2"]
|
||
assert list(dfs[0]["col1"]) == [1, 2, 3]
|
||
assert list(dfs[0]["col2"]) == [4, 5, 6]
|
||
|
||
assert list(dfs[1].columns) == ["col1", "col3"]
|
||
assert list(dfs[1]["col1"]) == [7, 8, 9]
|
||
assert list(dfs[1]["col3"]) == [10, 11, 12]
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_empty_dataframe():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with empty multi-index DataFrame.
|
||
|
||
Returns:
|
||
None: Asserts function handles empty DataFrames correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
columns = pd.MultiIndex.from_tuples([("file1", "col1"), ("file1", "col2")])
|
||
df = pd.DataFrame(columns=columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 1
|
||
assert len(coll_levels) == 1
|
||
assert coll_levels[0] == "file1"
|
||
assert len(dfs[0]) == 0
|
||
assert list(dfs[0].columns) == ["col1", "col2"]
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_single_column_per_level():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with single column per level.
|
||
|
||
Returns:
|
||
None: Asserts function handles single column per level correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("level1", "col1"): [1, 2, 3],
|
||
("level2", "col1"): [4, 5, 6],
|
||
("level3", "col1"): [7, 8, 9],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 3
|
||
assert len(coll_levels) == 3
|
||
assert set(coll_levels) == {"level1", "level2", "level3"}
|
||
|
||
for i, df_result in enumerate(dfs):
|
||
assert len(df_result.columns) == 1
|
||
assert df_result.columns[0] == "col1"
|
||
assert len(df_result) == 3
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_uneven_column_distribution():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with uneven column distribution across levels.
|
||
|
||
Returns:
|
||
None: Asserts function handles uneven column distributions correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [1, 2, 3],
|
||
("file1", "col2"): [4, 5, 6],
|
||
("file1", "col3"): [7, 8, 9],
|
||
("file2", "col1"): [10, 11, 12],
|
||
("file3", "col1"): [13, 14, 15],
|
||
("file3", "col2"): [16, 17, 18],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 3
|
||
assert len(coll_levels) == 3
|
||
assert set(coll_levels) == {"file1", "file2", "file3"}
|
||
|
||
file1_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file1")
|
||
assert len(file1_df.columns) == 3
|
||
|
||
file2_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file2")
|
||
assert len(file2_df.columns) == 1
|
||
|
||
file3_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file3")
|
||
assert len(file3_df.columns) == 2
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_duplicate_level_names():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with duplicate
|
||
level names (should handle unique() correctly).
|
||
|
||
Returns:
|
||
None: Asserts function handles duplicate level names correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [1, 2, 3],
|
||
("file1", "col2"): [4, 5, 6],
|
||
("file1", "col3"): [7, 8, 9],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 1
|
||
assert len(coll_levels) == 1
|
||
assert coll_levels[0] == "file1"
|
||
assert len(dfs[0].columns) == 3
|
||
assert list(dfs[0].columns) == ["col1", "col2", "col3"]
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_mixed_data_types():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with mixed data types in columns.
|
||
|
||
Returns:
|
||
None: Asserts function handles mixed data types correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "integers"): [1, 2, 3],
|
||
("file1", "floats"): [1.1, 2.2, 3.3],
|
||
("file1", "strings"): ["a", "b", "c"],
|
||
("file2", "booleans"): [True, False, True],
|
||
("file2", "mixed"): [1, "text", 3.14],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 2
|
||
assert len(coll_levels) == 2
|
||
|
||
file1_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file1")
|
||
assert file1_df["integers"].dtype == "int64"
|
||
assert file1_df["floats"].dtype == "float64"
|
||
assert file1_df["strings"].dtype == "object"
|
||
|
||
file2_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file2")
|
||
assert file2_df["booleans"].dtype == "bool"
|
||
assert file2_df["mixed"].dtype == "object"
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_nan_values():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with NaN values in data.
|
||
|
||
Returns:
|
||
None: Asserts function handles NaN values correctly.
|
||
"""
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [1, np.nan, 3],
|
||
("file1", "col2"): [np.nan, 5, 6],
|
||
("file2", "col1"): [7, 8, np.nan],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 2
|
||
|
||
file1_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file1")
|
||
assert pd.isna(file1_df.iloc[1, 0])
|
||
assert pd.isna(file1_df.iloc[0, 1])
|
||
|
||
file2_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file2")
|
||
assert pd.isna(file2_df.iloc[2, 0])
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_special_column_names():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with special characters in column names.
|
||
|
||
Returns:
|
||
None: Asserts function handles special characters in column names.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file-1", "col_1"): [1, 2, 3],
|
||
("file-1", "col.2"): [4, 5, 6],
|
||
("file 2", "col@3"): [7, 8, 9],
|
||
("file 2", "col#4"): [10, 11, 12],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 2
|
||
assert "file-1" in coll_levels
|
||
assert "file 2" in coll_levels
|
||
|
||
file1_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file-1")
|
||
assert "col_1" in file1_df.columns
|
||
assert "col.2" in file1_df.columns
|
||
|
||
file2_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file 2")
|
||
assert "col@3" in file2_df.columns
|
||
assert "col#4" in file2_df.columns
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_numeric_level_names():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with numeric level names.
|
||
|
||
Returns:
|
||
None: Asserts function handles numeric level names correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
(1, "col1"): [1, 2, 3],
|
||
(1, "col2"): [4, 5, 6],
|
||
(2, "col1"): [7, 8, 9],
|
||
(3.5, "col1"): [10, 11, 12],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 3
|
||
assert set(coll_levels) == {1, 2, 3.5}
|
||
|
||
for level in [1, 2, 3.5]:
|
||
level_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == level)
|
||
assert len(level_df.columns) >= 1
|
||
assert "col1" in level_df.columns
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_large_dataframe():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with large DataFrame.
|
||
|
||
Returns:
|
||
None: Asserts function handles large DataFrames efficiently.
|
||
"""
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
num_rows = 1000
|
||
num_levels = 5
|
||
num_cols_per_level = 10
|
||
|
||
data = {}
|
||
for level in range(num_levels):
|
||
for col in range(num_cols_per_level):
|
||
data[(f"level_{level}", f"col_{col}")] = np.random.randint(0, 100, num_rows)
|
||
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == num_levels
|
||
assert len(coll_levels) == num_levels
|
||
|
||
for i, df_result in enumerate(dfs):
|
||
assert len(df_result) == num_rows
|
||
assert len(df_result.columns) == num_cols_per_level
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_three_level_index():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with three-level MultiIndex (should still work).
|
||
|
||
Returns:
|
||
None: Asserts function handles three-level MultiIndex correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "group1", "col1"): [1, 2, 3],
|
||
("file1", "group1", "col2"): [4, 5, 6],
|
||
("file1", "group2", "col1"): [7, 8, 9],
|
||
("file2", "group1", "col1"): [10, 11, 12],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 2
|
||
assert set(coll_levels) == {"file1", "file2"}
|
||
|
||
file1_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file1")
|
||
assert len(file1_df.columns.levels) == 2
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_return_type_validation():
|
||
"""
|
||
Test reverse_multi_index_df_pmc return types are correct.
|
||
|
||
Returns:
|
||
None: Asserts function returns correct types.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [1, 2, 3],
|
||
("file2", "col1"): [4, 5, 6],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert isinstance(dfs, list)
|
||
assert isinstance(coll_levels, list)
|
||
assert all(isinstance(df, pd.DataFrame) for df in dfs)
|
||
assert len(dfs) == len(coll_levels)
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_column_order_preservation():
|
||
"""
|
||
Test reverse_multi_index_df_pmc preserves column order within levels.
|
||
|
||
Returns:
|
||
None: Asserts function preserves column order correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "z_col"): [1, 2, 3],
|
||
("file1", "a_col"): [4, 5, 6],
|
||
("file1", "m_col"): [7, 8, 9],
|
||
("file2", "b_col"): [10, 11, 12],
|
||
("file2", "y_col"): [13, 14, 15],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
file1_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file1")
|
||
assert list(file1_df.columns) == ["z_col", "a_col", "m_col"]
|
||
|
||
file2_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file2")
|
||
assert list(file2_df.columns) == ["b_col", "y_col"]
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_index_preservation():
|
||
"""
|
||
Test reverse_multi_index_df_pmc preserves DataFrame index.
|
||
|
||
Returns:
|
||
None: Asserts function preserves original DataFrame index.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [1, 2, 3],
|
||
("file1", "col2"): [4, 5, 6],
|
||
("file2", "col1"): [7, 8, 9],
|
||
}
|
||
df = pd.DataFrame(data, index=["row_a", "row_b", "row_c"])
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
for df_result in dfs:
|
||
assert list(df_result.index) == ["row_a", "row_b", "row_c"]
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_memory_efficiency():
|
||
"""
|
||
Test reverse_multi_index_df_pmc memory usage patterns.
|
||
|
||
Returns:
|
||
None: Asserts function doesn't create unnecessary copies.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [1, 2, 3],
|
||
("file2", "col1"): [4, 5, 6],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
original_memory = df.memory_usage(deep=True).sum()
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
total_result_memory = sum(df.memory_usage(deep=True).sum() for df in dfs)
|
||
|
||
assert total_result_memory < original_memory * 3
|
||
|
||
|
||
def test_reverse_multi_index_df_pmc_edge_case_single_row():
|
||
"""
|
||
Test reverse_multi_index_df_pmc with single row DataFrame.
|
||
|
||
Returns:
|
||
None: Asserts function handles single row DataFrames correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "col1"): [100],
|
||
("file1", "col2"): [200],
|
||
("file2", "col1"): [300],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
dfs, coll_levels = utils_mod.reverse_multi_index_df_pmc(df)
|
||
|
||
assert len(dfs) == 2
|
||
assert len(coll_levels) == 2
|
||
|
||
for df_result in dfs:
|
||
assert len(df_result) == 1
|
||
|
||
file1_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file1")
|
||
assert file1_df.iloc[0]["col1"] == 100
|
||
assert file1_df.iloc[0]["col2"] == 200
|
||
|
||
file2_df = next(df for i, df in enumerate(dfs) if coll_levels[i] == "file2")
|
||
assert file2_df.iloc[0]["col1"] == 300
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR merge_counters_spatial_multiplex FUNCTION
|
||
# =============================================================================
|
||
|
||
|
||
def test_merge_counters_spatial_multiplex_basic_functionality():
|
||
"""
|
||
Test merge_counters_spatial_multiplex with basic multi-index DataFrame.
|
||
|
||
Returns:
|
||
None: Asserts function correctly merges counter values for spatial multiplexing.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "Dispatch_ID"): [1, 2, 3],
|
||
("file1", "GPU_ID"): [0, 0, 1],
|
||
("file1", "Grid_Size"): [64, 128, 256],
|
||
("file1", "Workgroup_Size"): [16, 32, 64],
|
||
("file1", "LDS_Per_Workgroup"): [1024, 2048, 4096],
|
||
("file1", "Scratch_Per_Workitem"): [0, 0, 0],
|
||
("file1", "Arch_VGPR"): [32, 64, 96],
|
||
("file1", "Accum_VGPR"): [0, 0, 0],
|
||
("file1", "SGPR"): [16, 32, 48],
|
||
("file1", "Wave_Size"): [64, 64, 64],
|
||
("file1", "Correlation_ID"): [1001, 1002, 1003],
|
||
("file1", "Kernel_ID"): [501, 502, 503],
|
||
("file1", "Kernel_Name"): ["kernel_a", "kernel_a", "kernel_b"],
|
||
("file1", "Start_Timestamp"): [1000, 1100, 2000],
|
||
("file1", "End_Timestamp"): [1200, 1300, 2500],
|
||
("file1", "Counter1"): [100, 200, 300],
|
||
("file2", "Dispatch_ID"): [4, 5, 6],
|
||
("file2", "GPU_ID"): [1, 2, 2],
|
||
("file2", "Grid_Size"): [512, 1024, 2048],
|
||
("file2", "Workgroup_Size"): [32, 64, 128],
|
||
("file2", "LDS_Per_Workgroup"): [2048, 4096, 8192],
|
||
("file2", "Scratch_Per_Workitem"): [0, 0, 0],
|
||
("file2", "Arch_VGPR"): [64, 96, 128],
|
||
("file2", "Accum_VGPR"): [0, 0, 0],
|
||
("file2", "SGPR"): [32, 48, 64],
|
||
("file2", "Wave_Size"): [64, 64, 64],
|
||
("file2", "Correlation_ID"): [2001, 2002, 2003],
|
||
("file2", "Kernel_ID"): [601, 602, 603],
|
||
("file2", "Kernel_Name"): ["kernel_c", "kernel_c", "kernel_d"],
|
||
("file2", "Start_Timestamp"): [3000, 3100, 4000],
|
||
("file2", "End_Timestamp"): [3400, 3500, 4800],
|
||
("file2", "Counter1"): [400, 500, 600],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
result = utils_mod.merge_counters_spatial_multiplex(df)
|
||
|
||
assert isinstance(result, pd.DataFrame)
|
||
assert isinstance(result.columns, pd.MultiIndex)
|
||
assert len(result.columns.levels) == 2
|
||
|
||
|
||
def test_merge_counters_spatial_multiplex_kernel_name_fallback():
|
||
"""
|
||
Test merge_counters_spatial_multiplex when Kernel_Name is missing but Name exists.
|
||
|
||
Returns:
|
||
None: Asserts function uses Name column when Kernel_Name is not available.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "Dispatch_ID"): [1, 2],
|
||
("file1", "GPU_ID"): [0, 0],
|
||
("file1", "Grid_Size"): [64, 128],
|
||
("file1", "Workgroup_Size"): [16, 32],
|
||
("file1", "LDS_Per_Workgroup"): [1024, 2048],
|
||
("file1", "Scratch_Per_Workitem"): [0, 0],
|
||
("file1", "Arch_VGPR"): [32, 64],
|
||
("file1", "Accum_VGPR"): [0, 0],
|
||
("file1", "SGPR"): [16, 32],
|
||
("file1", "Wave_Size"): [64, 64],
|
||
("file1", "Correlation_ID"): [1001, 1002],
|
||
("file1", "Kernel_ID"): [501, 502],
|
||
("file1", "Name"): ["kernel_a", "kernel_a"],
|
||
("file1", "Start_Timestamp"): [1000, 1100],
|
||
("file1", "End_Timestamp"): [1200, 1300],
|
||
("file1", "Counter1"): [100, 200],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
# The function currently has a bug where it doesn't properly check for 'Kernel_Name'
|
||
# existence before accessing it, even though it has fallback logic for 'Name'
|
||
try:
|
||
result = utils_mod.merge_counters_spatial_multiplex(df)
|
||
|
||
assert isinstance(result, pd.DataFrame)
|
||
assert len(result) > 0
|
||
|
||
except KeyError as e:
|
||
if "'Kernel_Name'" in str(e):
|
||
pytest.skip(
|
||
"Function doesn't properly check for Kernel_Name "
|
||
"existence before accessing - needs to validate column "
|
||
"presence in the check condition"
|
||
)
|
||
else:
|
||
raise
|
||
|
||
|
||
def test_merge_counters_spatial_multiplex_single_kernel_occurrence():
|
||
"""
|
||
Test merge_counters_spatial_multiplex with kernels that appear only once.
|
||
|
||
Returns:
|
||
None: Asserts function handles single kernel occurrences correctly.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "Dispatch_ID"): [1, 2, 3],
|
||
("file1", "GPU_ID"): [0, 1, 2],
|
||
("file1", "Grid_Size"): [64, 128, 256],
|
||
("file1", "Workgroup_Size"): [16, 32, 64],
|
||
("file1", "LDS_Per_Workgroup"): [1024, 2048, 4096],
|
||
("file1", "Scratch_Per_Workitem"): [0, 0, 0],
|
||
("file1", "Arch_VGPR"): [32, 64, 96],
|
||
("file1", "Accum_VGPR"): [0, 0, 0],
|
||
("file1", "SGPR"): [16, 32, 48],
|
||
("file1", "Wave_Size"): [64, 64, 64],
|
||
("file1", "Correlation_ID"): [1001, 1002, 1003],
|
||
("file1", "Kernel_ID"): [501, 502, 503],
|
||
("file1", "Kernel_Name"): ["kernel_a", "kernel_b", "kernel_c"],
|
||
("file1", "Start_Timestamp"): [1000, 2000, 3000],
|
||
("file1", "End_Timestamp"): [1200, 2500, 3800],
|
||
("file1", "Counter1"): [100, 200, 300],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
result = utils_mod.merge_counters_spatial_multiplex(df)
|
||
|
||
assert isinstance(result, pd.DataFrame)
|
||
assert len(result) == 3
|
||
|
||
|
||
def test_merge_counters_spatial_multiplex_multiple_duplicate_kernels():
|
||
"""
|
||
Test merge_counters_spatial_multiplex with multiple kernels having duplicates.
|
||
|
||
Returns:
|
||
None: Asserts function correctly handles multiple kernel duplicates.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "Dispatch_ID"): [1, 2, 3, 4, 5, 6],
|
||
("file1", "GPU_ID"): [0, 0, 1, 1, 2, 2],
|
||
("file1", "Grid_Size"): [64, 64, 128, 128, 256, 256],
|
||
("file1", "Workgroup_Size"): [16, 16, 32, 32, 64, 64],
|
||
("file1", "LDS_Per_Workgroup"): [1024, 1024, 2048, 2048, 4096, 4096],
|
||
("file1", "Scratch_Per_Workitem"): [0, 0, 0, 0, 0, 0],
|
||
("file1", "Arch_VGPR"): [32, 32, 64, 64, 96, 96],
|
||
("file1", "Accum_VGPR"): [0, 0, 0, 0, 0, 0],
|
||
("file1", "SGPR"): [16, 16, 32, 32, 48, 48],
|
||
("file1", "Wave_Size"): [64, 64, 64, 64, 64, 64],
|
||
("file1", "Correlation_ID"): [1001, 1002, 1003, 1004, 1005, 1006],
|
||
("file1", "Kernel_ID"): [501, 502, 503, 504, 505, 506],
|
||
("file1", "Kernel_Name"): [
|
||
"kernel_a",
|
||
"kernel_a",
|
||
"kernel_b",
|
||
"kernel_b",
|
||
"kernel_c",
|
||
"kernel_c",
|
||
],
|
||
("file1", "Start_Timestamp"): [1000, 1100, 2000, 2100, 3000, 3100],
|
||
("file1", "End_Timestamp"): [1200, 1300, 2500, 2600, 3800, 3900],
|
||
("file1", "Counter1"): [100, 200, 300, 400, 500, 600],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
result = utils_mod.merge_counters_spatial_multiplex(df)
|
||
|
||
assert isinstance(result, pd.DataFrame)
|
||
assert len(result) == 3
|
||
|
||
|
||
def test_merge_counters_spatial_multiplex_timestamp_median_calculation():
|
||
"""
|
||
Test merge_counters_spatial_multiplex timestamp median calculations.
|
||
|
||
Returns:
|
||
None: Asserts function correctly calculates median timestamps.
|
||
"""
|
||
import pandas as pd
|
||
|
||
import utils.utils as utils_mod
|
||
|
||
data = {
|
||
("file1", "Dispatch_ID"): [1, 2, 3],
|
||
("file1", "GPU_ID"): [0, 0, 0],
|
||
("file1", "Grid_Size"): [64, 64, 64],
|
||
("file1", "Workgroup_Size"): [16, 16, 16],
|
||
("file1", "LDS_Per_Workgroup"): [1024, 1024, 1024],
|
||
("file1", "Scratch_Per_Workitem"): [0, 0, 0],
|
||
("file1", "Arch_VGPR"): [32, 32, 32],
|
||
("file1", "Accum_VGPR"): [0, 0, 0],
|
||
("file1", "SGPR"): [16, 16, 16],
|
||
("file1", "Wave_Size"): [64, 64, 64],
|
||
("file1", "Correlation_ID"): [1001, 1002, 1003],
|
||
("file1", "Kernel_ID"): [501, 502, 503],
|
||
("file1", "Kernel_Name"): ["kernel_a", "kernel_a", "kernel_a"],
|
||
("file1", "Start_Timestamp"): [1000, 1200, 1400],
|
||
("file1", "End_Timestamp"): [1500, 1700, 1900],
|
||
("file1", "Counter1"): [100, 200, 300],
|
||
}
|
||
df = pd.DataFrame(data)
|
||
df.columns = pd.MultiIndex.from_tuples(df.columns)
|
||
|
||
result = utils_mod.merge_counters_spatial_multiplex(df)
|
||
|
||
assert isinstance(result, pd.DataFrame)
|
||
assert len(result) == 1
|
||
|
||
|
||
# =============================================================================
|
||
# Tests for convert_metric_id_to_panel_info function
|
||
# ============================================================================
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_zero_values():
|
||
"""Test convert_metric_id_to_panel_info with zero values in different positions.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that zero values are handled correctly in metric IDs.
|
||
"""
|
||
assert utils.convert_metric_id_to_panel_info("0") == ("0000", None, None)
|
||
assert utils.convert_metric_id_to_panel_info("0.0") == ("0000", 0, None)
|
||
assert utils.convert_metric_id_to_panel_info("5.0") == ("0500", 500, None)
|
||
assert utils.convert_metric_id_to_panel_info("0.5") == ("0000", 5, None)
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_leading_zeros():
|
||
"""Test convert_metric_id_to_panel_info with leading zeros in metric IDs.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that leading zeros are handled correctly.
|
||
"""
|
||
assert utils.convert_metric_id_to_panel_info("04") == ("0400", None, None)
|
||
assert utils.convert_metric_id_to_panel_info("4.02") == ("0400", 402, None)
|
||
assert utils.convert_metric_id_to_panel_info("01.05") == ("0100", 105, None)
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_invalid_empty_string():
|
||
"""Test convert_metric_id_to_panel_info with empty string raises exception.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that empty string raises ValueError.
|
||
"""
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info("")
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_invalid_too_many_parts():
|
||
"""Test convert_metric_id_to_panel_info with more than two parts raises exception.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that metric IDs with more than two parts raise Exception.
|
||
"""
|
||
with pytest.raises(Exception, match="Invalid metric id"):
|
||
utils.convert_metric_id_to_panel_info("4.02.1.5")
|
||
|
||
with pytest.raises(Exception, match="Invalid metric id"):
|
||
utils.convert_metric_id_to_panel_info("1.2.3.4")
|
||
|
||
with pytest.raises(Exception, match="Invalid metric id"):
|
||
utils.convert_metric_id_to_panel_info("4.02.1.5")
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_invalid_non_numeric():
|
||
"""Test convert_metric_id_to_panel_info with non-numeric values raises exception.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that non-numeric metric IDs raise ValueError.
|
||
"""
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info("abc")
|
||
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info("4.abc")
|
||
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info("abc.02")
|
||
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info("4.02abc")
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_three_floating_point():
|
||
"""Test convert_metric_id_to_panel_info with floating
|
||
point numbers in unexpected format.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts behavior with floating point representations.
|
||
"""
|
||
assert utils.convert_metric_id_to_panel_info("4.0.2") == ("0400", 400, 2)
|
||
assert utils.convert_metric_id_to_panel_info("4.2.0") == ("0400", 402, 0)
|
||
assert utils.convert_metric_id_to_panel_info("4.0.3") == ("0400", 400, 3)
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_edge_case_whitespace():
|
||
"""Test convert_metric_id_to_panel_info with whitespace in metric IDs.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that whitespace is handled (int() strips whitespace).
|
||
"""
|
||
assert utils.convert_metric_id_to_panel_info(" 4") == ("0400", None, None)
|
||
assert utils.convert_metric_id_to_panel_info("4 ") == ("0400", None, None)
|
||
assert utils.convert_metric_id_to_panel_info("4 . 02") == ("0400", 402, None)
|
||
|
||
|
||
def test_convert_metric_id_to_panel_info_edge_case_dot_only():
|
||
"""Test convert_metric_id_to_panel_info with only dot character raises exception.
|
||
|
||
Args:
|
||
None
|
||
Returns:
|
||
None: Asserts that metric ID with only dot raises Exception.
|
||
"""
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info("..")
|
||
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info(".")
|
||
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info("4.")
|
||
|
||
with pytest.raises(ValueError):
|
||
utils.convert_metric_id_to_panel_info(".02")
|
||
|
||
|
||
# =============================================================================
|
||
# --- New test functions for add_counter_extra_config_input_yaml ---
|
||
# =============================================================================
|
||
|
||
|
||
def test_add_counter_invalid_architectures_type():
|
||
"""
|
||
Test that add_counter_extra_config_input_yaml raises TypeError
|
||
if 'architectures' is not a list.
|
||
"""
|
||
data = {}
|
||
with pytest.raises(TypeError, match="'architectures' must be a list, got str"):
|
||
utils.add_counter_extra_config_input_yaml(
|
||
data=data,
|
||
counter_name="test_counter",
|
||
description="A test counter",
|
||
expression="expr1",
|
||
architectures="not_a_list", # Invalid type
|
||
properties=["prop1"],
|
||
)
|
||
with pytest.raises(TypeError, match="'architectures' must be a list, got int"):
|
||
utils.add_counter_extra_config_input_yaml(
|
||
data=data,
|
||
counter_name="test_counter_2",
|
||
description="A test counter 2",
|
||
expression="expr2",
|
||
architectures=123, # Invalid type
|
||
properties=["prop1"],
|
||
)
|
||
|
||
|
||
def test_add_counter_invalid_properties_type():
|
||
"""
|
||
Test that add_counter_extra_config_input_yaml raises TypeError
|
||
if 'properties' is not a list (and not None).
|
||
"""
|
||
data = {}
|
||
with pytest.raises(TypeError, match="'properties' must be a list, got str"):
|
||
utils.add_counter_extra_config_input_yaml(
|
||
data=data,
|
||
counter_name="test_counter",
|
||
description="A test counter",
|
||
expression="expr1",
|
||
architectures=["arch1"],
|
||
properties="not_a_list", # Invalid type
|
||
)
|
||
with pytest.raises(TypeError, match="'properties' must be a list, got dict"):
|
||
utils.add_counter_extra_config_input_yaml(
|
||
data=data,
|
||
counter_name="test_counter_2",
|
||
description="A test counter 2",
|
||
expression="expr2",
|
||
architectures=["arch1"],
|
||
properties={"key": "value"}, # Invalid type
|
||
)
|
||
|
||
|
||
def test_add_counter_overwrite_existing():
|
||
"""
|
||
Test that add_counter_extra_config_input_yaml overwrites an existing counter
|
||
with the same name.
|
||
"""
|
||
data = {}
|
||
counter_name = "MY_COUNTER"
|
||
initial_description = "Initial version"
|
||
initial_expression = "initial_expr"
|
||
initial_architectures = ["gfx900"]
|
||
initial_properties = ["P_INIT"]
|
||
|
||
# Add the counter for the first time
|
||
data = utils.add_counter_extra_config_input_yaml(
|
||
data=data,
|
||
counter_name=counter_name,
|
||
description=initial_description,
|
||
expression=initial_expression,
|
||
architectures=initial_architectures,
|
||
properties=initial_properties,
|
||
)
|
||
|
||
assert len(data["rocprofiler-sdk"]["counters"]) == 1
|
||
assert data["rocprofiler-sdk"]["counters"][0]["name"] == counter_name
|
||
assert data["rocprofiler-sdk"]["counters"][0]["description"] == initial_description
|
||
assert (
|
||
data["rocprofiler-sdk"]["counters"][0]["definitions"][0]["expression"]
|
||
== initial_expression
|
||
)
|
||
|
||
updated_description = "Updated version" # noqa
|
||
updated_expression = "updated_expr" # noqa
|
||
updated_architectures = ["gfx908"] # noqa
|
||
updated_properties = ["P_UPDATED", "P_NEW"] # noqa
|
||
|
||
|
||
# =================================================================================
|
||
# Test extract counter info extra config input yaml
|
||
# =================================================================================
|
||
|
||
|
||
def test_extract_counter_info_returns_none_when_not_found():
|
||
"""
|
||
Test that extract_counter_info_extra_config_input_yaml returns None
|
||
when the counter is not found or data structure is incomplete.
|
||
"""
|
||
data_empty = {}
|
||
assert (
|
||
utils.extract_counter_info_extra_config_input_yaml(data_empty, "ANY_COUNTER")
|
||
is None
|
||
)
|
||
|
||
data_no_counters_key = {"rocprofiler-sdk": {}}
|
||
assert (
|
||
utils.extract_counter_info_extra_config_input_yaml(
|
||
data_no_counters_key, "ANY_COUNTER"
|
||
)
|
||
is None
|
||
)
|
||
|
||
data_empty_counters_list = {"rocprofiler-sdk": {"counters": []}}
|
||
assert (
|
||
utils.extract_counter_info_extra_config_input_yaml(
|
||
data_empty_counters_list, "ANY_COUNTER"
|
||
)
|
||
is None
|
||
)
|
||
|
||
data_with_other_counters = {
|
||
"rocprofiler-sdk": {
|
||
"counters": [
|
||
{"name": "EXISTING_COUNTER_1", "value": "val1"},
|
||
{"name": "EXISTING_COUNTER_2", "value": "val2"},
|
||
]
|
||
}
|
||
}
|
||
assert (
|
||
utils.extract_counter_info_extra_config_input_yaml(
|
||
data_with_other_counters, "NON_EXISTENT_COUNTER"
|
||
)
|
||
is None
|
||
)
|
||
|
||
data_with_malformed_counter = {
|
||
"rocprofiler-sdk": {
|
||
"counters": [
|
||
{"value": "val1"}, # No 'name' key
|
||
{"name": "EXISTING_COUNTER_2", "value": "val2"},
|
||
]
|
||
}
|
||
}
|
||
assert (
|
||
utils.extract_counter_info_extra_config_input_yaml(
|
||
data_with_malformed_counter, "EXISTING_COUNTER_1"
|
||
)
|
||
is None
|
||
)
|
||
assert (
|
||
utils.extract_counter_info_extra_config_input_yaml(
|
||
data_with_malformed_counter, "EXISTING_COUNTER_2"
|
||
)
|
||
is not None
|
||
)
|
||
|
||
|
||
def test_extract_counter_info_returns_counter_when_found():
|
||
"""
|
||
Test that extract_counter_info_extra_config_input_yaml returns the correct
|
||
counter dictionary when the counter is found.
|
||
"""
|
||
counter1_details = {
|
||
"name": "MY_COUNTER_1",
|
||
"description": "Desc 1",
|
||
"expression": "expr1",
|
||
}
|
||
counter2_details = {
|
||
"name": "MY_COUNTER_2",
|
||
"description": "Desc 2",
|
||
"expression": "expr2",
|
||
}
|
||
data = {
|
||
"rocprofiler-sdk": {
|
||
"counters-schema-version": 1,
|
||
"counters": [
|
||
counter1_details,
|
||
counter2_details,
|
||
],
|
||
}
|
||
}
|
||
|
||
extracted_counter1 = utils.extract_counter_info_extra_config_input_yaml(
|
||
data, "MY_COUNTER_1"
|
||
)
|
||
assert extracted_counter1 is not None
|
||
assert extracted_counter1 == counter1_details
|
||
|
||
extracted_counter2 = utils.extract_counter_info_extra_config_input_yaml(
|
||
data, "MY_COUNTER_2"
|
||
)
|
||
assert extracted_counter2 is not None
|
||
assert extracted_counter2 == counter2_details
|
||
|
||
|
||
# =============================================================================
|
||
# test using_v1 function
|
||
# =============================================================================
|
||
|
||
|
||
def test_using_v1_rocprof_set_and_ends_with_rocprof_returns_true():
|
||
"""
|
||
Covers the case where "ROCPROF" is in os.environ and its value ends with "rocprof".
|
||
This makes the entire expression True, so the function returns True.
|
||
"""
|
||
with mock.patch.dict(
|
||
os.environ, {"ROCPROF": "/opt/rocm/bin/rocprof", "OTHER_VAR": "value"}
|
||
):
|
||
assert utils.using_v1() is True
|
||
|
||
|
||
def test_using_v1_rocprof_set_but_not_ends_with_rocprof_returns_false():
|
||
"""
|
||
Covers the case where "ROCPROF" is in os.environ, but its value does
|
||
NOT end with "rocprof".
|
||
|
||
The second part of the 'and' (os.environ["ROCPROF"].endswith("rocprof")) is False.
|
||
So the function returns False.
|
||
"""
|
||
with mock.patch.dict(
|
||
os.environ, {"ROCPROF": "/opt/rocm/bin/rocprofv2", "OTHER_VAR": "value"}
|
||
):
|
||
assert utils.using_v1() is False
|
||
|
||
with mock.patch.dict(
|
||
os.environ, {"ROCPROF": "some/path/to/rocprof_tool", "OTHER_VAR": "value"}
|
||
):
|
||
assert utils.using_v1() is False
|
||
|
||
|
||
def test_using_v1_rocprof_not_in_environ_returns_false():
|
||
"""
|
||
Covers the case where "ROCPROF" is NOT in os.environ.
|
||
The first part of the 'and' ("ROCPROF" in os.environ.keys()) is False.
|
||
Due to short-circuiting, the second part is not evaluated.
|
||
So the function returns False.
|
||
"""
|
||
current_env = os.environ.copy()
|
||
if "ROCPROF" in current_env:
|
||
del current_env["ROCPROF"]
|
||
|
||
with mock.patch.dict(os.environ, current_env, clear=True):
|
||
assert utils.using_v1() is False
|
||
|
||
|
||
def test_using_v1_rocprof_is_empty_string_returns_false():
|
||
"""
|
||
Covers the case where "ROCPROF" is in os.environ but is an empty string.
|
||
The second part (os.environ["ROCPROF"].endswith("rocprof")) will be False.
|
||
So the function returns False.
|
||
"""
|
||
with mock.patch.dict(os.environ, {"ROCPROF": "", "OTHER_VAR": "value"}):
|
||
assert utils.using_v1() is False
|
||
|
||
|
||
# =============================================================================
|
||
# additional test detect_rocprof console error
|
||
# =============================================================================
|
||
class MockArgs:
|
||
def __init__(self, rocprofiler_sdk_library_path):
|
||
self.rocprofiler_sdk_library_path = rocprofiler_sdk_library_path
|
||
|
||
|
||
@mock.patch.dict(os.environ, {"ROCPROF": "rocprofiler-sdk"}, clear=True)
|
||
@mock.patch("utils.utils.console_error")
|
||
@mock.patch("utils.utils.path")
|
||
def test_detect_rocprof_calls_console_error_if_sdk_path_invalid(
|
||
mock_path_constructor, mock_console_error_func
|
||
):
|
||
"""
|
||
Tests that detect_rocprof calls console_error when ROCPROF is 'rocprofiler-sdk'
|
||
and the rocprofiler_sdk_library_path does not exist.
|
||
Focuses on the console_error call.
|
||
"""
|
||
mock_path_instance = mock.Mock()
|
||
mock_path_instance.exists.return_value = False
|
||
mock_path_constructor.return_value = mock_path_instance
|
||
|
||
fake_library_path = "/some/invalid/path/to/librocprofiler_sdk.so"
|
||
args = MockArgs(rocprofiler_sdk_library_path=fake_library_path)
|
||
|
||
with mock.patch("utils.utils.console_debug") as mock_console_debug: # noqa
|
||
utils.detect_rocprof(args)
|
||
|
||
expected_error_message = (
|
||
"Could not find rocprofiler-sdk library at " + fake_library_path
|
||
)
|
||
mock_console_error_func.assert_called_once_with(expected_error_message)
|
||
|
||
mock_path_constructor.assert_called_once_with(fake_library_path)
|
||
mock_path_instance.exists.assert_called_once()
|
||
|
||
|
||
class MockArgs: # noqa
|
||
def __init__(self, **kwargs):
|
||
self.__dict__.update(kwargs)
|
||
|
||
def __eq__(self, other):
|
||
if not isinstance(other, MockArgs):
|
||
return NotImplemented
|
||
return self.__dict__ == other.__dict__
|
||
|
||
|
||
def test_store_app_cmd_sets_global_rocprof_args():
|
||
"""
|
||
Tests that store_app_cmd correctly assigns the passed 'args'
|
||
object to the global 'rocprof_args'.
|
||
"""
|
||
sample_args_object = MockArgs(
|
||
rocprofiler_sdk_library_path="/path/to/sdk",
|
||
input_file="input.txt",
|
||
some_other_option=True,
|
||
)
|
||
|
||
if hasattr(utils, "rocprof_args"):
|
||
utils.rocprof_args = None
|
||
else:
|
||
pass
|
||
utils.store_app_cmd(sample_args_object)
|
||
assert utils.rocprof_args is sample_args_object, (
|
||
"Global rocprof_args should be the same object as the passed args"
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# additional tests for v3_counter_csv_to_v2_csv function
|
||
# =============================================================================
|
||
|
||
|
||
def create_csv_string(data_dict):
|
||
return pd.DataFrame(data_dict).to_csv(index=False)
|
||
|
||
|
||
@mock.patch("utils.utils.console_error")
|
||
@mock.patch("utils.utils.console_debug")
|
||
def test_v3_to_v2_agent_id_parsing_success_and_error(
|
||
mock_console_debug, mock_console_error, tmp_path
|
||
):
|
||
"""
|
||
Tests Line 1: Successful parsing of 'Agent Id' string.
|
||
Tests Line 2: Error during parsing of 'Agent Id' string, triggering console_error.
|
||
"""
|
||
agent_info_content = create_csv_string({
|
||
"Node_Id": [0, 1],
|
||
"Agent_Type": ["CPU", "GPU"],
|
||
"Wave_Front_Size": [0, 64],
|
||
})
|
||
agent_info_filepath = tmp_path / "agent_info.csv"
|
||
agent_info_filepath.write_text(agent_info_content)
|
||
converted_csv_filepath = tmp_path / "converted.csv"
|
||
counter_content_success = create_csv_string({
|
||
"Correlation_Id": [1],
|
||
"Dispatch_Id": [10],
|
||
"Agent_Id": ["Agent 1"],
|
||
"Queue_Id": [100],
|
||
"Process_Id": [1000],
|
||
"Thread_Id": [10000],
|
||
"Grid_Size": [256],
|
||
"Kernel_Id": [1],
|
||
"Kernel_Name": ["kernelA"],
|
||
"Workgroup_Size": [64],
|
||
"LDS_Block_Size": [32],
|
||
"Scratch_Size": [0],
|
||
"VGPR_Count": [16],
|
||
"Accum_VGPR_Count": [0],
|
||
"SGPR_Count": [32],
|
||
"Start_Timestamp": [100000],
|
||
"End_Timestamp": [100100],
|
||
"Counter_Name": ["Cycles"],
|
||
"Counter_Value": [5000],
|
||
})
|
||
counter_filepath_success = tmp_path / "counter_success.csv"
|
||
counter_filepath_success.write_text(counter_content_success)
|
||
|
||
utils.v3_counter_csv_to_v2_csv(
|
||
str(counter_filepath_success),
|
||
str(agent_info_filepath),
|
||
str(converted_csv_filepath),
|
||
)
|
||
|
||
mock_console_error.assert_not_called()
|
||
result_df_success = pd.read_csv(converted_csv_filepath)
|
||
assert "GPU_ID" in result_df_success.columns
|
||
assert result_df_success["GPU_ID"].iloc[0] == 0
|
||
assert result_df_success["GPU_ID"].dtype == "int64"
|
||
|
||
mock_console_error.reset_mock()
|
||
|
||
counter_content_error = create_csv_string({
|
||
"Correlation_Id": [2],
|
||
"Dispatch_Id": [20],
|
||
"Agent_Id": ["Malformed Agent X"],
|
||
"Queue_Id": [200],
|
||
"Process_Id": [2000],
|
||
"Thread_Id": [20000],
|
||
"Grid_Size": [512],
|
||
"Kernel_Id": [2],
|
||
"Kernel_Name": ["kernelB"],
|
||
"Workgroup_Size": [128],
|
||
"LDS_Block_Size": [64],
|
||
"Scratch_Size": [0],
|
||
"VGPR_Count": [32],
|
||
"Accum_VGPR_Count": [0],
|
||
"SGPR_Count": [64],
|
||
"Start_Timestamp": [200000],
|
||
"End_Timestamp": [200200],
|
||
"Counter_Name": ["Instructions"],
|
||
"Counter_Value": [10000],
|
||
})
|
||
counter_filepath_error = tmp_path / "counter_error.csv"
|
||
counter_filepath_error.write_text(counter_content_error)
|
||
|
||
try:
|
||
utils.v3_counter_csv_to_v2_csv(
|
||
str(counter_filepath_error),
|
||
str(agent_info_filepath),
|
||
str(converted_csv_filepath),
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
mock_console_error.assert_called_once()
|
||
call_args = mock_console_error.call_args[0][0]
|
||
assert 'Parsing rocprofv3 csv output: Error of getting "Agent_Id"' in call_args
|
||
assert (
|
||
"AttributeError" in call_args
|
||
or "'NoneType' object has no attribute 'group'" in call_args
|
||
)
|
||
|
||
|
||
@mock.patch("utils.utils.console_debug") # To suppress debug output
|
||
def test_v3_to_v2_accum_column_rename(mock_console_debug, tmp_path):
|
||
"""
|
||
Tests Line 3: Renaming of a column ending with '_ACCUM' to 'SQ_ACCUM_PREV_HIRES'.
|
||
"""
|
||
# --- Setup ---
|
||
agent_info_content = create_csv_string({
|
||
"Node_Id": [0],
|
||
"Agent_Type": ["GPU"],
|
||
"Wave_Front_Size": [64],
|
||
})
|
||
agent_info_filepath = tmp_path / "agent_info.csv"
|
||
agent_info_filepath.write_text(agent_info_content)
|
||
converted_csv_filepath = tmp_path / "converted_accum.csv"
|
||
|
||
counter_data = {
|
||
"Correlation_Id": [1, 1],
|
||
"Dispatch_Id": [10, 10],
|
||
"Agent_Id": [0, 0],
|
||
"Queue_Id": [100, 100],
|
||
"Process_Id": [1000, 1000],
|
||
"Thread_Id": [10000, 10000],
|
||
"Grid_Size": [256, 256],
|
||
"Kernel_Id": [1, 1],
|
||
"Kernel_Name": ["kernelA", "kernelA"],
|
||
"Workgroup_Size": [64, 64],
|
||
"LDS_Block_Size": [32, 32],
|
||
"Scratch_Size": [0, 0],
|
||
"VGPR_Count": [16, 16],
|
||
"Accum_VGPR_Count": [0, 0],
|
||
"SGPR_Count": [32, 32],
|
||
"Start_Timestamp": [100000, 100000],
|
||
"End_Timestamp": [100100, 100100],
|
||
"Counter_Name": ["FETCH_SIZE_ACCUM", "CYCLES"],
|
||
"Counter_Value": [12345, 5000],
|
||
}
|
||
counter_content = create_csv_string(counter_data)
|
||
counter_filepath = tmp_path / "counter_accum.csv"
|
||
counter_filepath.write_text(counter_content)
|
||
|
||
utils.v3_counter_csv_to_v2_csv(
|
||
str(counter_filepath), str(agent_info_filepath), str(converted_csv_filepath)
|
||
)
|
||
|
||
result_df = pd.read_csv(converted_csv_filepath)
|
||
assert "SQ_ACCUM_PREV_HIRES" in result_df.columns
|
||
assert "FETCH_SIZE_ACCUM" not in result_df.columns
|
||
assert "CYCLES" in result_df.columns
|
||
assert result_df["SQ_ACCUM_PREV_HIRES"].iloc[0] == 12345
|
||
assert result_df["CYCLES"].iloc[0] == 5000
|
||
|
||
|
||
@mock.patch("utils.utils.console_debug")
|
||
def test_v3_to_v2_default_accum_vgpr_count(mock_console_debug, tmp_path):
|
||
"""
|
||
Tests Line 4: 'Accum_VGPR_Count' is added and set to 0 if not present in input.
|
||
"""
|
||
agent_info_content = create_csv_string({
|
||
"Node_Id": [0],
|
||
"Agent_Type": ["GPU"],
|
||
"Wave_Front_Size": [64],
|
||
})
|
||
agent_info_filepath = tmp_path / "agent_info.csv"
|
||
agent_info_filepath.write_text(agent_info_content)
|
||
converted_csv_filepath = tmp_path / "converted_no_accum_vgpr.csv"
|
||
|
||
counter_content = create_csv_string({
|
||
"Correlation_Id": [1],
|
||
"Dispatch_Id": [10],
|
||
"Agent_Id": [0],
|
||
"Queue_Id": [100],
|
||
"Process_Id": [1000],
|
||
"Thread_Id": [10000],
|
||
"Grid_Size": [256],
|
||
"Kernel_Id": [1],
|
||
"Kernel_Name": ["kernelA"],
|
||
"Workgroup_Size": [64],
|
||
"LDS_Block_Size": [32],
|
||
"Scratch_Size": [0],
|
||
"VGPR_Count": [16],
|
||
"SGPR_Count": [32],
|
||
"Start_Timestamp": [100000],
|
||
"End_Timestamp": [100100],
|
||
"Counter_Name": ["Cycles"],
|
||
"Counter_Value": [5000],
|
||
})
|
||
counter_filepath = tmp_path / "counter_no_accum_vgpr.csv"
|
||
counter_filepath.write_text(counter_content)
|
||
|
||
utils.v3_counter_csv_to_v2_csv(
|
||
str(counter_filepath), str(agent_info_filepath), str(converted_csv_filepath)
|
||
)
|
||
|
||
result_df = pd.read_csv(converted_csv_filepath)
|
||
assert "Accum_VGPR" in result_df.columns
|
||
assert result_df["Accum_VGPR"].iloc[0] == 0
|
||
assert result_df["Accum_VGPR"].dtype == "int64"
|
||
|
||
|
||
# ===================================================================
|
||
# Test PC_sampling function
|
||
# ===================================================================
|
||
|
||
|
||
@mock.patch("utils.utils.capture_subprocess_output")
|
||
@mock.patch("utils.utils.console_error")
|
||
@mock.patch("utils.utils.console_debug")
|
||
def test_pc_sampling_prof_sdk_path_nonexistent_librocprofiler_sdk_tool(
|
||
mock_console_debug, mock_console_error, mock_capture_subprocess, tmp_path
|
||
):
|
||
"""
|
||
Edge Case: rocprofiler_sdk_library_path is valid, but librocprofiler-sdk-tool.so
|
||
is NOT found next to it (or in rocprofiler-sdk subdir).
|
||
This test primarily checks if the paths are constructed. The actual check for
|
||
file existence before `capture_subprocess_output` is not in the provided snippet,
|
||
but we test the path construction.
|
||
"""
|
||
with mock.patch("utils.utils.rocprof_cmd", "rocprofiler-sdk"):
|
||
method = "host_trap"
|
||
interval = 1000
|
||
workload_dir = str(tmp_path)
|
||
appcmd = "my_app --arg"
|
||
|
||
sdk_lib_dir = tmp_path / "rocm_sdk" / "lib"
|
||
sdk_lib_dir.mkdir(parents=True, exist_ok=True)
|
||
rocprofiler_sdk_library_path = str(sdk_lib_dir / "librocprofiler_sdk.so")
|
||
pathlib.Path(rocprofiler_sdk_library_path).touch()
|
||
|
||
expected_tool_path = str(
|
||
sdk_lib_dir / "rocprofiler-sdk" / "librocprofiler-sdk-tool.so"
|
||
)
|
||
|
||
mock_capture_subprocess.return_value = (True, "Success output")
|
||
|
||
utils.pc_sampling_prof(
|
||
method, interval, workload_dir, appcmd, rocprofiler_sdk_library_path
|
||
)
|
||
|
||
assert mock_capture_subprocess.called
|
||
call_args = mock_capture_subprocess.call_args
|
||
called_env = call_args.kwargs.get("new_env", {})
|
||
|
||
assert "LD_PRELOAD" in called_env
|
||
ld_preload_paths = called_env["LD_PRELOAD"].split(":")
|
||
assert expected_tool_path in ld_preload_paths
|
||
assert rocprofiler_sdk_library_path in ld_preload_paths
|
||
|
||
mock_console_error.assert_not_called()
|
||
|
||
|
||
@mock.patch("utils.utils.capture_subprocess_output")
|
||
@mock.patch("utils.utils.console_error")
|
||
@mock.patch("utils.utils.console_debug")
|
||
def test_pc_sampling_prof_subprocess_fails(
|
||
mock_console_debug, mock_console_error, mock_capture_subprocess, tmp_path
|
||
):
|
||
"""
|
||
Edge Case: The capture_subprocess_output returns success=False.
|
||
This should trigger the console_error("PC sampling failed.").
|
||
"""
|
||
with mock.patch("utils.utils.rocprof_cmd", "rocprof_cli_tool"):
|
||
method = "stochastic"
|
||
interval = 5000
|
||
workload_dir = str(tmp_path)
|
||
appcmd = "another_app"
|
||
rocprofiler_sdk_library_path = "/some/path/librocprofiler_sdk.so"
|
||
|
||
mock_capture_subprocess.return_value = (False, "Error output from subprocess")
|
||
|
||
utils.pc_sampling_prof(
|
||
method, interval, workload_dir, appcmd, rocprofiler_sdk_library_path
|
||
)
|
||
|
||
mock_capture_subprocess.assert_called_once()
|
||
mock_console_error.assert_called_once_with("PC sampling failed.")
|
||
|
||
mock_capture_subprocess.reset_mock()
|
||
mock_console_error.reset_mock()
|
||
with mock.patch("utils.utils.rocprof_cmd", "rocprofiler-sdk"):
|
||
sdk_lib_dir = tmp_path / "rocm_sdk_fail" / "lib"
|
||
sdk_lib_dir.mkdir(parents=True, exist_ok=True)
|
||
rocprofiler_sdk_library_path_sdk = str(sdk_lib_dir / "librocprofiler_sdk.so")
|
||
pathlib.Path(rocprofiler_sdk_library_path_sdk).touch()
|
||
|
||
tool_dir = sdk_lib_dir / "rocprofiler-sdk"
|
||
tool_dir.mkdir(parents=True, exist_ok=True)
|
||
(tool_dir / "librocprofiler-sdk-tool.so").touch()
|
||
|
||
mock_capture_subprocess.return_value = (
|
||
False,
|
||
"Error output from SDK subprocess",
|
||
)
|
||
|
||
utils.pc_sampling_prof(
|
||
method, interval, workload_dir, appcmd, rocprofiler_sdk_library_path_sdk
|
||
)
|
||
|
||
mock_capture_subprocess.assert_called_once()
|
||
mock_console_error.assert_called_once_with("PC sampling failed.")
|
||
|
||
|
||
@mock.patch("utils.utils.capture_subprocess_output")
|
||
@mock.patch("utils.utils.console_error")
|
||
@mock.patch("utils.utils.console_debug")
|
||
def test_pc_sampling_prof_empty_appcmd(
|
||
mock_console_debug, mock_console_error, mock_capture_subprocess, tmp_path
|
||
):
|
||
"""
|
||
Edge Case: The appcmd is an empty string.
|
||
The function should still attempt to run it. The behavior of
|
||
capture_subprocess_output with an empty command is external to this function.
|
||
"""
|
||
with mock.patch("utils.utils.rocprof_cmd", "rocprof_cli_tool"):
|
||
method = "host_trap"
|
||
interval = 100
|
||
workload_dir = str(tmp_path)
|
||
appcmd = ""
|
||
rocprofiler_sdk_library_path = "/some/path/librocprofiler_sdk.so"
|
||
|
||
mock_capture_subprocess.return_value = (True, "Output with empty appcmd")
|
||
|
||
utils.pc_sampling_prof(
|
||
method, interval, workload_dir, appcmd, rocprofiler_sdk_library_path
|
||
)
|
||
|
||
assert mock_capture_subprocess.called
|
||
options_list = mock_capture_subprocess.call_args[0][0]
|
||
assert options_list[-1] == "--"
|
||
mock_console_error.assert_not_called()
|
||
|
||
mock_capture_subprocess.reset_mock()
|
||
mock_console_error.reset_mock()
|
||
with mock.patch("utils.utils.rocprof_cmd", "rocprofiler-sdk"):
|
||
sdk_lib_dir = tmp_path / "rocm_sdk_empty" / "lib"
|
||
sdk_lib_dir.mkdir(parents=True, exist_ok=True)
|
||
rocprofiler_sdk_library_path_sdk = str(sdk_lib_dir / "librocprofiler_sdk.so")
|
||
pathlib.Path(rocprofiler_sdk_library_path_sdk).touch()
|
||
tool_dir = sdk_lib_dir / "rocprofiler-sdk"
|
||
tool_dir.mkdir(parents=True, exist_ok=True)
|
||
(tool_dir / "librocprofiler-sdk-tool.so").touch()
|
||
|
||
mock_capture_subprocess.return_value = (True, "Output with empty appcmd SDK")
|
||
|
||
utils.pc_sampling_prof(
|
||
method, interval, workload_dir, appcmd, rocprofiler_sdk_library_path_sdk
|
||
)
|
||
|
||
assert mock_capture_subprocess.called
|
||
assert mock_capture_subprocess.call_args[0][0] == ""
|
||
mock_console_error.assert_not_called()
|
||
|
||
|
||
# =============================================================================
|
||
# test replace_timestamps function
|
||
# =============================================================================
|
||
|
||
|
||
def create_dummy_csv(filepath, data_dict):
|
||
df = pd.DataFrame(data_dict)
|
||
df.to_csv(filepath, index=False)
|
||
|
||
|
||
@mock.patch("utils.utils.console_warning")
|
||
@mock.patch("utils.utils.path")
|
||
def test_replace_timestamps_no_timestamps_csv_returns_early(
|
||
mock_path_util, mock_console_warning, tmp_path
|
||
):
|
||
"""
|
||
Edge Case: timestamps.csv does not exist in workload_dir.
|
||
The function should return early.
|
||
Covers: if not path(workload_dir, "timestamps.csv").is_file(): return
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
|
||
mock_timestamps_path_obj = mock.Mock()
|
||
mock_timestamps_path_obj.is_file.return_value = False
|
||
|
||
mock_path_util.side_effect = lambda *args: (
|
||
mock_timestamps_path_obj if args[1] == "timestamps.csv" else mock.DEFAULT
|
||
)
|
||
|
||
utils.replace_timestamps(workload_dir)
|
||
|
||
mock_path_util.assert_any_call(workload_dir, "timestamps.csv")
|
||
mock_timestamps_path_obj.is_file.assert_called_once()
|
||
mock_console_warning.assert_not_called()
|
||
|
||
|
||
@mock.patch("utils.utils.console_warning")
|
||
@mock.patch("glob.glob") # Mock glob.glob
|
||
@mock.patch("utils.utils.path")
|
||
def test_replace_timestamps_timestamps_csv_missing_columns_warns(
|
||
mock_path_util, mock_glob, mock_console_warning, tmp_path
|
||
):
|
||
"""
|
||
Edge Case: timestamps.csv exists but is missing
|
||
'Start_Timestamp' or 'End_Timestamp'.
|
||
The function should call console_warning.
|
||
Covers: else: console_warning(...)
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
timestamps_csv_path_str = os.path.join(workload_dir, "timestamps.csv")
|
||
|
||
create_dummy_csv(timestamps_csv_path_str, {"Some_Other_Column": [123]})
|
||
|
||
mock_timestamps_path_obj = mock.Mock()
|
||
mock_timestamps_path_obj.is_file.return_value = True
|
||
mock_timestamps_path_obj.name = "timestamps.csv"
|
||
|
||
mock_path_util.side_effect = lambda *args, **kwargs: (
|
||
mock_timestamps_path_obj if args[-1] == "timestamps.csv" else mock.DEFAULT
|
||
)
|
||
|
||
utils.replace_timestamps(workload_dir)
|
||
|
||
mock_path_util.assert_any_call(workload_dir, "timestamps.csv")
|
||
mock_timestamps_path_obj.is_file.assert_called_once()
|
||
mock_console_warning.assert_called_once_with(
|
||
"Incomplete profiling data detected. Unable to update timestamps.\n"
|
||
)
|
||
mock_glob.assert_not_called()
|
||
|
||
|
||
@mock.patch("utils.utils.console_warning")
|
||
@mock.patch("glob.glob")
|
||
@mock.patch("utils.utils.path")
|
||
def test_replace_timestamps_updates_other_csvs_skips_sysinfo(
|
||
mock_path_util, mock_glob, mock_console_warning, tmp_path
|
||
):
|
||
"""
|
||
Edge Case: timestamps.csv is valid. Other CSVs exist, including sysinfo.csv.
|
||
Only non-sysinfo.csv files should be updated.
|
||
Covers: for fname in glob.glob(...): if path(fname).name != "sysinfo.csv": ...
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
timestamps_csv_path_str = os.path.join(workload_dir, "timestamps.csv")
|
||
data_csv_path_str = os.path.join(workload_dir, "data.csv")
|
||
sysinfo_csv_path_str = os.path.join(workload_dir, "sysinfo.csv")
|
||
|
||
new_start_ts = [1000, 2000]
|
||
new_end_ts = [1500, 2500]
|
||
create_dummy_csv(
|
||
timestamps_csv_path_str,
|
||
{"Start_Timestamp": new_start_ts, "End_Timestamp": new_end_ts},
|
||
)
|
||
|
||
create_dummy_csv(
|
||
data_csv_path_str,
|
||
{"Kernel_Name": ["A", "B"], "Start_Timestamp": [1, 2], "End_Timestamp": [3, 4]},
|
||
)
|
||
create_dummy_csv(
|
||
sysinfo_csv_path_str,
|
||
{"Info": ["CPU", "MEM"], "Start_Timestamp": [5, 6], "End_Timestamp": [7, 8]},
|
||
)
|
||
|
||
def path_side_effect(*args, **kwargs):
|
||
p_obj = mock.Mock()
|
||
full_path = args[0] if len(args) == 1 else os.path.join(args[0], args[1])
|
||
|
||
if full_path == timestamps_csv_path_str:
|
||
p_obj.is_file.return_value = True
|
||
p_obj.name = "timestamps.csv"
|
||
elif full_path == data_csv_path_str:
|
||
p_obj.is_file.return_value = True
|
||
p_obj.name = "data.csv"
|
||
elif full_path == sysinfo_csv_path_str:
|
||
p_obj.is_file.return_value = True
|
||
p_obj.name = "sysinfo.csv"
|
||
else:
|
||
p_obj.is_file.return_value = False
|
||
p_obj.name = os.path.basename(full_path)
|
||
return p_obj
|
||
|
||
mock_path_util.side_effect = path_side_effect
|
||
|
||
mock_glob.return_value = [
|
||
data_csv_path_str,
|
||
sysinfo_csv_path_str,
|
||
timestamps_csv_path_str,
|
||
]
|
||
|
||
utils.replace_timestamps(workload_dir)
|
||
|
||
mock_console_warning.assert_not_called()
|
||
|
||
df_data_updated = pd.read_csv(data_csv_path_str)
|
||
pd.testing.assert_series_equal(
|
||
df_data_updated["Start_Timestamp"],
|
||
pd.Series(new_start_ts, name="Start_Timestamp"),
|
||
)
|
||
pd.testing.assert_series_equal(
|
||
df_data_updated["End_Timestamp"], pd.Series(new_end_ts, name="End_Timestamp")
|
||
)
|
||
|
||
df_sysinfo_original = pd.read_csv(sysinfo_csv_path_str)
|
||
assert list(df_sysinfo_original["Start_Timestamp"]) == [5, 6]
|
||
assert list(df_sysinfo_original["End_Timestamp"]) == [7, 8]
|
||
|
||
|
||
@mock.patch("utils.utils.console_warning")
|
||
@mock.patch("glob.glob")
|
||
@mock.patch("utils.utils.path")
|
||
def test_replace_timestamps_no_other_csvs_to_update(
|
||
mock_path_util, mock_glob, mock_console_warning, tmp_path
|
||
):
|
||
"""
|
||
Edge Case: timestamps.csv is valid, but no other *.csv files
|
||
(or only sysinfo.csv) exist.
|
||
The loop for updating files should not do anything or not run.
|
||
Covers: The for loop not iterating if glob returns empty or only sysinfo.
|
||
"""
|
||
workload_dir = str(tmp_path)
|
||
timestamps_csv_path_str = os.path.join(workload_dir, "timestamps.csv")
|
||
sysinfo_csv_path_str = os.path.join(workload_dir, "sysinfo.csv")
|
||
|
||
create_dummy_csv(
|
||
timestamps_csv_path_str, {"Start_Timestamp": [100], "End_Timestamp": [200]}
|
||
)
|
||
create_dummy_csv(
|
||
sysinfo_csv_path_str,
|
||
{"Info": ["CPU"], "Start_Timestamp": [5], "End_Timestamp": [7]},
|
||
)
|
||
|
||
def path_side_effect(*args, **kwargs):
|
||
p_obj = mock.Mock()
|
||
full_path = args[0] if len(args) == 1 else os.path.join(args[0], args[1])
|
||
if full_path == timestamps_csv_path_str:
|
||
p_obj.is_file.return_value = True
|
||
p_obj.name = "timestamps.csv"
|
||
elif full_path == sysinfo_csv_path_str:
|
||
p_obj.is_file.return_value = True
|
||
p_obj.name = "sysinfo.csv"
|
||
else:
|
||
p_obj.is_file.return_value = False
|
||
p_obj.name = os.path.basename(full_path)
|
||
return p_obj
|
||
|
||
mock_path_util.side_effect = path_side_effect
|
||
|
||
mock_glob.return_value = [timestamps_csv_path_str, sysinfo_csv_path_str]
|
||
|
||
utils.replace_timestamps(workload_dir)
|
||
|
||
mock_console_warning.assert_not_called()
|
||
df_sysinfo_original = pd.read_csv(sysinfo_csv_path_str)
|
||
assert list(df_sysinfo_original["Start_Timestamp"]) == [5]
|
||
assert list(df_sysinfo_original["End_Timestamp"]) == [7]
|
||
|
||
|
||
def test_set_parser():
|
||
from utils.utils import parse_sets_yaml
|
||
|
||
result = parse_sets_yaml("gfx90a")
|
||
|
||
assert "compute_thruput_util" in result
|
||
assert result["compute_thruput_util"]["title"] == "Compute Throughput Utilization"
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_scientific_notation_trigger_below_lower_bound():
|
||
value = 0.0001
|
||
result = utils.format_scientific_notation_if_needed(value)
|
||
assert pytest.approx(float(result.strip()), rel=1e-9) == value
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_scientific_notation_trigger_at_lower_bound():
|
||
value = 0.01
|
||
result = utils.format_scientific_notation_if_needed(value)
|
||
assert pytest.approx(float(result.strip()), rel=1e-9) == value
|
||
|
||
|
||
def test_scientific_notation_trigger_above_upper_bound():
|
||
value = 1234567890
|
||
result = utils.format_scientific_notation_if_needed(value)
|
||
assert pytest.approx(float(result.strip()), rel=1e-9) == value
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_scientific_notation_trigger_just_below_upper_bound():
|
||
value = 999999
|
||
result = utils.format_scientific_notation_if_needed(value, precision=6)
|
||
assert pytest.approx(float(result.strip()), rel=1e-6) == value
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_scientific_notation_trigger_zero():
|
||
value = 0
|
||
result = utils.format_scientific_notation_if_needed(value)
|
||
assert float(result.strip()) == value # Exact match for zero
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_scientific_notation_trigger_slightly_below_lower_bound():
|
||
value = 0.009
|
||
result = utils.format_scientific_notation_if_needed(value)
|
||
assert pytest.approx(float(result.strip()), rel=1e-9) == value
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_scientific_notation_trigger_well_below_lower_bound():
|
||
value = 1e-5
|
||
result = utils.format_scientific_notation_if_needed(value)
|
||
assert pytest.approx(float(result.strip()), rel=1e-9) == value
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_scientific_notation_trigger_well_above_upper_bound():
|
||
value = 1e10
|
||
result = utils.format_scientific_notation_if_needed(value)
|
||
assert pytest.approx(float(result.strip()), rel=1e-9) == value
|
||
|
||
|
||
@pytest.mark.sci_notion
|
||
def test_alignment_and_width():
|
||
value = 1e10
|
||
result = utils.format_scientific_notation_if_needed(
|
||
value,
|
||
align=">",
|
||
width_align=12,
|
||
precision=2,
|
||
fmt_type_align="f",
|
||
max_length=8,
|
||
)
|
||
assert pytest.approx(float(result.strip()), rel=1e-9) == value
|
||
|
||
|
||
# =============================================================================
|
||
# TESTS FOR MODELESS COMMAND LINE OPTIONS
|
||
# =============================================================================
|
||
|
||
|
||
@pytest.mark.list_metrics
|
||
def test_list_metrics(binary_handler_analyze_rocprof_compute, capsys):
|
||
return_code = binary_handler_analyze_rocprof_compute(["--list-metrics", "gfx90a"])
|
||
assert return_code == 0
|
||
|
||
# Test output
|
||
output = capsys.readouterr().out
|
||
assert "6 -> Workgroup Manager (SPI)" in output
|
||
assert "5.2 -> Command processor packet processor (CPC)" in output
|