From 697a3d5dfade5b348633b8cbc754c9b0b1c339aa Mon Sep 17 00:00:00 2001 From: Luigi Petrucco Date: Thu, 18 Feb 2021 23:35:22 +0100 Subject: [PATCH] Gui testing take two (#119) * first draft * Added first version of GUI testing - browse tabs * Added first version of GUI testing - browse tabs * Fixing setup.py * arrayqueues dep fix * disabled GUI testing * Testing only python 3.8 * fixed tests * Not testing on windows because of vispy/OpenGL problems with pytest-qt * Update main.yml * Update main.yml * Full exp test running now * comments added * removed unnecessary print, minor refactoring in streaming_save.py while debugging --- .github/workflows/main.yml | 9 +-- .travis.yml | 56 ------------------- noxfile.py | 28 ---------- requirements.txt | 4 +- requirements_dev.txt | 9 +++ sashimi/config.py | 4 +- .../hardware/cameras/hamamatsu/interface.py | 2 +- sashimi/processes/streaming_save.py | 30 ++++++---- sashimi/state.py | 29 +++++++--- setup.py | 13 ++++- tests/conftest.py | 12 ++++ tests/test_gui.py | 42 ++++++++++++++ 12 files changed, 119 insertions(+), 119 deletions(-) delete mode 100644 .travis.yml delete mode 100644 noxfile.py create mode 100644 requirements_dev.txt create mode 100644 tests/conftest.py create mode 100644 tests/test_gui.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 165b0d9d..8b1188d1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,13 +25,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.7, 3.8.6] - exclude: - - os: macos-latest - python-version: 3.7 - - os: windows-latest - python-version: 3.7 + os: [macos-latest] + python-version: [3.8.6] steps: - uses: actions/checkout@v2 - name: Set up Python diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2735674b..00000000 --- a/.travis.yml +++ /dev/null @@ -1,56 +0,0 @@ -if: (type = push AND branch IN (master, dev)) OR (type = pull_request AND NOT branch =~ /no-ci/) -language: python -dist: xenial -matrix: - include: - - python: '3.7' - env: NOXSESSION="tests-3.7" - - python: '3.8' - env: NOXSESSION="tests-3.8" - - python: '3.8' - env: NOXSESSION="lint" -install: -- pip install nox -script: nox --session "$NOXSESSION" - -after_success: -- pip install coveralls -- coveralls - -- pip install Sphinx==3.0.3 -- pip install myst-parser -- pip install . # Seems to be required for the apidocs - -- cd docs -- sphinx-apidoc ../sashimi -o source/ -- make html -- cd .. - -# Assuming you have installed the travis-ci CLI tool, after you -# create the GitHub repo and add it to Travis, run the -# following command to finish PyPI deployment setup: -# $ travis encrypt --add deploy.password -deploy: - - provider: pages - skip_cleanup: true - keep_history: true - github_token: - secure: "lOfXKhZ6HgSZY8HpIz9I54esCYyqRNr34rIUYV76kBZAZS9RuvA4CjaL3yhtCYIZj1uSgATQ6c/MAaGBerppciJPQz3sqKgcnhaUw/b6YlvYX4ZHH3GLwDWvfSbQzSVgRWqFh1x7Up+1lpv3S61G97lyYqJyUZf2rbdadAbx9pPvFDUZYVHBYYx6CenoUFXa+G8ro4Pr+zD3lqnyhJaePZDZHP2sT0+KPzpZw+nHP32THffL4fUUtJn5jholstVH6dHioiFKd/lOJCBMwfnEOCQbESl00hQsMwEsiEeEltc5RcIT3DlE1FfhN240Dr8ccI6dH7CJ1z7gzkR53DXhzCkUkelLeCOqiGg+cX9h31km9XNlQ6muT9XN4pbfXqkmomNJAk/cN8w+NJO0H3AFPD9iglqToBOauCrfi68vfiU7ghU3i94dx1n1VUKpAFDQ0n2pid5YwYhMp0TwbXHR+VkWfm93NNikZKzaNiLJFEcI9EOUMxX5o0H7TutRaYAQPUbOyLtNAnMM/ZbTIPWn2nyIqev0lT/al+EZ672r+TQxUg6R6xwo7KvnFWfmV6mS2H2axjn5EQh6TNULMsuql/iME8UxAJ3fy1ggIracO/iztRa/KDAF0WaKRHHpKgSLZl/fq/yT5FfUlsfboUcz1rtDXQLyiMhA5IFMHE32jkI=" - on: - python: 3.7 - branch: master - local-dir: docs/build/html - committer-from-gh: true - target_branch: gh-pages - - - provider: pypi - skip_cleanup: true - distributions: sdist bdist_wheel - user: __token__ - password: - secure: "WF9apZHztLe8VjSF4t3lz9NmWfVOZl8082HXIm64MDj8QIhpFuQLBw9cMpZVf1nBXyuanJBwy5FTSXQxEYfqZSrzMkp5tb2S2JwygYYC1xfIILCRawJepWShsR8MCnS0YT12kgZ7esqVxjxjXN7oKrnF0DHHyNpsoNV/pY97L8J4a/Fs1Z1lwhUuotCE2EhK2jDNvl2jKofxmsf9oiTH5gtMHa8yfaRZURye3wjxDbtXK88qH1wxjUi1i0UGjnrzbpwP17CTggK0CjKj248oq7cRcPRC6Vj/NOfa1rLjrkIgY3PDVe2k/7wBuu0FM8VAoHIElGd4y0qwDg4lLn0gv0T3LWM7zzoOx+Uu8AXIu67p0ADV/bJzWL0jaTHBq7J7q6a1zMNCq550mr5m/phgIMr8JH9XwzvTFXsBMokxe1L9WDQ0B/pUFb5Kk5b8gAqQ4XBzFbZB/3cwbKSsEL99RKCwFnPg/GTqBDEWPTw7vv/mIrP/aLEHoCgo+RX5e7S809wgvgvtsFyw/3jXzlVtZxp8EMJCEvBU5hqyT1UNhFKvGSgaDu1jihi1CZyFJBE5iJy+r0O/wY+ckEisbcgtvG8w46hWBIR/6/CPuYxNJo6Dlt2I2PpTdMoaMiquKPh/ioV7WvdhezRHk1VTEnooIURrXtXOe+0t6RkwieNgzSk=" - on: - tags: true - python: 3.7 - skip_existing: true - committer-from-gh: true diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index fb48280c..00000000 --- a/noxfile.py +++ /dev/null @@ -1,28 +0,0 @@ -import nox -import os - -python = os.environ.get("TRAVIS_PYTHON_VERSION") - -lint_dependencies = ["black", "flake8"] - - -nox.options.reuse_existing_virtualenvs = True - - -@nox.session(python=python) -def tests(session): - session.install("pytest") - session.install("pytest-cov") - session.install("-r", "requirements.txt") - session.install(".") - session.run("pytest", "--cov=sashimi") - - -@nox.session -def lint(session): - session.install(*lint_dependencies) - - files = ["sashimi", "tests", "noxfile.py", "setup.py"] - - session.run("black", "--check", *files) - session.run("flake8", *files) diff --git a/requirements.txt b/requirements.txt index 3468037f..62fc0ddb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -arrayqueues==1.2.0b0 -flammkuchen==0.9 +arrayqueues==1.3.1 +split_dataset lightparam>=0.4, <0.5 pyqt5>=5.15 pyzmq diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 00000000..478f247a --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,9 @@ +pytest-cov +pytest-qt +pytest +gitpython +coverage +pre-commit +black +flake8 +isort \ No newline at end of file diff --git a/sashimi/config.py b/sashimi/config.py index ba20143f..69a16193 100644 --- a/sashimi/config.py +++ b/sashimi/config.py @@ -56,12 +56,12 @@ }, "camera": { "id": 0, - "name": "hamamatsu", + "name": "mock", "max_sensor_resolution": [2048, 2048], "default_exposure": 60, "default_binning": 1, }, - "light_source": {"name": "cobolt", "port": "COM4", "intensity_units": "mA"}, + "light_source": {"name": "mock", "port": "COM4", "intensity_units": "mock"}, "external_communication": {"name": "stytra", "address": "tcp://O1-589:5555"}, "notifier": "none", "notifier_options": {}, diff --git a/sashimi/hardware/cameras/hamamatsu/interface.py b/sashimi/hardware/cameras/hamamatsu/interface.py index b6db49ac..edcab219 100644 --- a/sashimi/hardware/cameras/hamamatsu/interface.py +++ b/sashimi/hardware/cameras/hamamatsu/interface.py @@ -511,7 +511,7 @@ def get_camera_properties(self): "dcamprop_getname", ) - if properties == {'': 0}: + if properties == {"": 0}: raise ConnectionError("The Hamamatsu camera seems to be off!") return properties diff --git a/sashimi/processes/streaming_save.py b/sashimi/processes/streaming_save.py index 9da69418..6cdb59e3 100644 --- a/sashimi/processes/streaming_save.py +++ b/sashimi/processes/streaming_save.py @@ -12,6 +12,7 @@ from sashimi.config import read_config from sashimi.processes.logging import LoggingProcess from sashimi.events import LoggedEvent, SashimiEvents +from sashimi.utilities import get_last_parameters conf = read_config() @@ -130,7 +131,8 @@ def fill_dataset(self, volume): if self.current_data is None: self.calculate_optimal_size(volume) self.current_data = np.empty( - (self.save_parameters.chunk_size, *volume.shape), dtype=self.dtype, + (self.save_parameters.chunk_size, *volume.shape), + dtype=self.dtype, ) self.current_data[self.i_in_chunk, :, :, :] = volume @@ -165,7 +167,10 @@ def finalize_dataset(self): ) as f: json.dump( { - "shape_full": (self.n_volumes, *self.current_data.shape[1:],), + "shape_full": ( + self.n_volumes, + *self.current_data.shape[1:], + ), "shape_block": ( self.save_parameters.chunk_size, *self.current_data.shape[1:], @@ -201,16 +206,17 @@ def calculate_optimal_size(self, volume): ) def receive_save_parameters(self): - try: - self.save_parameters = self.saving_parameter_queue.get(timeout=0.001) - except Empty: - pass - try: - new_duration = self.duration_queue.get(timeout=0.001) - print(new_duration) + """Receive parameters on the stack shape from the `State` obj and new duration + from either the `EsternalTrigger` or the `State` if triggering is disabled. + """ + # Get parameters: + parameters = get_last_parameters(self.saving_parameter_queue) + if parameters is not None: + self.save_parameters = parameters + + # Get duration and update number of volumes: + new_duration = get_last_parameters(self.duration_queue) + if new_duration is not None: self.n_volumes = int( np.ceil(self.save_parameters.volumerate * new_duration) ) - print(self.n_volumes) - except Empty: - pass diff --git a/sashimi/state.py b/sashimi/state.py index 14f2de90..fd463d6f 100644 --- a/sashimi/state.py +++ b/sashimi/state.py @@ -62,7 +62,7 @@ class TriggerSettings(ParametrizedQt): def __init__(self): super().__init__(self) self.name = "trigger_settings" - self.experiment_duration = Param(2_000, (1, 10_000), unit="s") + self.experiment_duration = Param(5, (1, 50_000), unit="s") self.is_triggered = Param(True, [True, False], gui=False) @@ -71,7 +71,8 @@ def __init__(self): super().__init__() self.name = "general/scanning_state" self.scanning_state = Param( - "Paused", ["Paused", "Calibration", "Planar", "Volume"], + "Paused", + ["Paused", "Calibration", "Planar", "Volume"], ) @@ -183,7 +184,11 @@ def __init__(self): def add_calibration_point(self): self.calibrations_points.append( - (self.z_settings.piezo, self.z_settings.lateral, self.z_settings.frontal,) + ( + self.z_settings.piezo, + self.z_settings.lateral, + self.z_settings.frontal, + ) ) self.calculate_calibration() @@ -219,7 +224,8 @@ def calculate_calibration(self): def get_voxel_size( - scanning_settings: ZRecordingSettings, camera_settings: CameraSettings, + scanning_settings: ZRecordingSettings, + camera_settings: CameraSettings, ): scan_length = ( scanning_settings.piezo_scan_range[1] - scanning_settings.piezo_scan_range[0] @@ -522,7 +528,9 @@ def send_scansave_settings(self): elif self.global_state == GlobalState.PLANAR_PREVIEW: params = convert_single_plane_params( - self.planar_setting, self.single_plane_settings, self.calibration, + self.planar_setting, + self.single_plane_settings, + self.calibration, ) elif self.global_state == GlobalState.VOLUME_PREVIEW: @@ -644,10 +652,13 @@ def get_waveform(self): return None def calculate_pulse_times(self): - return np.arange( - self.volume_setting.n_skip_start, - self.volume_setting.n_planes - self.volume_setting.n_skip_end, - ) / (self.volume_setting.frequency * self.volume_setting.n_planes) + return ( + np.arange( + self.volume_setting.n_skip_start, + self.volume_setting.n_planes - self.volume_setting.n_skip_end, + ) + / (self.volume_setting.frequency * self.volume_setting.n_planes) + ) def set_trigger_mode(self, mode: bool): if mode: diff --git a/setup.py b/setup.py index 8ce7adc4..55c5ab8e 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,12 @@ with open("requirements.txt") as f: requirements = f.read().splitlines() +with open("requirements_dev.txt") as f: + requirements_dev = f.read().splitlines() + +with open("README.md") as f: + long_description = f.read() + setup( name="sashimi", version="0.2.0", @@ -11,16 +17,19 @@ author_email="vilim@neuro.mpg.de", packages=find_packages(), install_requires=requirements, - python_requires=">=3.7", + extras_require=dict(dev=requirements_dev), + python_requires=">=3.8", classifiers=[ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Science/Research", "Natural Language :: English", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], keywords="imaging microscopy lightsheet", description="A user-friendly software for efficient control of digital scanned light sheet microscopes (DSLMs).", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/portugueslab/sashimi", entry_points={ "console_scripts": [ "sashimi=sashimi.main:main", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..885c029e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest +from pathlib import Path +import tempfile +import shutil + + +@pytest.fixture() +def temp_path(): + """Temporary path cleaned after the tests run.""" + path = Path(tempfile.mkdtemp()) + yield path + shutil.rmtree(path) diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 00000000..981536be --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,42 @@ +from sashimi.gui.main_gui import MainWindow +from sashimi.state import State, TriggerSettings +import qdarkstyle +from PyQt5.QtCore import Qt +from split_dataset import SplitDataset + + +class MockEvt: + def accept(self): + pass + + +def test_main(qtbot, temp_path): + st = State() + style = qdarkstyle.load_stylesheet_pyqt5() + main_window = MainWindow(st, style) + main_window.show() + qtbot.wait(300) + + # go to calibration and volumetric mode: + main_window.wid_status.setCurrentIndex(1) + qtbot.wait(300) + main_window.wid_status.setCurrentIndex(3) + + # Manually update new directory (to avoid nasty pop up window for filesystem): + st.save_settings.save_dir = str(temp_path) + main_window.wid_save_options.set_locationbutton() + st.send_scansave_settings() + + # Wait to send and receive parameters: + qtbot.wait(10000) + + qtbot.mouseClick(main_window.toolbar.experiment_toggle_btn, Qt.LeftButton, delay=1) + + # wait end of the experiment: + qtbot.wait(TriggerSettings().experiment_duration + 5000) + + # try opening the result: + SplitDataset(temp_path / "original") + + main_window.closeEvent(MockEvt()) + qtbot.wait(1000)