Skip to content

Commit

Permalink
Add CI, tox config and other test adjustments (#3)
Browse files Browse the repository at this point in the history
* add GHA workflow and tox config

* add fixture by @Czaki

* remove pyside6 pin

* fail-fast: false

* prune matrix a bit more

* use optional-dependencies

* allow dialog via decorator-marker

* try something else

* prune matrix a bit more

* fix link in README

* remove py38

* skip on linux py311 pyside2

* skip on all platforms

* parametrize fixture and waitUntil

* skip module

* increase timeout?

* update workflows

* a bit more?

* run all bindings in the same job

* wait for a known number of mocked plugins

* pre-commit

* add pre-commit step

* pin to x.x.x

* do not use a lambda here

* Update napari_plugin_manager/_tests/test_qt_plugin_dialog.py

Co-authored-by: Grzegorz Bokota <[email protected]>

* Apply suggestions from code review

Co-authored-by: Grzegorz Bokota <[email protected]>

---------

Co-authored-by: Grzegorz Bokota <[email protected]>
  • Loading branch information
jaimergp and Czaki authored May 4, 2023
1 parent 9fe2e15 commit 76e8725
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 64 deletions.
107 changes: 107 additions & 0 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: test and deploy

on:
push:
branches:
- main
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
pull_request:
branches:
- main
workflow_dispatch:

concurrency:
# Concurrency group that uses the workflow name and PR number if available
# or commit SHA as a fallback. If a new build is triggered under that
# concurrency group while a previous build is running it will be canceled.
# Repeated pushes to a PR will cancel all previous builds, while multiple
# merges to main will not cancel.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

jobs:
test:
name: ${{ matrix.platform }}, py${{ matrix.python-version }}, napari ${{ matrix.napari }}
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11"]
napari: ["latest", "repo"]
exclude:
# TODO: Remove when we have a napari release with the plugin manager changes
- napari: "latest"
# TODO: PyQt / PySide wheels missing
- python-version: "3.11"
platform: "windows-latest"

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- uses: tlambert03/setup-qt-libs@v1

# strategy borrowed from vispy for installing opengl libs on windows
- name: Install Windows OpenGL
if: runner.os == 'Windows'
run: |
git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git
powershell gl-ci-helpers/appveyor/install_opengl.ps1
if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools tox tox-gh-actions
- name: Test with tox
uses: aganders3/headless-gui@v1
with:
run: python -m tox -vv
env:
PYVISTA_OFF_SCREEN: True # required for opengl on windows
NAPARI: ${{ matrix.napari }}
FORCE_COLOR: 1
# PySide6 only functional with Python 3.10+
TOX_SKIP_ENV: ".*py39-PySide6.*"

- name: pre-commit
uses: pre-commit/[email protected]

- name: Coverage
uses: codecov/codecov-action@v3

deploy:
# this will run when you have tagged a commit, starting with "v*"
# and requires that you have put your twine API key in your
# github secrets (see readme for details)
needs: [test]
runs-on: ubuntu-latest
if: contains(github.ref, 'tags')
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U setuptools twine build
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
run: |
git tag
python -m build
twine upload dist/*
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![License](https://img.shields.io/pypi/l/napari-plugin-manager.svg?color=green)](https://github.com/napari/napari-plugin-manager/raw/main/LICENSE)
[![PyPI](https://img.shields.io/pypi/v/napari-plugin-manager.svg?color=green)](https://pypi.org/project/napari-plugin-manager)
[![Python Version](https://img.shields.io/pypi/pyversions/napari-plugin-manager.svg?color=green)](https://python.org)
[![tests](https://github.com/napari/napari-plugin-manager/workflows/tests/badge.svg)](https://github.com/napari/napari-plugin-manager/actions)
[![tests](https://github.com/napari/napari-plugin-manager/workflows/test_and_deploy/badge.svg)](https://github.com/napari/napari-plugin-manager/actions)
[![codecov](https://codecov.io/gh/napari/napari-plugin-manager/branch/main/graph/badge.svg)](https://codecov.io/gh/napari/napari-plugin-manager)

A plugin that adds a plugin manager to [napari].
Expand Down
18 changes: 18 additions & 0 deletions napari_plugin_manager/_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest
from qtpy.QtWidgets import QDialog, QInputDialog, QMessageBox


@pytest.fixture(autouse=True)
def _block_message_box(monkeypatch, request):
def raise_on_call(*_, **__):
raise RuntimeError("exec_ call") # pragma: no cover

monkeypatch.setattr(QMessageBox, "exec_", raise_on_call)
monkeypatch.setattr(QMessageBox, "critical", raise_on_call)
monkeypatch.setattr(QMessageBox, "information", raise_on_call)
monkeypatch.setattr(QMessageBox, "question", raise_on_call)
monkeypatch.setattr(QMessageBox, "warning", raise_on_call)
monkeypatch.setattr(QInputDialog, "getText", raise_on_call)
# QDialogs can be allowed via a marker; only raise if not decorated
if "enabledialog" not in request.keywords:
monkeypatch.setattr(QDialog, "exec_", raise_on_call)
119 changes: 56 additions & 63 deletions napari_plugin_manager/_tests/test_qt_plugin_dialog.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import importlib.metadata
import sys
from typing import Generator, Optional, Tuple
from unittest.mock import patch

import napari.plugins
import npe2
import pytest
import qtpy
from napari.plugins._tests.test_npe2 import mock_pm # noqa
from napari.utils.translations import trans

from napari_plugin_manager import qt_plugin_dialog
from napari_plugin_manager.qt_package_installer import InstallerActions

if qtpy.API_NAME == 'PySide2' and sys.version_info[:2] == (3, 11):
pytest.skip(
"Known PySide2 x Python 3.11 incompatibility: "
"TypeError: 'PySide2.QtCore.Qt.Alignment' object cannot be interpreted as an integer",
allow_module_level=True,
)

N_MOCKED_PLUGINS = 2


def _iter_napari_pypi_plugin_info(
conda_forge: bool = True,
Expand All @@ -35,7 +46,7 @@ def _iter_napari_pypi_plugin_info(
"author": "test author",
"license": "UNKNOWN",
}
for i in range(2):
for i in range(N_MOCKED_PLUGINS):
yield npe2.PackageMetadata(name=f"test-name-{i}", **base_data), bool(
i
), {
Expand Down Expand Up @@ -70,8 +81,32 @@ def plugins(qtbot):
return PluginsMock()


@pytest.fixture
def plugin_dialog(qtbot, monkeypatch, mock_pm, plugins, old_plugins): # noqa
class WarnPopupMock:
def __init__(self, text):
self._is_visible = False

def exec_(self):
self._is_visible = True

def move(self, pos):
return False

def isVisible(self):
return self._is_visible

def close(self):
self._is_visible = False


@pytest.fixture(params=[True, False], ids=["constructor", "no-constructor"])
def plugin_dialog(
request,
qtbot,
monkeypatch,
mock_pm, # noqa
plugins,
old_plugins,
):
"""Fixture that provides a plugin dialog for a normal napari install."""

class PluginManagerMock:
Expand Down Expand Up @@ -102,16 +137,6 @@ def disable(self, plugin):
self.plugins[plugin] = False
return

class WarnPopupMock:
def __init__(self, text):
return None

def exec_(self):
return None

def move(self, pos):
return False

def mock_metadata(name):
meta = {
'version': '0.1.0',
Expand Down Expand Up @@ -145,15 +170,12 @@ def set_blocked(self, plugin, blocked):
"iter_napari_plugin_info",
_iter_napari_pypi_plugin_info,
)

monkeypatch.setattr(qt_plugin_dialog, 'WarnPopup', WarnPopupMock)

# This is patching `napari.utils.misc.running_as_constructor_app` function
# to mock a normal napari install.
monkeypatch.setattr(
qt_plugin_dialog,
"running_as_constructor_app",
lambda: False,
qt_plugin_dialog, "running_as_constructor_app", lambda: request.param
)

monkeypatch.setattr(
Expand All @@ -166,42 +188,20 @@ def set_blocked(self, plugin, blocked):

widget = qt_plugin_dialog.QtPluginDialog()
widget.show()
qtbot.wait(300)
qtbot.add_widget(widget)
yield widget
widget.hide()
widget._add_items_timer.stop()
assert not widget._add_items_timer.isActive()
qtbot.waitUntil(widget.isVisible, timeout=300)

def available_list_populated():
return widget.available_list.count() == N_MOCKED_PLUGINS

@pytest.fixture
def plugin_dialog_constructor(qtbot, monkeypatch):
"""
Fixture that provides a plugin dialog for a constructor based install.
"""
monkeypatch.setattr(
qt_plugin_dialog,
"iter_napari_plugin_info",
_iter_napari_pypi_plugin_info,
)

# This is patching `napari.utils.misc.running_as_constructor_app` function
# to mock a constructor based install.
monkeypatch.setattr(
qt_plugin_dialog,
"running_as_constructor_app",
lambda: True,
)
widget = qt_plugin_dialog.QtPluginDialog()
widget.show()
qtbot.wait(300)
qtbot.waitUntil(available_list_populated, timeout=3000)
qtbot.add_widget(widget)
yield widget
widget.hide()
widget._add_items_timer.stop()
assert not widget._add_items_timer.isActive()


def test_filter_not_available_plugins(plugin_dialog_constructor):
def test_filter_not_available_plugins(plugin_dialog):
"""
Check that the plugins listed under available plugins are
enabled and disabled accordingly.
Expand All @@ -212,14 +212,14 @@ def test_filter_not_available_plugins(plugin_dialog_constructor):
The second plugin ("test-name-1") is available on conda-forge and
should be enabled without the tooltip warning.
"""
item = plugin_dialog_constructor.available_list.item(0)
widget = plugin_dialog_constructor.available_list.itemWidget(item)
item = plugin_dialog.available_list.item(0)
widget = plugin_dialog.available_list.itemWidget(item)
if widget:
assert not widget.action_button.isEnabled()
assert widget.warning_tooltip.isVisible()

item = plugin_dialog_constructor.available_list.item(1)
widget = plugin_dialog_constructor.available_list.itemWidget(item)
item = plugin_dialog.available_list.item(1)
widget = plugin_dialog.available_list.itemWidget(item)
assert widget.action_button.isEnabled()
assert not widget.warning_tooltip.isVisible()

Expand Down Expand Up @@ -253,26 +253,19 @@ def test_filter_installed_plugins(plugin_dialog):
assert plugin_dialog.installed_list._count_visible() == 0


def test_visible_widgets(plugin_dialog):
def test_visible_widgets(request, plugin_dialog):
"""
Test that the direct entry button and textbox are visible for
normal napari installs.
Test that the direct entry button and textbox are visible
"""

if "no-constructor" not in request.node.name:
# the plugin_dialog fixture has this id
# skip for 'constructor' variant
pytest.skip()
assert plugin_dialog.direct_entry_edit.isVisible()
assert plugin_dialog.direct_entry_btn.isVisible()


def test_constructor_visible_widgets(plugin_dialog_constructor):
"""
Test that the direct entry button and textbox are hidden for
constructor based napari installs.
"""
assert not plugin_dialog_constructor.direct_entry_edit.isVisible()
assert not plugin_dialog_constructor.direct_entry_btn.isVisible()


def test_version_dropdown(plugin_dialog):
def test_version_dropdown(qtbot, plugin_dialog):
"""
Test that when the source drop down is changed, it displays the other versions properly.
"""
Expand Down
12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ dynamic = [
"version"
]

[project.optional-dependencies]
testing = [
"pytest",
"virtualenv",
"pytest-cov",
"pytest-qt",
]

[project.urls]
homepage = "https://github.com/napari/napari-plugin-manager"

Expand Down Expand Up @@ -159,6 +167,10 @@ filterwarnings = [
"error:::test_.*", # turn warnings in our own tests into errors
]

markers = [
"enabledialog: Allow to use dialog in test"
]

[tool.mypy]
files = "napari_plugin_manager"
ignore_missing_imports = true
Expand Down
Loading

0 comments on commit 76e8725

Please sign in to comment.