Skip to content

ENH: indicate when a PyDMShellCommand's cmd is currently running by changing the button's appearance and disabling the button. #1249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions examples/shell_command/shell_command.ui
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,74 @@
</property>
</widget>
</item>
<item>
<widget class="PyDMShellCommand" name="PyDMShellCommand_4">
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>Run Long-running Command</string>
</property>
<property name="alarmSensitiveContent" stdset="0">
<bool>false</bool>
</property>
<property name="alarmSensitiveBorder" stdset="0">
<bool>true</bool>
</property>
<property name="PyDMToolTip" stdset="0">
<string/>
</property>
<property name="channel" stdset="0">
<string/>
</property>
<property name="PyDMIcon" stdset="0">
<string/>
</property>
<property name="showConfirmDialog" stdset="0">
<bool>false</bool>
</property>
<property name="runCommandsInFullShell" stdset="0">
<bool>true</bool>
</property>
<property name="confirmMessage" stdset="0">
<string>Are you sure you want to proceed?</string>
</property>
<property name="environmentVariables" stdset="0">
<string/>
</property>
<property name="showIcon" stdset="0">
<bool>true</bool>
</property>
<property name="stdout" stdset="0">
<enum>PyDMShellCommand::SHOW</enum>
</property>
<property name="stderr" stdset="0">
<enum>PyDMShellCommand::SHOW</enum>
</property>
<property name="allowMultipleExecutions" stdset="0">
<bool>false</bool>
</property>
<property name="titles" stdset="0">
<stringlist>
<string></string>
</stringlist>
</property>
<property name="commands" stdset="0">
<stringlist>
<string>for i in {1..5}; do echo $i; sleep 1; done</string>
</stringlist>
</property>
<property name="passwordProtected" stdset="0">
<bool>false</bool>
</property>
<property name="password" stdset="0">
<string/>
</property>
<property name="protectedPassword" stdset="0">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
Expand Down Expand Up @@ -106,21 +174,65 @@
<property name="text">
<string>Multiple Shell Command Menu</string>
</property>
<property name="alarmSensitiveContent" stdset="0">
<bool>false</bool>
</property>
<property name="alarmSensitiveBorder" stdset="0">
<bool>true</bool>
</property>
<property name="PyDMToolTip" stdset="0">
<string/>
</property>
<property name="channel" stdset="0">
<string/>
</property>
<property name="PyDMIcon" stdset="0">
<string/>
</property>
<property name="showConfirmDialog" stdset="0">
<bool>false</bool>
</property>
<property name="runCommandsInFullShell" stdset="0">
<bool>true</bool>
</property>
<property name="confirmMessage" stdset="0">
<string>Are you sure you want to proceed?</string>
</property>
<property name="environmentVariables" stdset="0">
<string/>
</property>
<property name="showIcon" stdset="0">
<bool>true</bool>
</property>
<property name="redirectCommandOutput" stdset="0">
<bool>true</bool>
</property>
<property name="allowMultipleExecutions" stdset="0">
<bool>false</bool>
</property>
<property name="titles" stdset="0">
<stringlist>
<string>Print &quot;Hello, World!&quot; to terminal</string>
<string>Print current working directory to terminal</string>
<string>Run Long-running Command</string>
</stringlist>
</property>
<property name="commands" stdset="0">
<stringlist>
<string>echo &quot;Hello, World!&quot;</string>
<string>pwd</string>
<string>for i in {1..5}; do echo $i; sleep 1; done</string>
</stringlist>
</property>
<property name="passwordProtected" stdset="0">
<bool>false</bool>
</property>
<property name="password" stdset="0">
<string/>
</property>
<property name="protectedPassword" stdset="0">
<string/>
</property>
</widget>
</item>
</layout>
Expand Down
24 changes: 12 additions & 12 deletions pydm/tests/widgets/test_datetime_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest
import time

from ...widgets.datetime import PyDMDateTimeEdit, TimeBase
from pydm.widgets.datetime import PyDMDateTimeEdit, TimeBase
from qtpy.QtCore import QDateTime


Expand Down Expand Up @@ -53,15 +53,15 @@ def test_construct(qtbot, init_channel):
"value, expected_value",
[
# Assuming the time is localized to EST
(0, "1969/12/31 19:00:00.000"),
(1000, "1969/12/31 19:00:01.000"),
(60000, "1969/12/31 19:01:00.000"),
(3600000, "1969/12/31 20:00:00.000"),
(18000000, "1970/01/01 00:00:00.000"),
(0.0, "1969/12/31 19:00:00.000"),
(1000.0, "1969/12/31 19:00:01.000"),
(60000.0, "1969/12/31 19:01:00.000"),
(3600000.0, "1969/12/31 20:00:00.000"),
(0, "1969/12/31 19:00:00.000"),
(1000, "1969/12/31 19:00:01.000"),
(60000, "1969/12/31 19:01:00.000"),
(3600000, "1969/12/31 20:00:00.000"),
(18000000, "1970/01/01 00:00:00.000"),
(0.0, "1969/12/31 19:00:00.000"),
(1000.0, "1969/12/31 19:00:01.000"),
(60000.0, "1969/12/31 19:01:00.000"),
(3600000.0, "1969/12/31 20:00:00.000"),
(18000000.0, "1970/01/01 00:00:00.000"),
],
)
Expand All @@ -86,10 +86,10 @@ def test_value_changed(qtbot, signals, value, expected_value):
The expected displayed value of the widget
"""
# These tests will fail on Windows, we can't easily modify time
if platform.system() == 'Windows':
if platform.system() == "Windows":
return

os.environ['TZ'] = 'US/Eastern'
os.environ["TZ"] = "US/Eastern"
time.tzset()

pydm_datetimeedit = PyDMDateTimeEdit()
Expand Down
28 changes: 14 additions & 14 deletions pydm/tests/widgets/test_datetime_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
import time

from ...widgets.datetime import PyDMDateTimeLabel, TimeBase
from pydm.widgets.datetime import PyDMDateTimeLabel, TimeBase

# --------------------
# POSITIVE TEST CASES
Expand Down Expand Up @@ -40,17 +40,17 @@ def test_construct(qtbot):
"value, text_format, expected_value",
[
# Assuming the time is localized to EST
(0, "yyyy-MM-dd", "1969-12-31"),
(0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-00-000"),
(1000, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-01-000"),
(60000, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-01-00-000"),
(3600000, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-20-00-00-000"),
(18000000, "yyyy-MM-dd-hh-mm-ss-zzz", "1970-01-01-00-00-00-000"),
(0.0, "yyyy-MM-dd", "1969-12-31"),
(0.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-00-000"),
(1000.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-01-000"),
(60000.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-01-00-000"),
(3600000.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-20-00-00-000"),
(0, "yyyy-MM-dd", "1969-12-31"),
(0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-00-000"),
(1000, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-01-000"),
(60000, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-01-00-000"),
(3600000, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-20-00-00-000"),
(18000000, "yyyy-MM-dd-hh-mm-ss-zzz", "1970-01-01-00-00-00-000"),
(0.0, "yyyy-MM-dd", "1969-12-31"),
(0.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-00-000"),
(1000.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-00-01-000"),
(60000.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-19-01-00-000"),
(3600000.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1969-12-31-20-00-00-000"),
(18000000.0, "yyyy-MM-dd-hh-mm-ss-zzz", "1970-01-01-00-00-00-000"),
],
)
Expand All @@ -77,10 +77,10 @@ def test_value_changed(qtbot, signals, value, text_format, expected_value):
The expected displayed value of the widget
"""
# These tests will fail on Windows, we can't easily modify time
if platform.system() == 'Windows':
if platform.system() == "Windows":
return

os.environ['TZ'] = 'US/Eastern'
os.environ["TZ"] = "US/Eastern"
time.tzset()

pydm_label = PyDMDateTimeLabel()
Expand Down
132 changes: 132 additions & 0 deletions pydm/tests/widgets/test_shell_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import os
import pytest
import time

import platform
from logging import ERROR
Expand Down Expand Up @@ -218,6 +219,137 @@ def check_command_output(command, expected_retcode, expected_stdout):
assert pydm_shell_command.process is None


def test_long_running_command_shows_currently_running_text(qtbot):
"""
Test that the button is updated to indicate when a command is currently running.
These indications are:
- Prepend "(Running...) " to the button's text
- Make button text italic
- Disable button while cmd is running (unless allowMultipleExecutions is set True)
- Set button icon to hourglass symbol
And the button should be reset back to it's original state after the command is done.
"""
pydm_shell_command = PyDMShellCommand()
pydm_shell_command.stdout = TermOutputMode.HIDE
qtbot.addWidget(pydm_shell_command)

# Long running cmd, which we will kill after checking button state while its running
pydm_shell_command.commands = ["for i in {1..4}; do echo $i; sleep 0.25; done"]

original_text = "Run Long Cmd"
pydm_shell_command.setText(original_text)
pydm_shell_command.runCommandsInFullShell = True

# Execute the cmd
qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)

# Check icon is updated while cmd is running
qtbot.wait_until(lambda: pydm_shell_command.text().startswith("(Running...)"))
assert pydm_shell_command.text() == f"(Running...) {original_text}"
icon_size = QSize(16, 16)
default_icon = IconFont().icon("hourglass-start")
default_icon_pixmap = default_icon.pixmap(icon_size)
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()

assert pydm_shell_command.font().italic()

assert not pydm_shell_command.isEnabled()

# Temp disable this part of test
"""
# This 2nd 'wait_until' in the 'long_running_command' testcases causes error only when running all tests together:
# 'RuntimeError: wrapped C/C++ object of type PyDMShellCommand has been deleted'.
# To prevent this, have tried giving shell_command a parent widget, using 'qtbot.wait(n)' instead,
# calling shell_command.show(), etc, but nothing seems to prevent the C++ object deletion.

# Check icon is reverted back after cmd stops running
qtbot.wait_until(lambda: pydm_shell_command.text() == original_text)
default_icon = IconFont().icon("cog")
default_icon_pixmap = default_icon.pixmap(icon_size)
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()

assert not pydm_shell_command.font().italic()

assert pydm_shell_command.isEnabled()
"""


def test_long_running_command_shows_currently_running_text_dropdown(qtbot):
"""
Test that A button with multiple-cmds (so it has a drop-down menu) is updated to indicate
a drop-down menu command is currently running.
These indications are:
- Prepend "(Submenu cmd running...) " to the button's text
- Prepend "(Running...) " to the curr running drop-down item
- Make button and curr running drop-down text italic
- Disable button and all drop-down menu items while cmd is running (unless allowMultipleExecutions is set True)
- Set both button and curr running drop-down icons to hourglass symbol
And the button should be reset back to it's original state after the command is done.
"""
pydm_shell_command = PyDMShellCommand()
qtbot.addWidget(pydm_shell_command)
pydm_shell_command.stdout = TermOutputMode.SHOW

original_button_text = "Run Long Cmd"
# Text of the specific item in drop-down we want to check the state of
original_action_text = "for i in {1..4}; do echo $i; sleep 0.25; done"
pydm_shell_command.setText(original_button_text)
pydm_shell_command.runCommandsInFullShell = True

pydm_shell_command.commands = ["echo 'command 1'", "echo 'command 2'", original_action_text]

# We need to execute the shell-cmd's mousePressEvent() so it can build the drop-down menu,
# and right-click since left-click seems to cause this test to then display the drop-down menu
# and just pauses waiting for user interaction.
qtbot.mouseClick(pydm_shell_command, QtCore.Qt.RightButton)

actions = pydm_shell_command.menu().actions()
assert len(actions) >= 3

# Activate drop-down menu button cmd
actions[2].trigger()

# Check button icon is changed while cmd is running
qtbot.wait_until(lambda: pydm_shell_command.text().startswith("(Submenu cmd running...)"))
assert pydm_shell_command.text() == f"(Submenu cmd running...) {original_button_text}"

icon_size = QSize(16, 16)
default_icon = IconFont().icon("hourglass-start")
default_icon_pixmap = default_icon.pixmap(icon_size)
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()
assert pydm_shell_command.font().italic()

# Check state of action buttons in drop-down menu
# Check icon is changed on curr running action button while cmd is running
default_icon = IconFont().icon("hourglass-start")
default_icon_pixmap = default_icon.pixmap(icon_size)
curr_icon_pixmap = actions[2].icon().pixmap(icon_size)
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()

assert actions[2].text() == f"(Running...) {original_action_text}"
assert actions[2].font().italic()

assert all(not action.isEnabled() for action in actions)

# Temp disable this part of test (see comment in other 'long_running_command' testcase)
"""
# Check button icon is reverted back after cmd stops running
qtbot.wait_until(lambda: pydm_shell_command.text() == original_button_text)
default_icon = IconFont().icon("cog")
default_icon_pixmap = default_icon.pixmap(icon_size)
curr_icon_pixmap = pydm_shell_command.icon().pixmap(icon_size)
assert curr_icon_pixmap.toImage() == default_icon_pixmap.toImage()
assert not pydm_shell_command.font().italic()

# Check drop-down menu icon is reverted
actions[2].icon().isNull() # Drop-down menu cmds have no icon by default
assert all(action.isEnabled() for action in actions)
"""


@pytest.mark.parametrize(
"allow_multiple",
[
Expand Down
Loading
Loading