CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
563 lines
19 KiB
Python
563 lines
19 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
Unit tests for TUI (Text User Interface) components.
|
|
|
|
These tests cover the critical functionality of the TUI analysis system,
|
|
including dataframe processing, aggregated kernel analysis,
|
|
and widget creation.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from rocprof_compute_tui.widgets.instant_button import InstantButton
|
|
from rocprof_compute_tui.widgets.menu_bar.menu_bar import DropdownMenu, MenuButton
|
|
|
|
# Mark all tests in this file with the 'tui' marker for pytest
|
|
pytestmark = pytest.mark.tui
|
|
|
|
# =============================================================================
|
|
# Tests for InstantButton Widget
|
|
# =============================================================================
|
|
|
|
|
|
class TestInstantButton:
|
|
"""Test suite for InstantButton widget."""
|
|
|
|
def test_instant_button_posts_message_exactly_once(self):
|
|
"""Test that InstantButton posts message exactly once per press."""
|
|
button = InstantButton("Test Button")
|
|
button.post_message = Mock()
|
|
|
|
# Create a Button.Pressed event
|
|
event = MagicMock()
|
|
event.button = button
|
|
|
|
# Press button once
|
|
button.on_button_pressed(event)
|
|
|
|
# Should have posted exactly one message
|
|
assert button.post_message.call_count == 1
|
|
posted_message = button.post_message.call_args[0][0]
|
|
assert isinstance(posted_message, InstantButton.InstantPressed)
|
|
assert posted_message.button is button
|
|
|
|
def test_instant_button_ignores_other_button_events(self):
|
|
"""Test that InstantButton ignores events from other buttons."""
|
|
button = InstantButton("Test Button")
|
|
other_button = InstantButton("Other Button")
|
|
button.post_message = Mock()
|
|
|
|
# Create event from different button
|
|
event = MagicMock()
|
|
event.button = other_button
|
|
|
|
button.on_button_pressed(event)
|
|
|
|
# Should not have posted any message
|
|
assert not button.post_message.called
|
|
|
|
def test_trigger_posts_instant_pressed(self):
|
|
"""Test that trigger() method posts InstantPressed message."""
|
|
button = InstantButton("Test Button")
|
|
button.post_message = Mock()
|
|
|
|
button.trigger()
|
|
|
|
# Verify InstantPressed was posted
|
|
assert button.post_message.called
|
|
posted_message = button.post_message.call_args[0][0]
|
|
assert isinstance(posted_message, InstantButton.InstantPressed)
|
|
assert posted_message.button is button
|
|
|
|
|
|
# =============================================================================
|
|
# Tests for DropdownMenu Widget
|
|
# =============================================================================
|
|
|
|
|
|
class TestDropdownMenu:
|
|
"""Test suite for DropdownMenu widget."""
|
|
|
|
def test_is_visible_false_sets_hidden_state(self):
|
|
"""Test that is_visible=False sets correct hidden styles."""
|
|
menu = DropdownMenu()
|
|
menu.styles = MagicMock()
|
|
menu.refresh = Mock()
|
|
|
|
# Trigger the watcher by setting is_visible
|
|
menu.watch_is_visible(False)
|
|
|
|
# Verify styles are set for hidden state
|
|
assert menu.styles.pointer_events == "none"
|
|
assert menu.styles.visibility == "hidden"
|
|
assert menu.styles.opacity == 0.0
|
|
assert menu.display is False
|
|
menu.refresh.assert_called_with(repaint=True, layout=False)
|
|
|
|
def test_is_visible_true_sets_visible_state(self):
|
|
"""Test that is_visible=True sets correct visible styles."""
|
|
menu = DropdownMenu()
|
|
menu.styles = MagicMock()
|
|
menu.refresh = Mock()
|
|
|
|
# Trigger the watcher by setting is_visible
|
|
menu.watch_is_visible(True)
|
|
|
|
# Verify styles are set for visible state
|
|
assert menu.display is True
|
|
assert menu.styles.pointer_events == "auto"
|
|
assert menu.styles.visibility == "visible"
|
|
assert menu.styles.opacity == 1.0
|
|
menu.refresh.assert_called_with(repaint=True, layout=False)
|
|
|
|
def test_check_focus_closes_when_sequence_matches(self):
|
|
"""Test that _check_focus_and_close closes menu when no focus after blur."""
|
|
menu = DropdownMenu()
|
|
menu.is_visible = True
|
|
menu.hide = Mock()
|
|
|
|
# Mock the app property using patch
|
|
with patch.object(type(menu), "app", new_callable=lambda: MagicMock()):
|
|
menu.app.focused = None
|
|
|
|
# Set event sequence to 5 (no focus after blur)
|
|
menu._event_sequence = 5
|
|
|
|
# Call with blur sequence 5 (same as current, so no focus occurred)
|
|
menu._check_focus_and_close(5)
|
|
|
|
# Should have called hide
|
|
menu.hide.assert_called_once()
|
|
|
|
def test_check_focus_ignores_old_blur_events(self):
|
|
"""Test that _check_focus_and_close ignores old blur events."""
|
|
menu = DropdownMenu()
|
|
menu.is_visible = True
|
|
menu.hide = Mock()
|
|
|
|
# Current event sequence is newer (focus occurred after the blur)
|
|
menu._event_sequence = 10
|
|
|
|
# Call with old blur sequence
|
|
menu._check_focus_and_close(5)
|
|
|
|
# Should not have called hide (focus event at seq 10 > blur seq 5)
|
|
assert not menu.hide.called
|
|
|
|
def test_check_focus_stays_open_when_refocused(self):
|
|
"""Test that _check_focus_and_close stays open if focus was regained."""
|
|
menu = DropdownMenu()
|
|
menu.is_visible = True
|
|
menu.hide = Mock()
|
|
|
|
# Blur happened at sequence 5, then focus at sequence 6
|
|
# Current event sequence is 6 (focus was regained)
|
|
menu._event_sequence = 6
|
|
|
|
# Call with blur sequence 5
|
|
menu._check_focus_and_close(5)
|
|
|
|
# Should not close because focus was regained (6 > 5)
|
|
assert not menu.hide.called
|
|
|
|
def test_show_sets_visible_and_focuses(self):
|
|
"""Test that show() sets is_visible=True and focuses menu."""
|
|
menu = DropdownMenu()
|
|
menu.focus = Mock()
|
|
|
|
menu.show()
|
|
|
|
assert menu.is_visible is True
|
|
menu.focus.assert_called_once()
|
|
|
|
def test_hide_sets_not_visible_and_posts_closed(self):
|
|
"""Test that hide() sets is_visible=False and posts Closed message."""
|
|
menu = DropdownMenu()
|
|
menu.is_visible = True # Set visible first
|
|
menu.post_message = Mock()
|
|
|
|
menu.hide()
|
|
|
|
assert menu.is_visible is False
|
|
assert menu.post_message.called
|
|
posted_message = menu.post_message.call_args[0][0]
|
|
assert isinstance(posted_message, DropdownMenu.Closed)
|
|
|
|
def test_hide_is_idempotent(self):
|
|
"""Test that hide() does nothing when menu is already hidden."""
|
|
menu = DropdownMenu()
|
|
menu.is_visible = False # Already hidden
|
|
menu.post_message = Mock()
|
|
|
|
menu.hide()
|
|
|
|
# Should not post Closed message when already hidden
|
|
assert not menu.post_message.called
|
|
assert menu.is_visible is False
|
|
|
|
|
|
# =============================================================================
|
|
# Tests for MenuButton Widget
|
|
# =============================================================================
|
|
|
|
|
|
class TestMenuButton:
|
|
"""Test suite for MenuButton widget."""
|
|
|
|
def test_is_open_true_shows_dropdown(self):
|
|
"""Test that is_open=True calls dropdown.show()."""
|
|
button = MenuButton("File", "test-dropdown")
|
|
dropdown = MagicMock()
|
|
button._dropdown = dropdown
|
|
button.add_class = Mock()
|
|
button.refresh = Mock()
|
|
|
|
button.watch_is_open(True)
|
|
|
|
dropdown.show.assert_called_once()
|
|
button.add_class.assert_called_with("-active")
|
|
|
|
def test_is_open_false_hides_dropdown(self):
|
|
"""Test that is_open=False calls dropdown.hide()."""
|
|
button = MenuButton("File", "test-dropdown")
|
|
dropdown = MagicMock()
|
|
button._dropdown = dropdown
|
|
button.remove_class = Mock()
|
|
button.refresh = Mock()
|
|
|
|
button.watch_is_open(False)
|
|
|
|
dropdown.hide.assert_called_once()
|
|
button.remove_class.assert_called_with("-active")
|
|
|
|
def test_button_pressed_toggles_is_open(self):
|
|
"""Test that button press toggles is_open state."""
|
|
button = MenuButton("File", "test-dropdown")
|
|
button.is_open = False
|
|
|
|
event = MagicMock()
|
|
event.button = button
|
|
|
|
button.on_button_pressed(event)
|
|
|
|
assert button.is_open is True
|
|
|
|
# Press again
|
|
button.on_button_pressed(event)
|
|
|
|
assert button.is_open is False
|
|
|
|
def test_dropdown_closed_sets_is_open_false(self):
|
|
"""Test that dropdown closed event sets is_open to False."""
|
|
button = MenuButton("File", "test-dropdown")
|
|
button.is_open = True
|
|
|
|
event = DropdownMenu.Closed()
|
|
|
|
button.on_dropdown_closed(event)
|
|
|
|
assert button.is_open is False
|
|
|
|
|
|
# =============================================================================
|
|
# Test Fixtures
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_top_kernel_df() -> pd.DataFrame:
|
|
"""Create a sample top kernel dataframe (dfs[1])
|
|
with aggregated per-kernel stats."""
|
|
return pd.DataFrame({
|
|
"Kernel_Name": ["kernel_a", "kernel_b", "kernel_c"],
|
|
"Percent": [50.0, 30.0, 20.0],
|
|
"Count": [10, 5, 8],
|
|
"Total_Time": [1500.0, 900.0, 600.0],
|
|
})
|
|
|
|
|
|
# =============================================================================
|
|
# Tests for tui_utils.py - get_top_kernels
|
|
# =============================================================================
|
|
|
|
|
|
class TestGetTopKernels:
|
|
"""Tests for the get_top_kernels function (aggregated kernel stats)."""
|
|
|
|
def test_returns_none_for_invalid_input(self) -> None:
|
|
"""Test that function returns None for invalid inputs."""
|
|
from rocprof_compute_tui.utils.tui_utils import get_top_kernels
|
|
|
|
# Empty runs
|
|
assert get_top_kernels({}) is None
|
|
|
|
# No dfs attribute
|
|
mock_workload = MagicMock(spec=[])
|
|
assert get_top_kernels({"path": mock_workload}) is None
|
|
|
|
# Missing dfs[1]
|
|
mock_workload = MagicMock()
|
|
mock_workload.dfs = {}
|
|
assert get_top_kernels({"path": mock_workload}) is None
|
|
|
|
def test_returns_empty_list_when_dataframe_empty(self) -> None:
|
|
"""Test that function returns empty list when dfs[1] is empty."""
|
|
from rocprof_compute_tui.utils.tui_utils import get_top_kernels
|
|
|
|
mock_workload = MagicMock()
|
|
mock_workload.dfs = {
|
|
1: pd.DataFrame(columns=["Kernel_Name", "Percent", "Count"])
|
|
}
|
|
|
|
assert get_top_kernels({"path": mock_workload}) == []
|
|
|
|
def test_returns_sorted_kernel_records(
|
|
self,
|
|
sample_top_kernel_df: pd.DataFrame,
|
|
) -> None:
|
|
"""Test that function returns kernel records sorted by Percent descending."""
|
|
from rocprof_compute_tui.utils.tui_utils import get_top_kernels
|
|
|
|
mock_workload = MagicMock()
|
|
mock_workload.dfs = {1: sample_top_kernel_df}
|
|
|
|
result = get_top_kernels({"path": mock_workload})
|
|
|
|
assert result is not None
|
|
assert len(result) == 3
|
|
# Verify sorted by Percent descending
|
|
pct_values = [record["Percent"] for record in result]
|
|
assert pct_values == sorted(pct_values, reverse=True)
|
|
# Verify all expected columns preserved
|
|
assert all("Kernel_Name" in r and "Percent" in r for r in result)
|
|
|
|
def test_handles_missing_pct_column(self) -> None:
|
|
"""Test that function returns unsorted records when Pct column is missing."""
|
|
from rocprof_compute_tui.utils.tui_utils import get_top_kernels
|
|
|
|
df_no_pct = pd.DataFrame({
|
|
"Kernel_Name": ["kernel_a", "kernel_b"],
|
|
"Count": [10, 5],
|
|
})
|
|
mock_workload = MagicMock()
|
|
mock_workload.dfs = {1: df_no_pct}
|
|
|
|
result = get_top_kernels({"path": mock_workload})
|
|
|
|
assert result is not None
|
|
assert len(result) == 2
|
|
|
|
|
|
# =============================================================================
|
|
# Tests for tui_utils.py - process_panels_to_dataframes
|
|
# =============================================================================
|
|
|
|
|
|
class TestProcessPanelsToDataframes:
|
|
"""Tests for the process_panels_to_dataframes function."""
|
|
|
|
def test_returns_dict_structure(self) -> None:
|
|
"""Test that function returns proper nested dict structure."""
|
|
from rocprof_compute_tui.utils.tui_utils import process_panels_to_dataframes
|
|
|
|
mock_args = MagicMock()
|
|
mock_args.decimal = 2
|
|
mock_args.membw_analysis = False
|
|
|
|
mock_arch_configs = MagicMock()
|
|
mock_arch_configs.panel_configs = {}
|
|
|
|
result = process_panels_to_dataframes(
|
|
mock_args,
|
|
{},
|
|
mock_arch_configs,
|
|
{},
|
|
)
|
|
|
|
assert isinstance(result, dict)
|
|
|
|
def test_skips_hidden_sections(self) -> None:
|
|
"""Test that hidden sections are skipped."""
|
|
import config
|
|
from rocprof_compute_tui.utils.tui_utils import process_panels_to_dataframes
|
|
|
|
mock_args = MagicMock()
|
|
mock_args.decimal = 2
|
|
mock_args.membw_analysis = False
|
|
|
|
# Create panel config with hidden section ID
|
|
hidden_id = list(config.HIDDEN_SECTIONS)[0] if config.HIDDEN_SECTIONS else 0
|
|
mock_arch_configs = MagicMock()
|
|
mock_arch_configs.panel_configs = {
|
|
hidden_id: {
|
|
"title": "Hidden Panel",
|
|
"data source": [],
|
|
}
|
|
}
|
|
|
|
result = process_panels_to_dataframes(
|
|
mock_args,
|
|
{},
|
|
mock_arch_configs,
|
|
{},
|
|
)
|
|
|
|
# Hidden section should not appear in result
|
|
for section_name in result.keys():
|
|
assert "Hidden Panel" not in section_name
|
|
|
|
def test_applies_rounding_logic(self) -> None:
|
|
"""Test that decimal rounding is applied to dataframes."""
|
|
from rocprof_compute_tui.utils.tui_utils import apply_rounding_logic
|
|
|
|
df = pd.DataFrame({
|
|
"Value": [1.23456789, 2.987654321],
|
|
"Percent": [50.123456, 49.876544],
|
|
})
|
|
|
|
result = apply_rounding_logic(df, decimal_precision=2)
|
|
|
|
# Check that values are rounded to 2 decimal places
|
|
assert result["Value"].iloc[0] == pytest.approx(1.23, rel=0.01)
|
|
assert result["Percent"].iloc[0] == pytest.approx(50.12, rel=0.01)
|
|
|
|
|
|
# =============================================================================
|
|
# Tests for collapsibles.py - Widget Creation
|
|
# =============================================================================
|
|
|
|
|
|
class TestCollapsiblesWidgetCreation:
|
|
"""Tests for widget creation in collapsibles.py."""
|
|
|
|
def test_create_table_with_empty_dataframe(self) -> None:
|
|
"""Test that create_table returns Label for empty dataframe."""
|
|
from textual.widgets import Label
|
|
|
|
from rocprof_compute_tui.widgets.collapsibles import create_table
|
|
|
|
df = pd.DataFrame()
|
|
result = create_table(df)
|
|
|
|
assert isinstance(result, Label)
|
|
|
|
def test_create_widget_from_data_with_none(self) -> None:
|
|
"""Test that create_widget_from_data handles None correctly."""
|
|
from textual.widgets import Label
|
|
|
|
from rocprof_compute_tui.widgets.collapsibles import create_widget_from_data
|
|
|
|
result = create_widget_from_data(None)
|
|
|
|
assert isinstance(result, Label)
|
|
|
|
def test_create_widget_with_unknown_style(self) -> None:
|
|
"""Test that unknown tui_style returns Label with error message."""
|
|
from textual.widgets import Label
|
|
|
|
from rocprof_compute_tui.widgets.collapsibles import create_widget_from_data
|
|
|
|
df = pd.DataFrame({"col": [1, 2, 3]})
|
|
result = create_widget_from_data(df, tui_style="unknown_style")
|
|
|
|
assert isinstance(result, Label)
|
|
|
|
|
|
# =============================================================================
|
|
# Tests for Logger in tui_utils.py
|
|
# =============================================================================
|
|
|
|
|
|
class TestTuiLogger:
|
|
"""Tests for the Logger class in tui_utils.py."""
|
|
|
|
def test_logger_initialization(self) -> None:
|
|
"""Test that Logger initializes correctly."""
|
|
from rocprof_compute_tui.utils.tui_utils import Logger
|
|
|
|
logger = Logger()
|
|
|
|
assert logger.output_area is None
|
|
assert logger.logger is not None
|
|
|
|
# Verify logging methods work without errors
|
|
logger.info("Info message", update_ui=False)
|
|
logger.warning("Warning message", update_ui=False)
|
|
logger.error("Error message", update_ui=False)
|
|
logger.success("Success message", update_ui=False)
|
|
|
|
|
|
# =============================================================================
|
|
# Tests for Config Loading
|
|
# =============================================================================
|
|
|
|
|
|
class TestConfigLoading:
|
|
"""Tests for configuration loading in collapsibles.py."""
|
|
|
|
def test_load_config_with_valid_yaml(self, tmp_path: Path) -> None:
|
|
"""Test that load_config correctly parses valid YAML."""
|
|
from rocprof_compute_tui.widgets.collapsibles import load_config
|
|
|
|
yaml_content = """
|
|
sections:
|
|
- title: "Test Section"
|
|
collapsed: true
|
|
subsections: []
|
|
"""
|
|
config_file = tmp_path / "test_config.yaml"
|
|
config_file.write_text(yaml_content)
|
|
|
|
result = load_config(str(config_file))
|
|
|
|
assert "sections" in result
|
|
assert len(result["sections"]) == 1
|
|
assert result["sections"][0]["title"] == "Test Section"
|
|
|
|
def test_load_config_raises_on_invalid_file(self, tmp_path: Path) -> None:
|
|
"""Test that load_config raises error for non-existent file."""
|
|
from rocprof_compute_tui.widgets.collapsibles import load_config
|
|
|
|
with pytest.raises(FileNotFoundError):
|
|
load_config(str(tmp_path / "nonexistent.yaml"))
|
|
|
|
|
|
# =============================================================================
|
|
# Integration Test - Data Structure Validation
|
|
# =============================================================================
|
|
|
|
|
|
class TestDataStructureIntegration:
|
|
"""Validate expected data structures flow correctly between components."""
|
|
|
|
def test_kernel_selection_data_lookup(self) -> None:
|
|
"""Test that kernel selection via Kernel_Name correctly looks up analysis data.
|
|
|
|
This validates the contract between:
|
|
- get_top_kernels() returns list with Kernel_Name keys
|
|
- run_kernel_analysis() returns dict keyed by kernel name
|
|
- kernel_view uses Kernel_Name to look up the correct data
|
|
"""
|
|
# Data structures as produced by the analysis pipeline
|
|
kernel_to_df_dict: dict[str, dict[str, Any]] = {
|
|
"kernel_a": {"section": {"value": 100}},
|
|
"kernel_b": {"section": {"value": 200}},
|
|
}
|
|
top_kernel_list = [
|
|
{"Kernel_Name": "kernel_a", "Percent": 50.0},
|
|
{"Kernel_Name": "kernel_b", "Percent": 30.0},
|
|
]
|
|
|
|
# Verify kernel_view lookup logic works
|
|
for kernel_data in top_kernel_list:
|
|
kernel_name = kernel_data["Kernel_Name"]
|
|
selected_data = kernel_to_df_dict.get(kernel_name)
|
|
assert selected_data is not None, f"Missing data for {kernel_name}"
|