Browse Source

CI: Enable testing on macOS and Windows (#707)

pull/725/head
Andreas Motl 2 years ago committed by GitHub
parent
commit
cddd5c4fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .github/workflows/codeql-analysis.yml
  2. 61
      .github/workflows/tests.yml
  3. 52
      apprise/plugins/NotifyMQTT.py
  4. 4
      requirements.txt
  5. 7
      setup.py
  6. 4
      test/test_apprise_config.py
  7. 10
      test/test_attach_file.py
  8. 2
      test/test_config_base.py
  9. 4
      test/test_config_memory.py
  10. 22
      test/test_locale.py
  11. 16
      test/test_logger.py
  12. 5
      test/test_plugin_macosx.py
  13. 8
      test/test_plugin_syslog.py
  14. 6
      test/test_plugin_windows.py

5
.github/workflows/codeql-analysis.yml

@ -8,6 +8,11 @@ on:
schedule: schedule:
- cron: '42 15 * * 5' - cron: '42 15 * * 5'
# Cancel in-progress jobs when pushing to the same branch.
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze

61
.github/workflows/tests.yml

@ -27,33 +27,62 @@ jobs:
# all jobs once the first one fails (true). # all jobs once the first one fails (true).
fail-fast: true fail-fast: true
# Define a minimal test matrix, it will be
# expanded using subsequent `include` items.
matrix: matrix:
os: [ os: ["ubuntu-latest"]
"ubuntu-latest", python-version: ["3.10"]
# "macos-latest",
# "windows-latest",
]
python-version: [
"3.6", "3.7", "3.8", "3.9", "3.10", "3.11-dev",
"pypy3.6", "pypy3.7", "pypy3.8", "pypy3.9",
]
bare: [false] bare: [false]
# Add another single item to the test matrix. It is the `bare` environment,
# where `all-plugin-requirements.txt` will NOT be installed, in order to
# verify the application also works well without those optional dependencies.
include: include:
# Within the `bare` environment, `all-plugin-requirements.txt` will NOT be
# installed, to verify the application also works without those dependencies.
- os: "ubuntu-latest" - os: "ubuntu-latest"
python-version: "3.10" python-version: "3.10"
bare: true bare: true
# Let's save resources and only build a single slot on macOS- and Windows.
- os: "macos-latest"
python-version: "3.10"
- os: "windows-latest"
python-version: "3.10"
# Test more available versions of CPython on Linux.
- os: "ubuntu-latest"
python-version: "3.6"
- os: "ubuntu-latest"
python-version: "3.7"
- os: "ubuntu-latest"
python-version: "3.8"
- os: "ubuntu-latest"
python-version: "3.9"
- os: "ubuntu-latest"
python-version: "3.10"
- os: "ubuntu-latest"
python-version: "3.11"
# Test more available versions of PyPy on Linux.
- os: "ubuntu-latest"
python-version: "pypy3.6"
- os: "ubuntu-latest"
python-version: "pypy3.7"
- os: "ubuntu-latest"
python-version: "pypy3.8"
- os: "ubuntu-latest"
python-version: "pypy3.9"
defaults:
run:
shell: bash
env: env:
OS: ${{ matrix.os }} OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python-version }} PYTHON: ${{ matrix.python-version }}
BARE: ${{ matrix.bare }} BARE: ${{ matrix.bare }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
name: Python ${{ matrix.python-version }} on ${{ matrix.os }} (bare=${{ matrix.bare }}) name: Python ${{ matrix.python-version }} on ${{ matrix.os }} ${{ matrix.bare && '(bare)' || '' }}
steps: steps:
- name: Acquire sources - name: Acquire sources
@ -88,13 +117,13 @@ jobs:
run: | run: |
pip install -r all-plugin-requirements.txt pip install -r all-plugin-requirements.txt
# Installing `dbus-python` will croak on PyPy, so skip it. # Installing `dbus-python` will only work on Linux/CPython.
[[ $PYTHON != 'pypy'* ]] && pip install dbus-python || true [[ $RUNNER_OS = "Linux" && $PYTHON != 'pypy'* ]] && pip install dbus-python || true
- name: Install project dependencies (Windows) - name: Install project dependencies (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
run: | run: |
pip install -r win-requirements.txt [[ $PYTHON != 'pypy'* ]] && pip install -r win-requirements.txt || true
# Install package in editable mode, # Install package in editable mode,
# and run project-specific tasks. # and run project-specific tasks.

52
apprise/plugins/NotifyMQTT.py

@ -132,23 +132,6 @@ class NotifyMQTT(NotifyBase):
# through their network flow at once. # through their network flow at once.
mqtt_inflight_messages = 200 mqtt_inflight_messages = 200
# Taken from https://golang.org/src/crypto/x509/root_linux.go
# TODO: Maybe migrate to a general utility function?
CA_CERTIFICATE_FILE_LOCATIONS = [
# Debian/Ubuntu/Gentoo etc.
"/etc/ssl/certs/ca-certificates.crt",
# Fedora/RHEL 6
"/etc/pki/tls/certs/ca-bundle.crt",
# OpenSUSE
"/etc/ssl/ca-bundle.pem",
# OpenELEC
"/etc/pki/tls/cacert.pem",
# CentOS/RHEL 7
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
# macOS Homebrew; brew install ca-certificates
"/usr/local/etc/ca-certificates/cert.pem",
]
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{user}@{host}/{topic}', '{schema}://{user}@{host}/{topic}',
@ -534,3 +517,38 @@ class NotifyMQTT(NotifyBase):
# return results # return results
return results return results
@property
def CA_CERTIFICATE_FILE_LOCATIONS(self):
"""
Return possible locations to root certificate authority (CA) bundles.
Taken from https://golang.org/src/crypto/x509/root_linux.go
TODO: Maybe refactor to a general utility function?
"""
candidates = [
# Debian/Ubuntu/Gentoo etc.
"/etc/ssl/certs/ca-certificates.crt",
# Fedora/RHEL 6
"/etc/pki/tls/certs/ca-bundle.crt",
# OpenSUSE
"/etc/ssl/ca-bundle.pem",
# OpenELEC
"/etc/pki/tls/cacert.pem",
# CentOS/RHEL 7
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
# macOS Homebrew; brew install ca-certificates
"/usr/local/etc/ca-certificates/cert.pem",
]
# Certifi provides Mozilla’s carefully curated collection of Root
# Certificates for validating the trustworthiness of SSL certificates
# while verifying the identity of TLS hosts. It has been extracted from
# the Requests project.
try:
import certifi
candidates.append(certifi.where())
except ImportError: # pragma: no cover
pass
return candidates

4
requirements.txt

@ -1,3 +1,7 @@
# Root certificate authority bundle.
certifi
# Application dependencies.
requests requests
requests-oauthlib requests-oauthlib
click >= 5.0 click >= 5.0

7
setup.py

@ -27,6 +27,8 @@
import re import re
import os import os
import platform import platform
import sys
from setuptools import find_packages, setup from setuptools import find_packages, setup
cmdclass = {} cmdclass = {}
@ -43,7 +45,8 @@ except ImportError:
install_options = os.environ.get("APPRISE_INSTALL", "").split(",") install_options = os.environ.get("APPRISE_INSTALL", "").split(",")
install_requires = open('requirements.txt').readlines() install_requires = open('requirements.txt').readlines()
if platform.system().lower().startswith('win'): if platform.system().lower().startswith('win') \
and not hasattr(sys, "pypy_version_info"):
# Windows Notification Support # Windows Notification Support
install_requires += open('win-requirements.txt').readlines() install_requires += open('win-requirements.txt').readlines()
@ -60,7 +63,7 @@ setup(
version='1.1.0', version='1.1.0',
description='Push Notifications that work with just about every platform!', description='Push Notifications that work with just about every platform!',
license='MIT', license='MIT',
long_description=open('README.md').read(), long_description=open('README.md', encoding="utf-8").read(),
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
cmdclass=cmdclass, cmdclass=cmdclass,
url='https://github.com/caronc/apprise', url='https://github.com/caronc/apprise',

4
test/test_apprise_config.py

@ -783,8 +783,8 @@ json://localhost:8080
include {}""".format(str(cfg01))) include {}""".format(str(cfg01)))
cfg02.write(""" cfg02.write("""
# syslog entry # json entry
syslog:// json://localhost:8080
# recursively include ourselves # recursively include ourselves
include cfg02.cfg""") include cfg02.cfg""")

10
test/test_attach_file.py

@ -25,6 +25,7 @@
import re import re
import time import time
import urllib
from unittest import mock from unittest import mock
from os.path import dirname from os.path import dirname
@ -98,8 +99,13 @@ def test_attach_file():
# Download is successful and has already been called by now; below pulls # Download is successful and has already been called by now; below pulls
# results from cache # results from cache
assert response.download() assert response.download()
assert response.url().startswith('file://{}'.format(path))
# No mime-type and/or filename over-ride was specified, so therefore it # On Windows, it is `file://D%3A%5Ca%5Capprise%5Capprise%5Ctest%5Cvar%5Capprise-test.gif`. # noqa E501
# TODO: Review - is this correct?
path_in_url = urllib.parse.quote(path)
assert response.url().startswith('file://{}'.format(path_in_url))
# No mime-type and/or filename over-ride was specified, so it
# won't show up in the generated URL # won't show up in the generated URL
assert re.search(r'[?&]mime=', response.url()) is None assert re.search(r'[?&]mime=', response.url()) is None
assert re.search(r'[?&]name=', response.url()) is None assert re.search(r'[?&]name=', response.url()) is None

2
test/test_config_base.py

@ -182,7 +182,7 @@ version: 1
urls: urls:
- pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b - pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b
- mailto://test:password@gmail.com - mailto://test:password@gmail.com
- syslog://: - json://localhost:
- tag: devops, admin - tag: devops, admin
""", asset=AppriseAsset()) """, asset=AppriseAsset())

4
test/test_config_memory.py

@ -39,7 +39,7 @@ def test_config_memory():
assert ConfigMemory.parse_url('garbage://') is None assert ConfigMemory.parse_url('garbage://') is None
# Initialize our object # Initialize our object
cm = ConfigMemory(content="syslog://", format='text') cm = ConfigMemory(content="json://localhost", format='text')
# one entry added # one entry added
assert len(cm) == 1 assert len(cm) == 1
@ -49,7 +49,7 @@ def test_config_memory():
assert isinstance(cm.read(), str) is True assert isinstance(cm.read(), str) is True
# Test situation where an auto-detect is required: # Test situation where an auto-detect is required:
cm = ConfigMemory(content="syslog://") cm = ConfigMemory(content="json://localhost")
# one entry added # one entry added
assert len(cm) == 1 assert len(cm) == 1

22
test/test_locale.py

@ -24,9 +24,11 @@
# THE SOFTWARE. # THE SOFTWARE.
import os import os
import sys
from unittest import mock from unittest import mock
import ctypes import ctypes
import pytest
from apprise import AppriseLocale from apprise import AppriseLocale
from apprise.utils import environ from apprise.utils import environ
@ -133,7 +135,9 @@ def test_detect_language_windows_users():
""" """
if not hasattr(ctypes, 'windll'): if hasattr(ctypes, 'windll'):
from ctypes import windll
else:
windll = mock.Mock() windll = mock.Mock()
# 4105 = en_CA # 4105 = en_CA
windll.kernel32.GetUserDefaultUILanguage.return_value = 4105 windll.kernel32.GetUserDefaultUILanguage.return_value = 4105
@ -152,14 +156,22 @@ def test_detect_language_windows_users():
with environ('LANG', 'LC_ALL', 'LC_CTYPE', LANGUAGE="en_CA"): with environ('LANG', 'LC_ALL', 'LC_CTYPE', LANGUAGE="en_CA"):
assert AppriseLocale.AppriseLocale.detect_language() == 'en' assert AppriseLocale.AppriseLocale.detect_language() == 'en'
@pytest.mark.skipif(sys.platform == "win32", reason="Does not work on Windows")
def test_detect_language_windows_users_croaks_please_review():
"""
When enabling CI testing on Windows, those tests did not produce the
correct results. They may want to be reviewed.
"""
# The below accesses the windows fallback code and fail # The below accesses the windows fallback code and fail
# then it will resort to the environment variables # then it will resort to the environment variables.
with environ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_CTYPE'): with environ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_CTYPE'):
# Language can't be detected # Language can't be detected
assert AppriseLocale.AppriseLocale.detect_language() is None assert AppriseLocale.AppriseLocale.detect_language() is None
# Detect French language.
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', LANG="fr_CA"): with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', LANG="fr_CA"):
# Detect french language
assert AppriseLocale.AppriseLocale.detect_language() == 'fr' assert AppriseLocale.AppriseLocale.detect_language() == 'fr'
# The following unsets all environment variables and sets LC_CTYPE # The following unsets all environment variables and sets LC_CTYPE
@ -174,10 +186,8 @@ def test_detect_language_windows_users():
with environ(*list(os.environ.keys())): with environ(*list(os.environ.keys())):
assert AppriseLocale.AppriseLocale.detect_language() is None assert AppriseLocale.AppriseLocale.detect_language() is None
# Tidy
delattr(ctypes, 'windll')
@pytest.mark.skipif(sys.platform == "win32", reason="Does not work on Windows")
@mock.patch('locale.getdefaultlocale') @mock.patch('locale.getdefaultlocale')
def test_detect_language_defaultlocale(mock_getlocale): def test_detect_language_defaultlocale(mock_getlocale):
""" """

16
test/test_logger.py

@ -25,6 +25,8 @@
import re import re
import os import os
import sys
import pytest import pytest
import requests import requests
from unittest import mock from unittest import mock
@ -281,12 +283,16 @@ def test_apprise_log_file_captures(tmpdir):
assert len(logs) == 5 assert len(logs) == 5
# Remove our file before we exit the with clause # Concurrent file access is not possible on Windows.
# this causes our delete() call to throw gracefully inside # PermissionError: [WinError 32] The process cannot access the file
os.unlink(str(log_file)) # because it is being used by another process.
if sys.platform != "win32":
# Remove our file before we exit the with clause
# this causes our delete() call to throw gracefully inside
os.unlink(str(log_file))
# Verify file is gone # Verify file is gone
assert not os.path.isfile(str(log_file)) assert not os.path.isfile(str(log_file))
# Verify that we did not lose our effective log level even though # Verify that we did not lose our effective log level even though
# the above steps the level up for the duration of the capture # the above steps the level up for the duration of the capture

5
test/test_plugin_macosx.py

@ -38,6 +38,11 @@ from helpers import reload_plugin
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
if sys.platform not in ["darwin", "linux"]:
pytest.skip("Only makes sense on macOS, but also works on Linux",
allow_module_level=True)
@pytest.fixture @pytest.fixture
def pretend_macos(mocker): def pretend_macos(mocker):
""" """

8
test/test_plugin_syslog.py

@ -32,10 +32,14 @@ import socket
# Disable logging for a cleaner testing output # Disable logging for a cleaner testing output
import logging import logging
logging.disable(logging.CRITICAL)
from apprise.plugins.NotifySyslog import NotifySyslog
logging.disable(logging.CRITICAL) # The `syslog` module is not available on Windows.
# `ModuleNotFoundError: No module named 'syslog'`
NotifySyslog = pytest.importorskip(
"apprise.plugins.NotifySyslog",
reason="`syslog` module not available on Windows").NotifySyslog
@mock.patch('syslog.syslog') @mock.patch('syslog.syslog')

6
test/test_plugin_windows.py

@ -195,8 +195,9 @@ def test_plugin_windows_mocked():
@mock.patch('win32gui.UpdateWindow') @mock.patch('win32gui.UpdateWindow')
@mock.patch('win32gui.Shell_NotifyIcon') @mock.patch('win32gui.Shell_NotifyIcon')
@mock.patch('win32gui.LoadImage') @mock.patch('win32gui.LoadImage')
def test_plugin_windows_native( def test_plugin_windows_native(mock_loadimage,
mock_update_window, mock_loadimage, mock_notify): mock_notify,
mock_update_window):
""" """
NotifyWindows() General Checks (via Windows platform) NotifyWindows() General Checks (via Windows platform)
@ -261,6 +262,7 @@ def test_plugin_windows_native(
assert obj.duration == obj.default_popup_duration_sec assert obj.duration == obj.default_popup_duration_sec
# To avoid slowdowns (for testing), turn it to zero for now # To avoid slowdowns (for testing), turn it to zero for now
obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False)
obj.duration = 0 obj.duration = 0
# Test our loading of our icon exception; it will still allow the # Test our loading of our icon exception; it will still allow the

Loading…
Cancel
Save