Improve CI tests (#14)

This commit is contained in:
Michaël Arnauts 2020-03-26 11:31:28 +01:00 committed by GitHub
parent fe82e8e9e0
commit e35dd28de9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 173 additions and 136 deletions

10
.gitattributes vendored Normal file
View File

@ -0,0 +1,10 @@
.github/ export-ignore
test/ export-ignore
.coverage export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.pylintrc export-ignore
codecov.yml export-ignore
Makefile export-ignore
requirements.txt export-ignore
tox.ini export-ignore

41
.github/workflows/addon-check.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Kodi
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
tests:
name: Kodi Add-on checker
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
kodi-branch: [leia, matrix]
steps:
- name: Check out ${{ github.sha }} from repository ${{ github.repository }}
uses: actions/checkout@v2
with:
path: ${{ github.repository }}
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
sudo apt-get install libxml2-utils xmlstarlet
python -m pip install --upgrade pip
pip install kodi-addon-checker
- name: Remove unwanted files
run: awk '/export-ignore/ { print $1 }' .gitattributes | xargs rm -rf --
working-directory: ${{ github.repository }}
- name: Rewrite addon.xml for Matrix
run: xmlstarlet ed -L -u '/addon/requires/import[@addon="xbmc.python"]/@version' -v "3.0.0" addon.xml
working-directory: ${{ github.repository }}
if: matrix.kodi-branch == 'matrix'
- name: Run kodi-addon-checker
run: kodi-addon-checker --branch=${{ matrix.kodi-branch }} ${{ github.repository }}/

View File

@ -2,41 +2,26 @@ name: CI
on: on:
push: push:
branches: [ master ] branches:
- master
pull_request: pull_request:
branches: [ master ] branches:
- master
jobs: jobs:
check-addon:
name: Run kodi-addon-checker
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt-get install libxml2-utils
python -m pip install --upgrade pip
pip install kodi-addon-checker
- name: Run kodi-addon-checker
run: |
make check-addon
tests: tests:
name: Run unit tests name: Unit tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
PYTHONIOENCODING: utf-8
PYTHONPATH: ${{ github.workspace }}/resources/lib:${{ github.workspace }}/test
strategy: strategy:
fail-fast: false
matrix: matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8] python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
steps: steps:
- uses: actions/checkout@v2 - name: Check out ${{ github.sha }} from repository ${{ github.repository }}
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1 uses: actions/setup-python@v1
with: with:
@ -44,19 +29,30 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get install gettext sudo apt-get install gettext
sudo pip install coverage --install-option="--install-scripts=/usr/bin"
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
- name: Run checks - name: Run pylint
run: | run: |
make check-pylint make check-pylint
- name: Run tox
run: |
make check-tox make check-tox
- name: Check translations
run: |
make check-translations make check-translations
- name: Run tests - name: Run unit tests
env: env:
PYTHONIOENCODING: utf-8
ADDON_USERNAME: ${{ secrets.ADDON_USERNAME }} ADDON_USERNAME: ${{ secrets.ADDON_USERNAME }}
ADDON_PASSWORD: ${{ secrets.ADDON_PASSWORD }} ADDON_PASSWORD: ${{ secrets.ADDON_PASSWORD }}
run: | run: |
make test coverage run -m unittest discover
- name: Upload code coverage - name: Run addon
run: |
coverage run -a test/run.py /
- name: Run add-on service
run: |
coverage run -a service_entry.py
- name: Upload code coverage to CodeCov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
continue-on-error: true

View File

@ -45,12 +45,7 @@ test: test-unit
test-unit: test-unit:
@echo ">>> Running unit tests" @echo ">>> Running unit tests"
ifdef GITHUB_ACTIONS
@coverage run -m unittest discover
@coverage xml
else
@$(PYTHON) -m unittest discover -v -b -f @$(PYTHON) -m unittest discover -v -b -f
endif
clean: clean:
@find . -name '*.pyc' -type f -delete @find . -name '*.pyc' -type f -delete

View File

@ -4,9 +4,9 @@
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-yellow.svg)](https://opensource.org/licenses/GPL-3.0) [![License: GPLv3](https://img.shields.io/badge/License-GPLv3-yellow.svg)](https://opensource.org/licenses/GPL-3.0)
[![Contributors](https://img.shields.io/github/contributors/add-ons/plugin.video.viervijfzes.svg)](https://github.com/add-ons/plugin.video.viervijfzes/graphs/contributors) [![Contributors](https://img.shields.io/github/contributors/add-ons/plugin.video.viervijfzes.svg)](https://github.com/add-ons/plugin.video.viervijfzes/graphs/contributors)
# VIER / VIJF / ZES Kodi add-on # VIER VIJF ZES Kodi add-on
*plugin.video.viervijfzes* is een Kodi add-on om de video-on-demand content van [vier.be](https://vier.be/), [vijf.be](https://vijf.be/) en [zestv.be](https://zestv.be/) te bekijken. *plugin.video.viervijfzes* is een Kodi add-on om de video-on-demand content van [vier.be](https://www.vier.be/), [vijf.be](https://www.vijf.be/) en [zestv.be](https://www.zestv.be/) te bekijken.
> Note: Je moet eerst een account aanmaken op één van bovenstaande websites. > Note: Je moet eerst een account aanmaken op één van bovenstaande websites.

View File

@ -1,4 +1,4 @@
codecov coverage
git+git://github.com/emilsvennesson/script.module.inputstreamhelper.git@master#egg=inputstreamhelper git+git://github.com/emilsvennesson/script.module.inputstreamhelper.git@master#egg=inputstreamhelper
polib polib
pylint pylint

View File

@ -46,7 +46,7 @@ class AuthApi:
if self._id_token and self._expiry > now: if self._id_token and self._expiry > now:
# We have a valid id token in memory, use it # We have a valid id token in memory, use it
_LOGGER.debug('Got an id token from memory: %s', self._id_token) _LOGGER.debug('Got an id token from memory')
return self._id_token return self._id_token
if self._refresh_token: if self._refresh_token:
@ -56,7 +56,6 @@ class AuthApi:
try: try:
self._id_token = self._refresh(self._refresh_token) self._id_token = self._refresh(self._refresh_token)
self._expiry = now + 3600 self._expiry = now + 3600
_LOGGER.debug('Got an id token by refreshing: %s', self._id_token)
except (InvalidLoginException, AuthenticationException) as exc: except (InvalidLoginException, AuthenticationException) as exc:
_LOGGER.error('Error logging in: %s', str(exc)) _LOGGER.error('Error logging in: %s', str(exc))
self._id_token = None self._id_token = None
@ -71,7 +70,6 @@ class AuthApi:
self._id_token = id_token self._id_token = id_token
self._refresh_token = refresh_token self._refresh_token = refresh_token
self._expiry = now + 3600 self._expiry = now + 3600
_LOGGER.debug('Got an id token by logging in: %s', self._id_token)
# Store new tokens in cache # Store new tokens in cache
if not kodiutils.exists(self._cache_dir): if not kodiutils.exists(self._cache_dir):

View File

@ -90,7 +90,6 @@ class AwsIdp:
auth_response = self._session.post(self.url, auth_data, headers=auth_headers) auth_response = self._session.post(self.url, auth_data, headers=auth_headers)
auth_response_json = json.loads(auth_response.text) auth_response_json = json.loads(auth_response.text)
challenge_parameters = auth_response_json.get("ChallengeParameters") challenge_parameters = auth_response_json.get("ChallengeParameters")
_LOGGER.debug(challenge_parameters)
challenge_name = auth_response_json.get("ChallengeName") challenge_name = auth_response_json.get("ChallengeName")
if not challenge_name == "PASSWORD_VERIFIER": if not challenge_name == "PASSWORD_VERIFIER":
@ -105,7 +104,6 @@ class AwsIdp:
} }
auth_response = self._session.post(self.url, challenge_data, headers=challenge_headers) auth_response = self._session.post(self.url, challenge_data, headers=challenge_headers)
auth_response_json = json.loads(auth_response.text) auth_response_json = json.loads(auth_response.text)
_LOGGER.debug("Got response: %s", auth_response_json)
if "message" in auth_response_json: if "message" in auth_response_json:
raise InvalidLoginException(auth_response_json.get("message")) raise InvalidLoginException(auth_response_json.get("message"))
@ -348,7 +346,6 @@ class AwsIdp:
format_string = "{} {} %-d %H:%M:%S UTC %Y".format(days[time_now.weekday()], months[time_now.month]) format_string = "{} {} %-d %H:%M:%S UTC %Y".format(days[time_now.weekday()], months[time_now.month])
time_string = datetime.datetime.utcnow().strftime(format_string) time_string = datetime.datetime.utcnow().strftime(format_string)
_LOGGER.debug("AWS Auth Timestamp: %s", time_string)
return time_string return time_string
def __str__(self): def __str__(self):

View File

@ -146,58 +146,6 @@ class ContentApi:
self._session = requests.session() self._session = requests.session()
self._auth = auth self._auth = auth
def get_notifications(self):
""" Get a list of notifications for your account.
:rtype list[dict]
"""
response = self._get_url(self.API_ENDPOINT + '/notifications', authentication=True)
data = json.loads(response)
return data
def get_content_tree(self, channel):
""" Get a list of all the content.
:type channel: str
:rtype list[dict]
"""
if channel not in self.SITE_APIS:
raise Exception('Unknown channel %s' % channel)
response = self._get_url(self.SITE_APIS[channel] + '/content_tree', authentication=True)
data = json.loads(response)
return data
def get_stream_by_uuid(self, uuid):
""" Get the stream URL to use for this video.
:type uuid: str
:rtype str
"""
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True)
data = json.loads(response)
return data['video']['S']
def get_programs_new(self, channel):
""" Get a list of all programs of the specified channel.
:type channel: str
:rtype list[Program]
"""
if channel not in CHANNELS:
raise Exception('Unknown channel %s' % channel)
# Request all content from this channel
content_tree = self.get_content_tree(channel)
programs = []
for uuid in content_tree['programs']:
try:
program = self.get_program_by_uuid(uuid)
program.channel = channel
programs.append(program)
except UnavailableException:
# Some programs are not available, but do occur in the content tree
pass
return programs
def get_programs(self, channel): def get_programs(self, channel):
""" Get a list of all programs of the specified channel. """ Get a list of all programs of the specified channel.
:type channel: str :type channel: str
@ -332,6 +280,15 @@ class ContentApi:
return None return None
def get_stream_by_uuid(self, uuid):
""" Get the stream URL to use for this video.
:type uuid: str
:rtype str
"""
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True)
data = json.loads(response)
return data['video']['S']
@staticmethod @staticmethod
def _parse_program_data(data): def _parse_program_data(data):
""" Parse the Program JSON. """ Parse the Program JSON.

40
test/run.py Executable file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
""" Run any Kodi VTM GO plugin:// URL on the commandline """
# pylint: disable=invalid-name
from __future__ import absolute_import, division, print_function, unicode_literals
import os
import sys
# Add current working directory to import paths
cwd = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(os.path.realpath(__file__))), os.pardir))
sys.path.insert(0, cwd)
from resources.lib import addon # noqa: E402 pylint: disable=wrong-import-position
xbmc = __import__('xbmc')
xbmcaddon = __import__('xbmcaddon')
xbmcgui = __import__('xbmcgui')
xbmcplugin = __import__('xbmcplugin')
xbmcvfs = __import__('xbmcvfs')
if len(sys.argv) <= 1:
print("%s: URI argument missing\nTry '%s plugin://plugin.video.viervijfzes/' to test." % (sys.argv[0], sys.argv[0]))
sys.exit(1)
# Also support bare paths like /recent/2
if not sys.argv[1].startswith('plugin://'):
sys.argv[1] = 'plugin://plugin.video.viervijfzes' + sys.argv[1]
# Split path and args
try:
path, args = sys.argv[1].split('?', 1)
except ValueError:
path, args = sys.argv[1], ''
print('** Running URI %s with args %s' % (path, args))
plugin = addon.routing
plugin.run([path, 0, args])

View File

@ -1,6 +1,7 @@
{ {
"plugin.video.viervijfzes": { "plugin.video.viervijfzes": {
"_comment": "do-not-add-username-and-password-here" "_comment": "do-not-add-username-and-password-here",
"metadata_update": "true"
}, },
"plugin.video.youtube": { "plugin.video.youtube": {
}, },

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com> # Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
''' This file implements the Kodi xbmc module, either using stubs or alternative functionality ''' """ This file implements the Kodi xbmc module, either using stubs or alternative functionality """
# pylint: disable=invalid-name,no-self-use,unused-argument # pylint: disable=invalid-name,no-self-use,unused-argument
@ -60,113 +60,115 @@ def from_unicode(text, encoding='utf-8'):
class Keyboard: class Keyboard:
''' A stub implementation of the xbmc Keyboard class ''' """ A stub implementation of the xbmc Keyboard class """
def __init__(self, line='', heading=''): def __init__(self, line='', heading=''):
''' A stub constructor for the xbmc Keyboard class ''' """ A stub constructor for the xbmc Keyboard class """
def doModal(self, autoclose=0): def doModal(self, autoclose=0):
''' A stub implementation for the xbmc Keyboard class doModal() method ''' """ A stub implementation for the xbmc Keyboard class doModal() method """
def isConfirmed(self): def isConfirmed(self):
''' A stub implementation for the xbmc Keyboard class isConfirmed() method ''' """ A stub implementation for the xbmc Keyboard class isConfirmed() method """
return True return True
def getText(self): def getText(self):
''' A stub implementation for the xbmc Keyboard class getText() method ''' """ A stub implementation for the xbmc Keyboard class getText() method """
return 'test' return 'test'
class Monitor: class Monitor:
''' A stub implementation of the xbmc Monitor class ''' """A stub implementation of the xbmc Monitor class"""
def __init__(self, line='', heading=''): def __init__(self, line='', heading=''):
''' A stub constructor for the xbmc Monitor class ''' """A stub constructor for the xbmc Monitor class"""
self._deadline = time.time() + 10 # 10 seconds
def abortRequested(self): def abortRequested(self):
''' A stub implementation for the xbmc Keyboard class abortRequested() method ''' """A stub implementation for the xbmc Keyboard class abortRequested() method"""
return False return time.time() > self._deadline
def waitForAbort(self, timeout=None): def waitForAbort(self, timeout=None):
''' A stub implementation for the xbmc Keyboard class waitForAbort() method ''' """A stub implementation for the xbmc Keyboard class waitForAbort() method"""
time.sleep(0.5)
return False return False
class Player: class Player:
''' A stub implementation of the xbmc Player class ''' """ A stub implementation of the xbmc Player class """
def __init__(self): def __init__(self):
self._count = 0 self._count = 0
def play(self, item='', listitem=None, windowed=False, startpos=-1): def play(self, item='', listitem=None, windowed=False, startpos=-1):
''' A stub implementation for the xbmc Player class play() method ''' """ A stub implementation for the xbmc Player class play() method """
return return
def isPlaying(self): def isPlaying(self):
''' A stub implementation for the xbmc Player class isPlaying() method ''' """ A stub implementation for the xbmc Player class isPlaying() method """
# Return True four times out of five # Return True four times out of five
self._count += 1 self._count += 1
return bool(self._count % 5 != 0) return bool(self._count % 5 != 0)
def setSubtitles(self, subtitleFile): def setSubtitles(self, subtitleFile):
''' A stub implementation for the xbmc Player class setSubtitles() method ''' """ A stub implementation for the xbmc Player class setSubtitles() method """
return return
def showSubtitles(self, visible): def showSubtitles(self, visible):
''' A stub implementation for the xbmc Player class showSubtitles() method ''' """ A stub implementation for the xbmc Player class showSubtitles() method """
return return
def getTotalTime(self): def getTotalTime(self):
''' A stub implementation for the xbmc Player class getTotalTime() method ''' """ A stub implementation for the xbmc Player class getTotalTime() method """
return 0 return 0
def getTime(self): def getTime(self):
''' A stub implementation for the xbmc Player class getTime() method ''' """ A stub implementation for the xbmc Player class getTime() method """
return 0 return 0
def getVideoInfoTag(self): def getVideoInfoTag(self):
''' A stub implementation for the xbmc Player class getVideoInfoTag() method ''' """ A stub implementation for the xbmc Player class getVideoInfoTag() method """
return VideoInfoTag() return VideoInfoTag()
def getPlayingFile(self): def getPlayingFile(self):
''' A stub implementation for the xbmc Player class getPlayingFile() method ''' """ A stub implementation for the xbmc Player class getPlayingFile() method """
return '' return ''
class VideoInfoTag: class VideoInfoTag:
''' A stub implementation of the xbmc VideoInfoTag class ''' """ A stub implementation of the xbmc VideoInfoTag class """
def __init__(self): def __init__(self):
''' A stub constructor for the xbmc VideoInfoTag class ''' """ A stub constructor for the xbmc VideoInfoTag class """
def getSeason(self): def getSeason(self):
''' A stub implementation for the xbmc VideoInfoTag class getSeason() method ''' """ A stub implementation for the xbmc VideoInfoTag class getSeason() method """
return 0 return 0
def getEpisode(self): def getEpisode(self):
''' A stub implementation for the xbmc VideoInfoTag class getEpisode() method ''' """ A stub implementation for the xbmc VideoInfoTag class getEpisode() method """
return 0 return 0
def getTVShowTitle(self): def getTVShowTitle(self):
''' A stub implementation for the xbmc VideoInfoTag class getTVShowTitle() method ''' """ A stub implementation for the xbmc VideoInfoTag class getTVShowTitle() method """
return '' return ''
def getPlayCount(self): def getPlayCount(self):
''' A stub implementation for the xbmc VideoInfoTag class getPlayCount() method ''' """ A stub implementation for the xbmc VideoInfoTag class getPlayCount() method """
return 0 return 0
def getRating(self): def getRating(self):
''' A stub implementation for the xbmc VideoInfoTag class getRating() method ''' """ A stub implementation for the xbmc VideoInfoTag class getRating() method """
return 0 return 0
def executebuiltin(string, wait=False): # pylint: disable=unused-argument def executebuiltin(string, wait=False): # pylint: disable=unused-argument
''' A stub implementation of the xbmc executebuiltin() function ''' """ A stub implementation of the xbmc executebuiltin() function """
return return
def executeJSONRPC(jsonrpccommand): def executeJSONRPC(jsonrpccommand):
''' A reimplementation of the xbmc executeJSONRPC() function ''' """ A reimplementation of the xbmc executeJSONRPC() function """
command = json.loads(jsonrpccommand) command = json.loads(jsonrpccommand)
if command.get('method') == 'Settings.GetSettingValue': if command.get('method') == 'Settings.GetSettingValue':
key = command.get('params').get('setting') key = command.get('params').get('setting')
@ -184,19 +186,19 @@ def executeJSONRPC(jsonrpccommand):
def getCondVisibility(string): def getCondVisibility(string):
''' A reimplementation of the xbmc getCondVisibility() function ''' """ A reimplementation of the xbmc getCondVisibility() function """
if string == 'system.platform.android': if string == 'system.platform.android':
return False return False
return True return True
def getInfoLabel(key): def getInfoLabel(key):
''' A reimplementation of the xbmc getInfoLabel() function ''' """ A reimplementation of the xbmc getInfoLabel() function """
return INFO_LABELS.get(key) return INFO_LABELS.get(key)
def getLocalizedString(msgctxt): def getLocalizedString(msgctxt):
''' A reimplementation of the xbmc getLocalizedString() function ''' """ A reimplementation of the xbmc getLocalizedString() function """
for entry in PO: for entry in PO:
if entry.msgctxt == '#%s' % msgctxt: if entry.msgctxt == '#%s' % msgctxt:
return entry.msgstr or entry.msgid return entry.msgstr or entry.msgid
@ -206,12 +208,12 @@ def getLocalizedString(msgctxt):
def getRegion(key): def getRegion(key):
''' A reimplementation of the xbmc getRegion() function ''' """ A reimplementation of the xbmc getRegion() function """
return REGIONS.get(key) return REGIONS.get(key)
def log(msg, level=LOGINFO): def log(msg, level=LOGINFO):
''' A reimplementation of the xbmc log() function ''' """ A reimplementation of the xbmc log() function """
if level in (LOGERROR, LOGFATAL): if level in (LOGERROR, LOGFATAL):
print('\033[31;1m%s: \033[32;0m%s\033[0;39m' % (LOG_MAPPING.get(level), to_unicode(msg))) print('\033[31;1m%s: \033[32;0m%s\033[0;39m' % (LOG_MAPPING.get(level), to_unicode(msg)))
if level == LOGFATAL: if level == LOGFATAL:
@ -223,17 +225,17 @@ def log(msg, level=LOGINFO):
def setContent(self, content): def setContent(self, content):
''' A stub implementation of the xbmc setContent() function ''' """ A stub implementation of the xbmc setContent() function """
return return
def sleep(seconds): def sleep(seconds):
''' A reimplementation of the xbmc sleep() function ''' """ A reimplementation of the xbmc sleep() function """
time.sleep(seconds) time.sleep(seconds)
def translatePath(path): def translatePath(path):
''' A stub implementation of the xbmc translatePath() function ''' """ A stub implementation of the xbmc translatePath() function """
if path.startswith('special://home'): if path.startswith('special://home'):
return path.replace('special://home', os.path.join(os.getcwd(), 'test/')) return path.replace('special://home', os.path.join(os.getcwd(), 'test/'))
if path.startswith('special://masterprofile'): if path.startswith('special://masterprofile'):