"""Implementation of the manager of the Test Runner window.

Exports only TestRunnerWindow: The object managing the window, a singleton. Instatiate it to create the window.

Limitations:
    - When tests are running and extensions are disabled then enabled as part of it any tests owned by the disabled
      extensions are removed and not restored.

Toolbar Interactions - what happens when each control on the toolbar is used:
    Run Selected: Gather the list of all selected tests and run them one at a time until completion (disabled when
                  no tests exist)
    Select All: Add all tests to the selection list (disabled when all tests are currently selected)
    Deselect All: Remove all tests from the selection list (disabled when no tests are currently selected)
    Filter: Text that full tests names must match before being displayed
    Load Tests...: Reload the set of available tests from one of the plug-in options
    Properties: Display the settings in a separate window

Note that all tests displayed must come from an enabled extension. To add tests on currently disabled extensions you
would just enable the extension and the test list will be repopulated from the new set of enabled extensions. Similarly
if you disable an extension its tests will be removed from the list.

Individual Test Interactions - what happens when each per-test control is used:
    Checkbox: Adds or removes the test from the selection list
    Run: Run the one test and modify its icon to indicate test state
    Soak: Run the one test 1,000 times and modify its icon to indicate test state
    Open: Open the source file of the test script for the test
    (Final icon is updated to depict the state of the test - not run, running, successful, failed)

TODO: Fix the text file processing to show potential missing extensions
TODO: Add in editing of the user settings in the window
    # --------------------------------------------------------------------------------------------------------------
    # def _edit_filter_settings(self):
    #     # Emit the UI commands required to edit the filters defined in the user settings
    #     from carb.settings import get_settings
    #     include_tests = get_settings().get("/exts/omni.kit.test/includeTests")
    #     exclude_tests = get_settings().get("/exts/omni.kit.test/excludeTests")
"""
from __future__ import annotations
import asyncio
from contextlib import suppress
from enum import auto, Enum
import fnmatch
from functools import partial
import logging
from pathlib import Path
import sys
import unittest

import carb
import carb.settings
import omni.ext
import omni.kit.test
import omni.kit.commands
import omni.kit.app
import omni.kit.ui
from omni.kit.test import TestRunStatus
from omni.kit.test import TestPopulator
from omni.kit.test import TestPopulateAll
from omni.kit.test import TestPopulateDisabled
from omni.kit.test import DEFAULT_POPULATOR_NAME
from omni.kit.window.properties import TestingPropertiesWindow
import omni.ui as ui


__all__ = ["TestRunnerWindow"]


# Size constants
_BUTTON_HEIGHT = 24


# ==============================================================================================================
# Local logger for dumping debug information about the test runner window operation.
# By default it is off but it can be enabled in the extension setup.
_LOG = logging.getLogger("test_runner_window")


# ==============================================================================================================
class _TestUiEntry:
    """UI Data for a single test in the test runner"""

    def __init__(self, test: unittest.TestCase, file_path: str):
        """Initialize the entry with the given test"""
        self.test: unittest.TestCase = test  # Test this entry manages
        self.checkbox: ui.Checkbox = None  # Selection checkbox for this test
        self.sub_checked: ui.Subscription = None  # Subscription to the checkbox change
        self.label_stack: ui.HStack = None  # Container stack for the test module and name
        self.run_button: ui.Button = None  # Button for running the test
        self.soak_button: ui.Button = None  # Button for running the test 1000 times
        self.open_button: ui.Button = None  # Button for opening the file containing the test
        self.file_path: Path = file_path  # Path to the file containing the test
        self.status: TestRunStatus = TestRunStatus.UNKNOWN  # Current test status
        self.status_label: ui.Label = None  # Icon label indicating the test status

    def destroy(self):
        """Destroy the test entry; mostly to avoid leaking caused by dangling callbacks"""
        with suppress(AttributeError):
            self.sub_checked = None
            self.checkbox = None
        with suppress(AttributeError):
            self.run_button.set_clicked_fn(None)
            self.run_button = None
        with suppress(AttributeError):
            self.soak_button.set_clicked_fn(None)
            self.soak_button = None
        with suppress(AttributeError):
            self.open_button.set_clicked_fn(None)
            self.open_button = None

    def __del__(self):
        """Ensure the destroy is always called - it's safe to call it multiple times"""
        self.destroy()


# ==============================================================================================================
class _Buttons(Enum):
    """Index for all of the buttons created in the toolbar"""
    RUN = auto()  # Run all selected tests
    SELECT_ALL = auto()  # Select every listed test
    DESELECT_ALL = auto()  # Deselect every listed test
    PROPERTIES = auto()  # Open the properties window
    LOAD_MENU = auto()  # Open the menu for loading a test list


# ==============================================================================================================
class _TestUiPopulator:
    """Base class for the objects used to populate the initial list of tests, before filtering."""
    def __init__(self, populator: TestPopulator):
        """Set up the populator with the important information it needs for getting tests from some location
        Args:
            name: Name of the populator, which can be used for a menu
            description: Verbose description of the populator, which can be used for the tooltip of the menu item
            doing_what: Parameter to the descriptive waiting sentence "Rebuilding after {doing_what}..."
            source: Source type this populator implements
        """
        self.populator = populator
        self._cached_tests: list[_TestUiEntry] = []

    # --------------------------------------------------------------------------------------------------------------
    @property
    def name(self) -> str:
        """The name of the populator"""
        return self.populator.name

    @property
    def description(self) -> str:
        """The description of the populator"""
        return self.populator.description

    @property
    def tests(self) -> list[_TestUiEntry]:
        """The list of test UI entries gleaned from the raw test list supplied by the populator implementation"""
        if not self._cached_tests:
            _LOG.info("Translating %d unit tests into a _TestUiEntry list", len(self.populator.tests))
            self._cached_tests = {}
            for test in self.populator.tests:
                try:
                    file_path = sys.modules[test.__module__].__file__
                except KeyError:
                    # if the module is not enabled the test does not belong in the list
                    continue
                entry = _TestUiEntry(test, file_path)
                self._cached_tests[test.id()] = entry
        return self._cached_tests

    # --------------------------------------------------------------------------------------------------------------
    def clear(self):
        """Remove the cache so that it can be rebuilt on demand, usually if the contents might have changed"""
        self._cached_tests = {}

    # --------------------------------------------------------------------------------------------------------------
    def destroy(self):
        """Opportunity to clean up any allocated resources"""
        self._cached_tests = {}
        self.populator.destroy()

    # --------------------------------------------------------------------------------------------------------------
    def get_tests(self, call_when_done: callable):
        """Main method for retrieving the list of tests that the populator provides.
        When the tests are available invoke the callback with the test list.
        call_when_done(_TestUiPopulator, canceled: bool)
        """
        def __create_cache(canceled: bool = False):
            call_when_done(self, canceled)
        if not self._cached_tests:
            self.populator.get_tests(__create_cache)
        else:
            call_when_done(self)


# ==============================================================================================================
class TestRunnerWindow:
    """Managers the window containing the test runner
    Members:
        _buttons: Dictionary of _Buttons:ui.Button for all of the toolbar buttons
        _change_sub: Subscription to the extension list change event
        _count: Temporary variable to count number of iterations during test soak
        _filter_begin_edit_sub: Subscription to the start of editing the filter text field
        _filter_end_edit_sub: Subscription to the end of editing the filter text field
        _filter_hint: Label widget holding the overlay text for the filter text field
        _filter_regex: Expression on which to filter the list of source tests
        _filter: StringField widget holding the filter text
        _is_running_tests: Are the selected tests currently running?
        _load_menu: UI Widget containing the menu used for loading the test list
        _properties_window: The temporary dialog that displays the test running properties set by the user
        _refresh_task: Async task to refresh the test status values as they are completed
        _status_label: Text to show in the label in the toolbar that shows test counts
        _test_frame: UI Widget encompassing the frame containing the list of tests to run
        _test_list_source_rc: RadioCollection containing the test source choices
        _test_populators: Dictionary of name:_TestUiPopulator used to populate the full list of tests in the window
        _tests: Dictionary of (ID, Checkbox) corresponding to all visible tests
        _tests_selected: Number of tests in the dictionary currently selected. This is maintained on the fly to avoid
                         an O(N^2) update problem when monitoring checkbox changes and updating the SelectAll buttons
        _toolbar_frame: UI Widget encompassing the set of tools at the top of the window
        _ui_status_label: UI Widget containing the label displaying test runner status
        _window: UI Widget of the toolbar window
    """

    # Location of the window in the larger UI element path space
    WINDOW_NAME = "Test Runner"
    MENU_PATH = f"Window/{WINDOW_NAME}"

    _POPULATORS = [TestPopulateAll(), TestPopulateDisabled()]
    WINDOW_MANAGER = None

    # --------------------------------------------------------------------------------------------------------------
    # API for adding and removing custom populators of the test list
    @classmethod
    def add_populator(cls, new_populator: TestPopulator):
        """Adds the new populator to the available list, raising ValueError if there already is one with that name"""
        if new_populator in cls._POPULATORS:
            raise ValueError(f"Tried to add the same populator twice '{new_populator.name}'")
        cls._POPULATORS.append(new_populator)
        # Updating the window allows dynamic adding and removal of populator types
        if cls.WINDOW_MANAGER is not None:
            cls.WINDOW_MANAGER._test_populators[new_populator.name] = _TestUiPopulator(new_populator)  # noqa: PLW0212
            cls.WINDOW_MANAGER._toolbar_frame.rebuild()  # noqa: PLW0212

    @classmethod
    def remove_populator(cls, populator_to_remove: str):
        """Removes the populator with the given name, raising KeyError if it does not exist"""
        to_remove = None
        for populator in cls._POPULATORS:
            if populator.name == populator_to_remove:
                to_remove = populator
                break
        if to_remove is None:
            raise KeyError(f"Trying to remove populator named {populator_to_remove} before adding it")
        # Updating the window allows dynamic adding and removal of populator types
        if cls.WINDOW_MANAGER is not None:
            del cls.WINDOW_MANAGER._test_populators[populator_to_remove]  # pylint: disable=protected-access
            cls.WINDOW_MANAGER._toolbar_frame.rebuild()  # pylint: disable=protected-access
        cls._POPULATORS.remove(to_remove)

    def __init__(self, start_open: bool):
        """Set up the window and open it if the setting to always open it is enabled"""
        TestRunnerWindow.WINDOW_MANAGER = self
        self._buttons: dict[_Buttons, ui.Button] = {}
        self._change_sub: carb.Subscription = None
        self._count: int = 0
        self._filter_begin_edit_sub: carb.Subscription = None
        self._filter_end_edit_sub: carb.Subscription = None
        self._filter_hint: ui.Label = None
        self._filter_regex: str = ""
        self._filter: ui.StringField = None
        self._is_running_tests: bool = False
        self._load_menu: ui.Menu = None
        self._test_file_path: Path = None
        self._properties_window: TestingPropertiesWindow = None
        self._refresh_task: asyncio.Task = None
        self._test_frame: ui.ScrollingFrame = None
        self._status_label: str = "Checking for tests..."
        self._test_list_source_rc: ui.RadioCollection = None
        self._tests: dict[str, ui.CheckBox] = {}
        self._tests_selected: int = 0
        self._test_populators = {
            populator.name: _TestUiPopulator(populator) for populator in self._POPULATORS
        }
        self._test_populator: TestPopulator = None
        self._toolbar_frame: ui.Frame = None
        self._ui_status_label: ui.Label = None

        _LOG.info("Initializing the main window")
        manager = omni.kit.app.get_app().get_extension_manager()
        self._change_sub = manager.get_change_event_stream().create_subscription_to_pop(
            self.on_extensions_changed, name="test_runner extensions change event"
        )

        self._window = ui.Window(
            self.WINDOW_NAME,
            menu_path=self.MENU_PATH,
            width=1200,
            height=800,
            dockPreference=ui.DockPreference.RIGHT_TOP,
            visibility_changed_fn=self._visibility_changed,
            width_changed_fn=self._width_changed,
        )

        with self._window.frame:
            with ui.VStack():
                self._toolbar_frame = ui.Frame(height=_BUTTON_HEIGHT + 4)
                self._toolbar_frame.set_build_fn(self._build_toolbar_frame)
                with self._toolbar_frame:
                    self._build_toolbar_frame()
                self._test_frame = ui.ScrollingFrame(
                    vertical_scrollbar_policy=ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON,
                    horizontal_scrollbar_policy=ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED,
                )
                self._test_frame.set_build_fn(self._build_test_frame)

        # Populate the initial test list
        self._populate_test_entries(self._test_populators[DEFAULT_POPULATOR_NAME])

        self._window.visible = start_open

    # --------------------------------------------------------------------------------------------------------------
    def destroy(self):
        """Detach all subscriptions and destroy the window elements to avoid dangling callbacks and UI elements"""
        _LOG.info("Destroying the main window {")
        self.WINDOW_MANAGER = None
        if self._window is not None:
            self._window.visible = False
        # Remove the callbacks first
        for _button_id, button in self._buttons.items():
            button.set_clicked_fn(None)
        self._buttons = {}

        for populator in self._test_populators.values():
            populator.destroy()
        self._test_populators = None

        # Remove the widget elements before removing the window itself
        self._clean_refresh_task()
        self._change_sub = None
        self._count = 0
        self._filter = None
        self._filter_begin_edit_sub = None
        self._filter_end_edit_sub = None
        self._filter_hint = None
        self._filter_regex = ""
        self._load_menu = None
        self._ui_status_label = None
        self._status_label = None
        self._test_list_source_rc = None
        self._test_populator = None
        self._tests = {}
        self._tests_selected = 0

        if self._properties_window is not None:
            self._properties_window.destroy()
            del self._properties_window
            self._properties_window = None

        if self._toolbar_frame is not None:
            self._toolbar_frame.set_build_fn(None)
            del self._toolbar_frame
            self._toolbar_frame = None
        if self._test_frame is not None:
            self._test_frame.set_build_fn(None)
            del self._test_frame
            self._test_frame = None
        if self._window is not None:
            self._window.set_visibility_changed_fn(None)
            self._window.set_width_changed_fn(None)
            self._window.frame.set_build_fn(None)
            del self._window
            self._window = None

    # --------------------------------------------------------------------------------------------------------------
    @property
    def visible(self) -> bool:
        return self._window.visible if self._window is not None else False

    @visible.setter
    def visible(self, visible: bool):
        if self._window is not None:
            self._window.visible = visible
        elif visible:
            raise ValueError("Tried to change visibility after window was destroyed")

    def _visibility_changed(self, visible: bool):
        """Update the menu with the visibility state"""
        _LOG.info("Window visibility changed to %s", visible)
        editor_menu = omni.kit.ui.EditorMenu()
        editor_menu.set_value(self.MENU_PATH, visible)

    # --------------------------------------------------------------------------------------------------------------
    def _width_changed(self, new_width: float):
        """Update the sizing of the test frame when the window size changes"""
        _LOG.info("Window width changed to %f", new_width)
        if self._test_frame is not None:
            self._resize_test_frame(new_width)

    # --------------------------------------------------------------------------------------------------------------
    def on_extensions_changed(self, *_):
        """Callback executed when the known extensions may have changed."""
        # Protect calls to _LOG since if it's this extension being disabled it may not exist
        if self._is_running_tests:
            if _LOG is not None:
                _LOG.info("Extension changes ignored while tests are running as it could be part of the tests")
        else:
            if _LOG is not None:
                _LOG.info("Extensions were changed, tests are rebuilding")
            for populator in self._test_populators.values():
                populator.clear()
            self._refresh_after_tests_complete(load_tests=True, cause="extensions changed")

    # --------------------------------------------------------------------------------------------------------------
    def _refresh_after_tests_complete(self, load_tests: bool, cause: str, new_populator: TestPopulator = None):
        """Refresh the window information after the tests finish running.
        If load_tests is True then reconstruct all of the tests from the current test source selected.
        If cause is not empty then place a temporary message in the test pane to indicate a rebuild is happening.
        If a new_populator is specified then it will replace the existing one if its population step succeeds,
        otherwise the original will remain.
        """
        async def _delayed_refresh():
            try:
                # Busy wait until the tests are done running
                slept = 0.0
                while self._is_running_tests:
                    _LOG.debug("....sleep(%f)", slept)
                    slept += 0.2
                    await asyncio.sleep(0.2)

                # Refresh the test information
                _LOG.info("Delayed refresh triggered with load_tests=%s", load_tests)
                if cause:
                    self._ui_status_label.text = f"Rebuilding after {cause}..."
                    await omni.kit.app.get_app().next_update_async()
                if load_tests:
                    _LOG.info("...repopulating the test list")
                    self._populate_test_entries(new_populator)
                else:
                    _LOG.info("...rebuilding the test frame")
                    with self._test_frame:
                        self._build_test_frame()
            except asyncio.CancelledError:
                pass
            except Exception as error:  # pylint: disable=broad-except
                carb.log_warn(f"Failed to refresh content of window.tests. Error: '{error}' {type(error)}.")

        self._clean_refresh_task()
        self._refresh_task = asyncio.ensure_future(_delayed_refresh())

    # --------------------------------------------------------------------------------------------------------------
    def _update_toolbar_status(self):
        """Update the state of the elements in the toolbar, avoiding a full rebuild when values change"""
        filtered_tests = self._filtered_tests()
        _LOG.info("Refreshing the toolbar status for %d tests", len(filtered_tests))
        any_on = self._tests_selected > 0
        any_off = self._tests_selected < len(filtered_tests)
        _LOG.info("  RUN=%s, SELECT_ALL=%s, DESELECT_ALL=%s", any_on, any_off, any_on)
        self._buttons[_Buttons.RUN].enabled = any_on
        self._buttons[_Buttons.SELECT_ALL].enabled = any_off
        self._buttons[_Buttons.DESELECT_ALL].enabled = any_on

        _LOG.info("Reset status to '%s'", self._status_label)
        self._ui_status_label.text = self._status_label

    # --------------------------------------------------------------------------------------------------------------
    def _build_toolbar_frame(self):
        """Emit the UI commands required to build the toolbar that appears at the top of the window"""
        _LOG.info("Rebuilding the toolbar frame")

        # Defining the toolbar callbacks at the top of this method because they are so small
        def on_run(*_):
            _LOG.info("Hit the Run button")
            tests = [entry.test for entry in self._tests.values() if entry.checkbox.model.as_bool]
            self._run_tests(tests)

        def on_select_all(*_):
            _LOG.info("Hit the Select All button")
            filtered_tests = self._filtered_tests()
            for entry in filtered_tests.values():
                entry.checkbox.model.set_value(True)
            self._tests_selected = len(filtered_tests)
            self._update_toolbar_status()

        def on_deselect_all(*_):
            _LOG.info("Hit the Deselect All button")
            filtered_tests = self._filtered_tests()
            for entry in filtered_tests.values():
                entry.checkbox.model.set_value(False)
            self._tests_selected = 0
            self._update_toolbar_status()

        def _on_filter_begin_edit(model: ui.AbstractValueModel):
            self._filter_hint.visible = False

        def _on_filter_end_edit(model: ui.AbstractValueModel):
            if len(model.get_value_as_string()) == 0:
                self._filter_hint.visible = True
                self._filter_regex = ""
            else:
                self._filter_regex = f"*{model.get_value_as_string()}*"
            _LOG.info("Reset filter to '%s'", self._filter_regex)
            self._refresh_after_tests_complete(load_tests=False, cause="filter changed")

        def on_properties(*_):
            _LOG.info("Hit the Properties button")
            if self._properties_window is None:
                self._properties_window = TestingPropertiesWindow()
            self._properties_window.show()

        # + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

        with ui.HStack(height=_BUTTON_HEIGHT + 4, style={"Button.Label:disabled": {"color": 0xFF606060}}):
            self._buttons[_Buttons.RUN] = ui.Button(
                "Run Selected", clicked_fn=on_run, width=ui.Percent(10), style={"border_radius": 5.0})

            self._buttons[_Buttons.SELECT_ALL] = ui.Button(
                "Select All", clicked_fn=on_select_all, width=ui.Percent(10), style={"border_radius": 5.0})

            self._buttons[_Buttons.DESELECT_ALL] = ui.Button(
                "Deselect All", clicked_fn=on_deselect_all, width=ui.Percent(10), style={"border_radius": 5.0})

            with ui.ZStack(width=ui.Percent(20)):
                ui.Spacer(width=ui.Pixel(10))
                # Trick required to give the string field a greyed out "hint" as to what should be typed in it
                self._filter = ui.StringField(height=_BUTTON_HEIGHT)
                self._filter_hint = ui.Label(
                    " Filter (*, ?)", alignment=ui.Alignment.LEFT_CENTER, style={"color": 0xFF3F3F3F}
                )
                if self._filter_regex:
                    self._filter.model.set_value(self._filter_regex[1:-1])
                    self._filter_hint.visible = False
                self._filter_begin_edit_sub = self._filter.model.subscribe_begin_edit_fn(_on_filter_begin_edit)
                self._filter_end_edit_sub = self._filter.model.subscribe_value_changed_fn(_on_filter_end_edit)

            with ui.HStack(height=0, width=ui.Percent(20)):
                ui.Spacer(width=ui.Pixel(3))

                def __on_flag_set(populator):
                    _LOG.info("Invoking load menu with %s", populator)
                    self._load_menu = None
                    self._refresh_after_tests_complete(
                        load_tests=True,
                        cause=f"selecting populator '{populator.name}'",
                        new_populator=populator
                    )

                def __show_load_menu(mouse_x: int, mouse_y: int, mouse_button: int, modifier: int):
                    _LOG.info("Invoked load menu at %d,%d - B%d/%d", mouse_x, mouse_y, mouse_button, modifier)
                    widget = self._buttons[_Buttons.LOAD_MENU]
                    self._load_menu = ui.Menu()
                    with self._load_menu:
                        for populator_name in sorted(self._test_populators.keys()):
                            populator = self._test_populators[populator_name]
                            ui.MenuItem(
                                populator.name,
                                triggered_fn=partial(__on_flag_set, populator),
                                checkable=True,
                                checked=(
                                    self._test_populator is not None and (populator.name == self._test_populator.name)
                                ),
                            )

                    self._load_menu.show_at(
                        (int)(widget.screen_position_x),
                        (int)(widget.screen_position_y + widget.computed_content_height)
                    )

                self._buttons[_Buttons.LOAD_MENU] = ui.Button(
                    "Load Tests From...",
                    height=_BUTTON_HEIGHT + 4,
                    width=0,
                    mouse_released_fn=__show_load_menu,
                    style={"border_radius": 5.0},
                )

            with ui.HStack(width=ui.Percent(10)):
                self._buttons[_Buttons.PROPERTIES] = ui.Button(
                    "Properties",
                    clicked_fn=on_properties,
                    style={"border_radius": 5.0},
                )
                ui.Spacer(width=ui.Pixel(10))

            with ui.HStack(height=_BUTTON_HEIGHT + 4, width=ui.Percent(15)):
                self._ui_status_label = ui.Label("Initializing tests...")

    # --------------------------------------------------------------------------------------------------------------
    def _space_for_test_labels(self, full_width: float) -> float:
        """Returns the number of pixels available for the test labels in the test frame, for manual resizing"""
        label_space = full_width - 20 - 45 - 50 - 50 - 30 - 5 * 3
        # Arbitrary "minimum readable" limit
        if label_space < 200:
            _LOG.info("Label space went below the threshold of 200 to %f, clipping it to 200", label_space)
            label_space = 200
        return label_space

    # --------------------------------------------------------------------------------------------------------------
    def _resize_test_frame(self, new_size: float):
        """Reset the manually computed size for all of the elements in the test frame. Avoids full rebuild."""
        label_space = self._space_for_test_labels(new_size)
        _LOG.info("Resizing test frame with %d pixels of %d for labels", label_space, new_size)
        for _test_id, entry in self._tests.items():
            if entry.label_stack is None:
                continue
            entry.label_stack.width = ui.Pixel(int(label_space))

    # --------------------------------------------------------------------------------------------------------------
    def _filtered_tests(self) -> dict[str, ui.CheckBox]:
        return {
            test_id: entry for test_id, entry in self._tests.items()
            if (not self._filter_regex) or fnmatch.fnmatch(test_id.lower(), self._filter_regex.lower())
        }

    # --------------------------------------------------------------------------------------------------------------
    def _build_test_frame(self):
        """Emit the UI commands required to populate the test frame with the list of visible tests"""
        _LOG.info("Rebuilding the test frame with %d tests", len(self._tests))

        # Compute the space available for the test names by taking the total frame width and subtracting the size
        # used by the checkbox, run button, soak button, open button, and status icon, plus spacing between them
        label_space = self._space_for_test_labels(self._window.frame.computed_width)
        with ui.VStack():
            ui.Spacer(height=ui.Pixel(10))
            filtered_tests = self._filtered_tests()
            for test_id, entry in filtered_tests.items():

                # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                # Test-specific callbacks
                def on_checked(check_model: ui.AbstractValueModel):
                    filtered_tests = self._filtered_tests()
                    if check_model.as_bool:
                        self._tests_selected += 1
                        if self._tests_selected in (1, len(filtered_tests)):
                            self._update_toolbar_status()
                    else:
                        self._tests_selected -= 1
                        if self._tests_selected in (0, len(filtered_tests) - 1):
                            self._update_toolbar_status()

                def on_run_test(*_, test: unittest.TestCase = entry.test):
                    self._run_tests([test])

                def on_soak_test(*_, test: unittest.TestCase = entry.test):
                    self._run_tests([test], repeats=1000)

                def on_open(*_, path: str = entry.file_path):
                    import webbrowser
                    webbrowser.open(path)
                # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

                # Test row
                entry.hstack = ui.HStack(spacing=2, height=0, width=ui.Percent(100))
                with entry.hstack:
                    entry.checkbox = ui.CheckBox(
                        value=False,
                        on_changed_fn=on_checked,
                        style={"margin_height": 2},
                        width=20,
                    )
                    entry.sub_checked = entry.checkbox.model.subscribe_value_changed_fn(on_checked)

                    test_class, test_method = test_id.rsplit(".", 1)

                    entry.label_stack = ui.HStack(width=label_space)
                    with entry.label_stack:
                        entry.label1 = ui.Label(
                            test_class,
                            tooltip=entry.file_path,
                            elided_text=True,
                            alignment=ui.Alignment.LEFT_TOP,
                            width=ui.Fraction(2),
                        )
                        entry.label2 = ui.Label(
                            test_method,
                            tooltip=test_method,
                            elided_text=True,
                            alignment=ui.Alignment.LEFT_TOP,
                            width=ui.Fraction(1),
                        )

                    entry.run_button = ui.Button("Run", clicked_fn=on_run_test, width=45)
                    entry.soak_button = ui.Button("Soak", clicked_fn=on_soak_test, width=50)
                    entry.open_button = ui.Button("Open", clicked_fn=on_open, width=50)
                    entry.status_label = ui.Label("", width=30)

                    entry.status = TestRunStatus.UNKNOWN
                    self._refresh_test_status(test_id)

        tests_available = len(self._tests)
        tests_used = len(filtered_tests)
        if self._test_populator is not None:
            self._status_label = f"Showing {tests_used} of {tests_available} test(s) from {self._test_populator.name}"
        else:
            self._status_label = f"Showing {tests_used} of {tests_available} test(s) with no populator"
        self._update_toolbar_status()

    # --------------------------------------------------------------------------------------------------------------
    def _refresh_test_status(self, test_id: str):
        _LOG.debug("Refresh status of test %s to %s", test_id, self._tests[test_id].status)
        status = self._tests[test_id].status
        status_to_svg = {
            TestRunStatus.UNKNOWN: "${glyphs}/question.svg",
            TestRunStatus.RUNNING: "${glyphs}/spinner.svg",
            TestRunStatus.FAILED: "${glyphs}/exclamation.svg",
            TestRunStatus.PASSED: "${glyphs}/check_solid.svg",
        }
        status_to_color = {
            TestRunStatus.UNKNOWN: 0xFFFFFFFF,
            TestRunStatus.RUNNING: 0xFFFF7D7D,
            TestRunStatus.PASSED: 0xFF00FF00,
            TestRunStatus.FAILED: 0xFF0000FF,
        }
        code = ui.get_custom_glyph_code(status_to_svg.get(status, ""))
        label = self._tests[test_id].status_label
        label.text = f"{code}"
        label.set_style({"color": status_to_color[status]})

    # --------------------------------------------------------------------------------------------------------------
    def _set_test_status(self, test_id: str, status: TestRunStatus):
        _LOG.info("Setting status of test '%s' to %s", test_id, status)
        try:
            self._tests[test_id].status = status
            self._refresh_test_status(test_id)
        except KeyError:
            _LOG.warning("...could not find test %s in the list", test_id)

    # --------------------------------------------------------------------------------------------------------------
    def _set_is_running(self, running: bool):
        _LOG.info("Change running state to %s", running)
        self._is_running_tests = running
        self._buttons[_Buttons.RUN].enabled = not running
        if running:
            self._ui_status_label.text = "Running Tests..."
        else:
            self._ui_status_label.text = ""

    # -------------------------------------------------------------------------------------------------------------
    def _populate_test_entries(self, new_populator: TestPopulator = None):
        """Repopulate the test entry information based on the current filtered source test list.
        If a new_populator is specified then set the current one to it if the population succeeded, otherwise retain
        the original.
        """
        async def __populate():
            _LOG.info("Retrieving the tests from the populator")

            def __repopulate(populator: _TestUiPopulator, canceled: bool = False):
                if canceled:
                    _LOG.info("...test retrieval was canceled")
                    self._test_frame.enabled = True
                else:
                    self._tests_selected = 0
                    self._tests = populator.tests
                    if new_populator is not None:
                        self._test_populator = new_populator
                    _LOG.info("...triggering the test frame rebuild after repopulation")
                    self._test_frame.enabled = True
                    self._test_frame.rebuild()
            self._test_frame.enabled = False
            if new_populator is not None:
                new_populator.get_tests(__repopulate)
            elif self._test_populator is not None:
                self._test_populator.get_tests(__repopulate)

        asyncio.ensure_future(__populate())

    # --------------------------------------------------------------------------------------------------------------
    def _run_tests(self, tests, repeats=0, ignore_running=False):
        """Find all of the selected tests and execute them all asynchronously"""
        _LOG.info("Running the tests with a repeat of %d", repeats)
        if self._is_running_tests and not ignore_running:
            _LOG.info("...skipping, already running the tests")
            return

        def on_finish(runner):
            if self._count > 0:
                self._count -= 1
                print(f"\n\n\n\n{'-'*40} Iteration {self._count} {'-'*40}\n\n")
                self._run_tests(tests, self._count, ignore_running=True)
                return
            self._set_is_running(False)

        def on_status_report(test_id, status, **_):
            self._set_test_status(test_id, status)

        self._count = repeats
        self._set_is_running(True)
        for t in tests:
            self._set_test_status(t.id(), TestRunStatus.UNKNOWN)
        omni.kit.test.run_tests(tests, on_finish, on_status_report)

    # --------------------------------------------------------------------------------------------------------------
    def _clean_refresh_task(self):
        """Clean up the refresh task, canceling it if it is in progress first"""
        with suppress(asyncio.CancelledError, AttributeError):
            self._refresh_task.cancel()
        self._refresh_task = None
