mirror of https://github.com/caronc/apprise
CI: Enable testing on macOS and Windows (#707)
parent
4fc4b8e95f
commit
cddd5c4fb3
|
@ -8,6 +8,11 @@ on:
|
|||
schedule:
|
||||
- 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:
|
||||
analyze:
|
||||
name: Analyze
|
||||
|
|
|
@ -27,33 +27,62 @@ jobs:
|
|||
# all jobs once the first one fails (true).
|
||||
fail-fast: true
|
||||
|
||||
# Define a minimal test matrix, it will be
|
||||
# expanded using subsequent `include` items.
|
||||
matrix:
|
||||
os: [
|
||||
"ubuntu-latest",
|
||||
# "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",
|
||||
]
|
||||
os: ["ubuntu-latest"]
|
||||
python-version: ["3.10"]
|
||||
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:
|
||||
|
||||
# Within the `bare` environment, `all-plugin-requirements.txt` will NOT be
|
||||
# installed, to verify the application also works without those dependencies.
|
||||
- os: "ubuntu-latest"
|
||||
python-version: "3.10"
|
||||
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:
|
||||
OS: ${{ matrix.os }}
|
||||
PYTHON: ${{ matrix.python-version }}
|
||||
BARE: ${{ matrix.bare }}
|
||||
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:
|
||||
|
||||
- name: Acquire sources
|
||||
|
@ -88,13 +117,13 @@ jobs:
|
|||
run: |
|
||||
pip install -r all-plugin-requirements.txt
|
||||
|
||||
# Installing `dbus-python` will croak on PyPy, so skip it.
|
||||
[[ $PYTHON != 'pypy'* ]] && pip install dbus-python || true
|
||||
# Installing `dbus-python` will only work on Linux/CPython.
|
||||
[[ $RUNNER_OS = "Linux" && $PYTHON != 'pypy'* ]] && pip install dbus-python || true
|
||||
|
||||
- name: Install project dependencies (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
pip install -r win-requirements.txt
|
||||
[[ $PYTHON != 'pypy'* ]] && pip install -r win-requirements.txt || true
|
||||
|
||||
# Install package in editable mode,
|
||||
# and run project-specific tasks.
|
||||
|
|
|
@ -132,23 +132,6 @@ class NotifyMQTT(NotifyBase):
|
|||
# through their network flow at once.
|
||||
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
|
||||
templates = (
|
||||
'{schema}://{user}@{host}/{topic}',
|
||||
|
@ -534,3 +517,38 @@ class NotifyMQTT(NotifyBase):
|
|||
|
||||
# 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
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
# Root certificate authority bundle.
|
||||
certifi
|
||||
|
||||
# Application dependencies.
|
||||
requests
|
||||
requests-oauthlib
|
||||
click >= 5.0
|
||||
|
|
7
setup.py
7
setup.py
|
@ -27,6 +27,8 @@
|
|||
import re
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
cmdclass = {}
|
||||
|
@ -43,7 +45,8 @@ except ImportError:
|
|||
|
||||
install_options = os.environ.get("APPRISE_INSTALL", "").split(",")
|
||||
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
|
||||
install_requires += open('win-requirements.txt').readlines()
|
||||
|
||||
|
@ -60,7 +63,7 @@ setup(
|
|||
version='1.1.0',
|
||||
description='Push Notifications that work with just about every platform!',
|
||||
license='MIT',
|
||||
long_description=open('README.md').read(),
|
||||
long_description=open('README.md', encoding="utf-8").read(),
|
||||
long_description_content_type='text/markdown',
|
||||
cmdclass=cmdclass,
|
||||
url='https://github.com/caronc/apprise',
|
||||
|
|
|
@ -783,8 +783,8 @@ json://localhost:8080
|
|||
include {}""".format(str(cfg01)))
|
||||
|
||||
cfg02.write("""
|
||||
# syslog entry
|
||||
syslog://
|
||||
# json entry
|
||||
json://localhost:8080
|
||||
|
||||
# recursively include ourselves
|
||||
include cfg02.cfg""")
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
import re
|
||||
import time
|
||||
import urllib
|
||||
from unittest import mock
|
||||
|
||||
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
|
||||
# results from cache
|
||||
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
|
||||
assert re.search(r'[?&]mime=', response.url()) is None
|
||||
assert re.search(r'[?&]name=', response.url()) is None
|
||||
|
|
|
@ -182,7 +182,7 @@ version: 1
|
|||
urls:
|
||||
- pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b
|
||||
- mailto://test:password@gmail.com
|
||||
- syslog://:
|
||||
- json://localhost:
|
||||
- tag: devops, admin
|
||||
""", asset=AppriseAsset())
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ def test_config_memory():
|
|||
assert ConfigMemory.parse_url('garbage://') is None
|
||||
|
||||
# Initialize our object
|
||||
cm = ConfigMemory(content="syslog://", format='text')
|
||||
cm = ConfigMemory(content="json://localhost", format='text')
|
||||
|
||||
# one entry added
|
||||
assert len(cm) == 1
|
||||
|
@ -49,7 +49,7 @@ def test_config_memory():
|
|||
assert isinstance(cm.read(), str) is True
|
||||
|
||||
# Test situation where an auto-detect is required:
|
||||
cm = ConfigMemory(content="syslog://")
|
||||
cm = ConfigMemory(content="json://localhost")
|
||||
|
||||
# one entry added
|
||||
assert len(cm) == 1
|
||||
|
|
|
@ -24,9 +24,11 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
import ctypes
|
||||
import pytest
|
||||
|
||||
from apprise import AppriseLocale
|
||||
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()
|
||||
# 4105 = en_CA
|
||||
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"):
|
||||
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
|
||||
# then it will resort to the environment variables
|
||||
# then it will resort to the environment variables.
|
||||
with environ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_CTYPE'):
|
||||
# Language can't be detected
|
||||
assert AppriseLocale.AppriseLocale.detect_language() is None
|
||||
|
||||
# Detect French language.
|
||||
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', LANG="fr_CA"):
|
||||
# Detect french language
|
||||
assert AppriseLocale.AppriseLocale.detect_language() == 'fr'
|
||||
|
||||
# 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())):
|
||||
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')
|
||||
def test_detect_language_defaultlocale(mock_getlocale):
|
||||
"""
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from unittest import mock
|
||||
|
@ -281,12 +283,16 @@ def test_apprise_log_file_captures(tmpdir):
|
|||
|
||||
assert len(logs) == 5
|
||||
|
||||
# Remove our file before we exit the with clause
|
||||
# this causes our delete() call to throw gracefully inside
|
||||
os.unlink(str(log_file))
|
||||
# Concurrent file access is not possible on Windows.
|
||||
# PermissionError: [WinError 32] The process cannot access the 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
|
||||
assert not os.path.isfile(str(log_file))
|
||||
# Verify file is gone
|
||||
assert not os.path.isfile(str(log_file))
|
||||
|
||||
# Verify that we did not lose our effective log level even though
|
||||
# the above steps the level up for the duration of the capture
|
||||
|
|
|
@ -38,6 +38,11 @@ from helpers import reload_plugin
|
|||
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
|
||||
def pretend_macos(mocker):
|
||||
"""
|
||||
|
|
|
@ -32,12 +32,16 @@ import socket
|
|||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
|
||||
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.openlog')
|
||||
def test_plugin_syslog_by_url(openlog, syslog):
|
||||
|
|
|
@ -195,8 +195,9 @@ def test_plugin_windows_mocked():
|
|||
@mock.patch('win32gui.UpdateWindow')
|
||||
@mock.patch('win32gui.Shell_NotifyIcon')
|
||||
@mock.patch('win32gui.LoadImage')
|
||||
def test_plugin_windows_native(
|
||||
mock_update_window, mock_loadimage, mock_notify):
|
||||
def test_plugin_windows_native(mock_loadimage,
|
||||
mock_notify,
|
||||
mock_update_window):
|
||||
"""
|
||||
NotifyWindows() General Checks (via Windows platform)
|
||||
|
||||
|
@ -261,6 +262,7 @@ def test_plugin_windows_native(
|
|||
assert obj.duration == obj.default_popup_duration_sec
|
||||
|
||||
# To avoid slowdowns (for testing), turn it to zero for now
|
||||
obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False)
|
||||
obj.duration = 0
|
||||
|
||||
# Test our loading of our icon exception; it will still allow the
|
||||
|
|
Loading…
Reference in New Issue