Skip to content

Commit

Permalink
Exception handling and integration tests (#84)
Browse files Browse the repository at this point in the history
* Working on improvement Exception handling [skip ci]
* Integration tests for data loading + CI attempt[skip ci]
* Integration tests in CIs [skip ci]
* Adding code coverage in the Ubuntu CI[skip ci]
* Graceful integration tests exit (threads issue) [skip ci]
* Disabling integration tests on Windows, hanging...[skip ci]
* Setting up CI for M1[skip ci]
  • Loading branch information
dbouget authored Oct 4, 2024
1 parent 4971b44 commit 6e34676
Show file tree
Hide file tree
Showing 58 changed files with 1,643 additions and 1,031 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/build_macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ jobs:
pip install matplotlib==3.3.4
pip install --force-reinstall --no-cache-dir pyside6
- name: Integration tests
env:
DISPLAY: ':99.0'
run: |
pip install pytest-qt pytest-cov
pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests
- name: Build software
run: |
pip install pyinstaller==5.13.2
Expand All @@ -72,8 +79,8 @@ jobs:
- name: Make installer
run: |
git clone https://github.com/dbouget/quickpkg.git
quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.2.4-macOS.pkg
cp -r Raidionics-1.2.4-macOS.pkg dist/Raidionics-1.2.4-macOS-x86_64.pkg
quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.0-macOS.pkg
cp -r Raidionics-1.3.0-macOS.pkg dist/Raidionics-1.3.0-macOS-x86_64.pkg
- name: Upload package
uses: actions/upload-artifact@v4
Expand Down
14 changes: 12 additions & 2 deletions .github/workflows/build_macos_arm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ jobs:
mkdir ${{github.workspace}}/ANTs
mv ${{github.workspace}}/downloads/install ${{github.workspace}}/ANTs/
- name: Integration tests
env:
DISPLAY: ':99.0'
run: |
cd ${{github.workspace}}
source tmp/venv/bin/activate
pip3 install pytest-qt pytest-cov
pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests
deactivate
- name: Build software
run: |
cd ${{github.workspace}}
Expand All @@ -67,8 +77,8 @@ jobs:
- name: Make installer
run: |
git clone https://github.com/dbouget/quickpkg.git
quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.2.4-macOS.pkg
cp -r Raidionics-1.2.4-macOS.pkg dist/Raidionics-1.2.4-macOS-arm64.pkg
quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.0-macOS.pkg
cp -r Raidionics-1.3.0-macOS.pkg dist/Raidionics-1.3.0-macOS-arm64.pkg
- name: Upload package
uses: actions/upload-artifact@v4
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/build_ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ jobs:
pip uninstall -y PySide6 PySide6-Addons PySide6-Essentials
pip install --force-reinstall --no-cache-dir pyside6==6.2.4
- name: Integration tests
env:
DISPLAY: ':99.0'
run: |
pip install pytest-qt pytest-cov
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX
pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests --cov-report=xml
- name: Build software
run: |
pip install pyinstaller==5.13.2
Expand Down Expand Up @@ -108,11 +116,17 @@ jobs:
cp -r dist/Raidionics assets/Raidionics_ubuntu/usr/local/bin
dpkg-deb --build --root-owner-group assets/Raidionics_ubuntu
ls -la
cp -r assets/Raidionics_ubuntu.deb dist/Raidionics-1.2.4-ubuntu.deb
cp -r assets/Raidionics_ubuntu.deb dist/Raidionics-1.3.0-ubuntu.deb
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: Package
path: ${{github.workspace}}/dist/Raidionics-*
if-no-files-found: error
if-no-files-found: error

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
9 changes: 8 additions & 1 deletion .github/workflows/build_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ jobs:
pip install matplotlib==3.3.4
pip install --force-reinstall --no-cache-dir pyside6
# - name: Integration tests
# env:
# DISPLAY: ':99.0'
# run: |
# pip install pytest-qt pytest-cov
# pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests

- name: Build software
run: |
pip install pyinstaller==5.13.2
Expand All @@ -62,7 +69,7 @@ jobs:
- name: Make installer
run: |
makensis.exe assets/Raidionics.nsi
cp -r assets/Raidionics-1.2.4-win.exe dist/Raidionics-1.2.4-win.exe
cp -r assets/Raidionics-1.3.0-win.exe dist/Raidionics-1.3.0-win.exe
- name: Upload package
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ quickpkg/
*.zip
*.tar.gz
assets/Raidionics_ubuntu/usr/local/bin
.coverage
4 changes: 2 additions & 2 deletions assets/Raidionics.nsi
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
!define APP_NAME "Raidionics"
!define COMP_NAME "SINTEF"
!define VERSION "1.2.2"
!define VERSION "1.3.0"
!define DESCRIPTION "Application"
!define INSTALLER_NAME "Raidionics-1.2.4-win.exe"
!define INSTALLER_NAME "Raidionics-1.3.0-win.exe"
!define MAIN_APP_EXE "Raidionics.exe"
!define INSTALL_TYPE "SetShellVarContext current"
!define REG_ROOT "HKLM"
Expand Down
2 changes: 1 addition & 1 deletion assets/Raidionics_ubuntu/DEBIAN/control
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Package: Raidionics
Version: 1.2.2
Version: 1.3.0
Architecture: i386
Maintainer: David Bouget <david.bouget@sintef.no>
Description: Raidionics—Reporting and Data System.
Expand Down
2 changes: 1 addition & 1 deletion assets/main.spec
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ if sys.platform == "darwin":
'CFBundleIdentifier': 'Raidionics',
'CFBundleInfoDictionaryVersion': '6.0',
'CFBundleName': 'Raidionics',
'CFBundleVersion': '1.2.4',
'CFBundleVersion': '1.3.0',
'CFBundlePackageType': 'APPL',
'LSBackgroundOnly': 'false',
},
Expand Down
2 changes: 1 addition & 1 deletion assets/main_arm.spec
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ if sys.platform == "darwin":
'CFBundleIdentifier': 'Raidionics',
'CFBundleInfoDictionaryVersion': '6.0',
'CFBundleName': 'Raidionics',
'CFBundleVersion': '1.2.4',
'CFBundleVersion': '1.3.0',
'CFBundlePackageType': 'APPL',
'LSBackgroundOnly': 'false',
},
Expand Down
57 changes: 40 additions & 17 deletions gui/RaidionicsMainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ class RaidionicsMainWindow(QMainWindow):
reload_interface = Signal()
new_patient_clicked = Signal(str) # Internal unique_id of the clicked patient

def __init__(self, application, *args, **kwargs):
def __init__(self, application=None, *args, **kwargs):
super(RaidionicsMainWindow, self).__init__(*args, **kwargs)

self.app = application
self.app.setWindowIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)),
'Images/raidionics-icon.png')))
self.app.setStyle("Fusion") # @TODO: Should we remove Fusion style? Looks strange on macOS
self.app = None
if application is not None:
self.app = application
self.app.setWindowIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)),
'Images/raidionics-icon.png')))
self.app.setStyle("Fusion") # @TODO: Should we remove Fusion style? Looks strange on macOS
self.logs_thread = LogReaderThread()
self.logs_thread.start()
self.__set_interface()
Expand All @@ -70,9 +72,11 @@ def closeEvent(self, event):
and SoftwareConfigResources.getInstance().get_active_patient_uid() \
and SoftwareConfigResources.getInstance().get_active_patient().has_unsaved_changes():
dialog = SavePatientChangesDialog()
code = dialog.exec_()
code = dialog.exec()
if code == 0: # Operation cancelled
event.ignore()
if self.logs_thread.isRunning():
self.logs_thread.stop()
logging.info("Graceful exit.")

def resizeEvent(self, event):
Expand All @@ -86,11 +90,14 @@ def __on_exit_software(self) -> None:
"""
Mirroring of the closeEvent, for when the user press the Quit action in the main menu.
"""
self.logs_thread.stop()
if self.logs_thread.isRunning():
self.logs_thread.stop()

if not SoftwareConfigResources.getInstance().is_patient_list_empty()\
and SoftwareConfigResources.getInstance().get_active_patient() is not None\
and SoftwareConfigResources.getInstance().get_active_patient().has_unsaved_changes():
dialog = SavePatientChangesDialog()
code = dialog.exec_()
code = dialog.exec()
if code == 1: # Operation approved
logging.info("Graceful exit.")
sys.exit()
Expand Down Expand Up @@ -123,6 +130,10 @@ def __set_mainmenu_interface(self):
'Images/download-tray-icon.png')),
'Download test data', self)
self.file_menu.addAction(self.download_example_data_action)
self.clear_scene_action = QAction(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)),
'Images/trash-bin_icon.png')), 'Clear', self)
self.file_menu.addAction(self.clear_scene_action)

self.quit_action = QAction(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)),
'Images/power-icon.png')), 'Quit', self)
self.quit_action.setShortcut("Ctrl+Q")
Expand Down Expand Up @@ -360,12 +371,16 @@ def __set_menubar_connections(self):
self.batch_mode_action.triggered.connect(self.__on_study_batch_clicked)
self.settings_preferences_action.triggered.connect(self.__on_settings_preferences_clicked)
# self.quit_action.triggered.connect(sys.exit)
self.clear_scene_action.triggered.connect(self.on_clear_scene)
self.quit_action.triggered.connect(self.__on_exit_software)
self.download_example_data_action.triggered.connect(self.__on_download_example_data)

def __get_screen_dimensions(self):
screen = self.app.primaryScreen()
self.primary_screen_dimensions = screen.size()
if self.app is None:
self.primary_screen_dimensions = QSize(1200, 700)
else:
screen = self.app.primaryScreen()
self.primary_screen_dimensions = screen.size()
logging.debug("Detected primary screen size [w: {}, h: {}]".format(self.primary_screen_dimensions.width(),
self.primary_screen_dimensions.height()))

Expand Down Expand Up @@ -439,7 +454,7 @@ def __on_study_batch_clicked(self):
def __on_settings_preferences_clicked(self):
patient_space = UserPreferencesStructure.getInstance().display_space
diag = SoftwareSettingsDialog(self)
diag.exec_()
diag.exec()

# Reloading the interface is mainly meant to perform a visual refreshment based on the latest user display choices
# For now: changing the display space for viewing a patient images.
Expand All @@ -464,11 +479,11 @@ def __on_patient_selected(self, patient_uid: str) -> None:

def __on_community_action_triggered(self):
popup = ResearchCommunityDialog(self)
popup.exec_()
popup.exec()

def __on_about_action_triggered(self):
popup = AboutDialog()
popup.exec_()
popup.exec()

def __on_help_action_triggered(self) -> None:
"""
Expand All @@ -484,11 +499,11 @@ def __on_view_logs_triggered(self) -> None:
Opens up a pop-up dialog allowing to read through the log file.
"""
diag = LogsViewerDialog(self)
diag.exec_()
diag.exec()

def __on_shortcuts_action_triggered(self):
popup = KeyboardShortcutsDialog(self)
popup.exec_()
popup.exec()

def __on_save_file_triggered(self):
if SoftwareConfigResources.getInstance().get_active_patient_uid() \
Expand All @@ -499,7 +514,7 @@ def __on_save_file_triggered(self):
SoftwareConfigResources.getInstance().get_active_study().save()

def __on_download_example_data(self):
QDesktopServices.openUrl(QUrl("https://drive.google.com/file/d/1W3klW_F7Rfge9-utczz9qp7uWh-pVPS1/view?usp=sharing"))
QDesktopServices.openUrl(QUrl("https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.zip"))

def on_process_log_message(self, log_msg: str) -> None:
"""
Expand All @@ -510,7 +525,7 @@ def on_process_log_message(self, log_msg: str) -> None:
if True in [x in log_msg for x in cases]:
diag = QErrorMessage(self)
diag.setWindowTitle("Error or warning identified!")
diag.showMessage(log_msg + "\nPlease visit the log file (Settings > Logs)")
diag.showMessage(log_msg + "<br><br>Please visit the log file (Settings > Logs)")
diag.setMinimumSize(QSize(400, 150))
diag.exec()

Expand All @@ -523,3 +538,11 @@ def standardOutputWritten(self, text):
# self.singleuse_mode_widget.standardOutputWritten(text)
elif self.central_stackedwidget.currentIndex() == 2:
self.batch_mode_widget.standardOutputWritten(text)

def on_clear_scene(self):
logging.info("[RaidionicsMainWindow] Interface clean-up. Removing all loaded patients and studies.")
self.batch_study_widget.on_clear_scene()
self.single_patient_widget.on_clear_scene()
SoftwareConfigResources.getInstance().reset() # <= Necessary for the integration tests not to crash...
if len(list(SoftwareConfigResources.getInstance().patients_parameters.keys())) > 0:
raise ValueError("[Software error] Existing patient IDs after clearing the scene!")
25 changes: 12 additions & 13 deletions gui/SinglePatientComponent/CentralAreaExecutionWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self, parent=None):
super(CentralAreaExecutionWidget, self).__init__()
self.parent = parent
self.widget_name = "central_area_execution_widget"
self._tumor_type_diag = TumorTypeSelectionQDialog(self)
self.__set_interface()
self.__set_layout_dimensions()
self.__set_stylesheets()
Expand Down Expand Up @@ -147,33 +148,31 @@ def on_pipeline_execution(self, pipeline_code: str) -> None:
"""
self.model_name = ""
if ("Classification" not in pipeline_code) and ("Brain" not in pipeline_code) and ("postop" not in pipeline_code) and ("Edema" not in pipeline_code) and ("Cavity" not in pipeline_code):
diag = TumorTypeSelectionQDialog(self)
code = diag.exec_()
code = self._tumor_type_diag.exec()
if code == 0: # Operation cancelled
return

if diag.tumor_type == 'Glioblastoma':
if self._tumor_type_diag.tumor_type == 'Glioblastoma':
self.model_name = "MRI_GBM"
elif diag.tumor_type == 'Low-Grade Glioma':
elif self._tumor_type_diag.tumor_type == 'Low-Grade Glioma':
self.model_name = "MRI_LGGlioma"
elif diag.tumor_type == 'Metastasis':
elif self._tumor_type_diag.tumor_type == 'Metastasis':
self.model_name = "MRI_Metastasis"
elif diag.tumor_type == 'Meningioma':
elif self._tumor_type_diag.tumor_type == 'Meningioma':
self.model_name = "MRI_Meningioma"

if UserPreferencesStructure.getInstance().segmentation_tumor_model_type != "Tumor":
self.model_name = self.model_name + '_multiclass'
if diag.tumor_type == 'Low-Grade Glioma':
if self._tumor_type_diag.tumor_type == 'Low-Grade Glioma':
self.model_name = "MRI_GBM_multiclass"
elif "postop" in pipeline_code:
diag = TumorTypeSelectionQDialog(self)
code = diag.exec_()
code = self._tumor_type_diag.exec()
if code == 0: # Operation cancelled
return
if diag.tumor_type == 'Glioblastoma':
if self._tumor_type_diag.tumor_type == 'Glioblastoma':
self.model_name = "MRI_GBM_Postop_FV_4p"
pipeline_code = pipeline_code + '_GBM'
elif diag.tumor_type == 'Low-Grade Glioma':
elif self._tumor_type_diag.tumor_type == 'Low-Grade Glioma':
self.model_name = "MRI_LGGlioma_Postop"
pipeline_code = pipeline_code + '_LGGlioma'
elif "Brain" in pipeline_code:
Expand Down Expand Up @@ -258,7 +257,7 @@ def assertion_input_compatible(self, tumor_type: str) -> bool:
color: rgba(0, 0, 0, 1);
background-color: rgba(255, 255, 255, 1);
}""")
box.exec_()
box.exec()
return False
else:
valid_ids = SoftwareConfigResources.getInstance().get_active_patient().get_all_mri_volumes_for_sequence_type(MRISequenceType.FLAIR)
Expand All @@ -273,7 +272,7 @@ def assertion_input_compatible(self, tumor_type: str) -> bool:
color: rgba(0, 0, 0, 1);
background-color: rgba(255, 255, 255, 1);
}""")
box.exec_()
box.exec()
return False

return True
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def dropEvent(self, event):
dialog.patient_imported.connect(self.patient_imported)

dialog.setup_interface_from_files(entered_eligible_files)
code = dialog.exec_()
code = dialog.exec()

# if code == QDialog.Accepted:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ def adjustSize(self):
self.resizeRequested.emit()

def on_options_clicked(self, point):
self.options_menu.exec_(self.options_pushbutton.mapToGlobal(QPoint(0, 0)))
self.options_menu.exec(self.options_pushbutton.mapToGlobal(QPoint(0, 0)))

def on_advanced_options_clicked(self):
self.adjustSize()
Expand Down Expand Up @@ -521,7 +521,7 @@ def __on_opacity_changed(self, value):
self.opacity_value_changed.emit(self.uid, value)

def __on_color_selector_clicked(self):
code = self.color_dialog.exec_()
code = self.color_dialog.exec()
if code == QColorDialog.Accepted:
color = self.color_dialog.currentColor()
self.color_value_changed.emit(self.uid, color)
Expand Down
Loading

0 comments on commit 6e34676

Please sign in to comment.