Add a basic test suit

This commit is contained in:
Tony Crisci
2019-04-30 22:40:31 -04:00
parent 2330b64ff9
commit 8907df2394
12 changed files with 439 additions and 0 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ tags
.swp
.swo
.swn
env

10
.travis.yml Normal file
View File

@@ -0,0 +1,10 @@
language: minimal
sudo: required
dist: xenial
services:
- docker
before_install:
- docker build -t playerctl-test .
script:
- docker run -it playerctl-test

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM ubuntu:19.04
WORKDIR /app
RUN apt-get update && apt-get install -y \
python3-pip \
ninja-build \
build-essential \
libglib2.0-dev \
libgirepository1.0-dev \
gtk-doc-tools
COPY requirements.txt .
RUN pip3 install -r requirements.txt
ADD . /app
RUN find -name __pycache__ | xargs rm -r || true
RUN meson --prefix=/usr build && ninja -C build && ninja -C build install
CMD ["dbus-run-session", "python3", "-m", "pytest"]

12
Makefile Normal file
View File

@@ -0,0 +1,12 @@
.PHONY: test
.DEFAULT_TARGET := test
test:
dbus-run-session python3 -m pytest -sq
docker-test:
docker build -t playerctl-test .
docker run -it playerctl-test
format:
yapf -rip test

2
pytest.ini Normal file
View File

@@ -0,0 +1,2 @@
[pytest]
timeout = 5

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
dbus-next==0.0.1
meson
pytest
pytest-timeout
pytest-asyncio

123
test/.gitignore vendored Normal file
View File

@@ -0,0 +1,123 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that dont work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

0
test/__init__.py Normal file
View File

174
test/mpris.py Normal file
View File

@@ -0,0 +1,174 @@
from dbus_next.service import ServiceInterface, dbus_property, method, signal
from dbus_next import PropertyAccess, RequestNameReply
from dbus_next.aio import session_bus
import asyncio
async def setup_buses(*names):
async def setup(name):
bus = await session_bus()
reply = await bus.request_name(f'org.mpris.MediaPlayer2.{name}')
assert reply == RequestNameReply.PRIMARY_OWNER
bus.export('/org/mpris/MediaPlayer2', MprisPlayer())
return bus
return await asyncio.gather(*(setup(name) for name in names))
def get_interfaces(bus):
return bus._path_exports.get('/org/mpris/MediaPlayer2', [])
class MprisPlayer(ServiceInterface):
def __init__(self):
super().__init__('org.mpris.MediaPlayer2.Player')
self.reset()
def reset(self):
# method calls
self.next_called = False
self.previous_called = False
self.pause_called = False
self.play_pause_called = False
self.stop_called = False
self.play_called = False
self.seek_called_with = None
self.set_position_called_with = None
self.open_uri_called_with = None
# properties
self.playback_status = 'Stopped'
self.loop_status = 'None'
self.rate = 1.0
self.shuffle = False
self.metadata = {}
self.volume = 1.0
self.position = 0
self.minimum_rate = 1.0
self.maximum_rate = 1.0
self.can_go_next = True
self.can_go_previous = True
self.can_play = True
self.can_pause = True
self.can_seek = True
self.can_control = True
# signals
self.seeked_value = 0
@method()
def Next(self):
self.next_called = True
@method()
def Previous(self):
self.previous_called = True
@method()
def Pause(self):
self.pause_called = True
@method()
def PlayPause(self):
self.play_pause_called = True
@method()
def Stop(self):
self.stop_called = True
@method()
def Play(self):
self.play_called = True
@method()
def Seek(self, offset: 'x'):
self.seek_called_with = offset
@method()
def SetPosition(self, track_id: 'o', position: 'x'):
self.set_position_called_with = (track_id, position)
@method()
def OpenUri(self, uri: 's'):
self.open_uri_called_with = uri
@signal()
def Seeked(self) -> 'x':
return self.seeked_value
@dbus_property(access=PropertyAccess.READ)
def PlaybackStatus(self) -> 's':
return self.playback_status
@dbus_property()
def LoopStatus(self) -> 's':
return self.loop_status
@LoopStatus.setter
def LoopStatus(self, status: 's'):
self.loop_status = status
@dbus_property()
def Rate(self) -> 'd':
return self.rate
@Rate.setter
def Rate(self, rate: 'd'):
self.rate = rate
@dbus_property()
def Shuffle(self) -> 'b':
return self.shuffle
@Shuffle.setter
def Shuffle(self, shuffle: 'b'):
self.shuffle = shuffle
@dbus_property(access=PropertyAccess.READ)
def Metadata(self) -> 'a{sv}':
return self.metadata
@dbus_property()
def Volume(self) -> 'd':
return self.volume
@Volume.setter
def Volume(self, volume: 'd'):
self.volume = volume
@dbus_property(access=PropertyAccess.READ)
def Position(self) -> 'x':
return self.position
@dbus_property(access=PropertyAccess.READ)
def MinimumRate(self) -> 'd':
return self.minimum_rate
@dbus_property(access=PropertyAccess.READ)
def MaximumRate(self) -> 'd':
return self.maximum_rate
@dbus_property(access=PropertyAccess.READ)
def CanGoNext(self) -> 'b':
return self.can_go_next
@dbus_property(access=PropertyAccess.READ)
def CanGoPrevious(self) -> 'b':
return self.can_go_previous
@dbus_property(access=PropertyAccess.READ)
def CanPlay(self) -> 'b':
return self.can_play
@dbus_property(access=PropertyAccess.READ)
def CanPause(self) -> 'b':
return self.can_pause
@dbus_property(access=PropertyAccess.READ)
def CanSeek(self) -> 'b':
return self.can_seek
@dbus_property(access=PropertyAccess.READ)
def CanControl(self) -> 'b':
return self.can_control

18
test/playerctl.py Normal file
View File

@@ -0,0 +1,18 @@
import asyncio
class CommandResult:
def __init__(self, stdout, stderr, ret):
self.stdout = stdout.decode().strip()
self.stderr = stderr.decode().strip()
self.ret = ret
async def playerctl(cmd):
proc = await asyncio.create_subprocess_shell(
f'playerctl {cmd}',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
await proc.wait()
return CommandResult(stdout, stderr, proc.returncode)

51
test/test_basics.py Normal file
View File

@@ -0,0 +1,51 @@
from .mpris import setup_buses
from .playerctl import playerctl
import asyncio
import pytest
@pytest.mark.asyncio
async def test_basics():
result = await playerctl('--help')
assert result.ret == 0, result.stderr
assert result.stdout
assert not result.stderr
# with no players
result = await playerctl('--list-all')
assert result.ret == 0, result.stderr
assert not result.stdout
assert result.stderr
result = await playerctl('--version')
assert result.ret == 0, result.stderr
assert result.stdout
assert not result.stderr
commands = ('play', 'pause', 'play-pause', 'stop', 'next', 'previous',
'position', 'position 5', 'volume', 'volume 0.5', 'status',
'metadata', 'loop', 'loop None', 'shuffle', 'shuffle On',
'open https://google.com')
results = await asyncio.gather(*(playerctl(cmd) for cmd in commands))
for result in results:
assert result.ret == 1
assert not result.stdout
assert result.stderr == 'No players found'
@pytest.mark.asyncio
async def test_list_names():
[bus1, bus2, bus3] = await setup_buses('basics1', 'basics2', 'basics3')
result = await playerctl('--list-all')
assert result.ret == 0, result.stderr
players = result.stdout.splitlines()
assert 'basics1' in players
assert 'basics2' in players
assert 'basics3' in players
for bus in [bus1, bus2, bus3]:
bus.disconnect()

23
test/test_commands.py Normal file
View File

@@ -0,0 +1,23 @@
from .mpris import setup_buses, get_interfaces
from .playerctl import playerctl
import asyncio
import pytest
@pytest.mark.asyncio
async def test_commands():
[bus] = await setup_buses('commands')
[interface] = get_interfaces(bus)
commands = ('play', 'pause', 'play-pause', 'stop', 'next', 'previous')
def get_called(cmd):
return getattr(interface, f'{cmd.replace("-", "_")}_called')
results = await asyncio.gather(*(playerctl(f'-p commands {cmd}')
for cmd in commands))
for i, result in enumerate(results):
cmd = commands[i]
assert get_called(cmd), f'{cmd} was not called: {result.stderr}'