Skip to content

Commit

Permalink
Add basic cli tests for date/time options and fix build warnings
Browse files Browse the repository at this point in the history
* Add basic cli tests for date/time option
* Fix mock deprecation warning using mocker in tests
* Fix PythonUnknownMarkWarning then running tests

Pytest does not recognize the 'datafiles' mark provided by the
corresponding plugin. The solution is to register this mark as a custom
one in pytest.ini, as suggested by the warning message itself.
  • Loading branch information
davidag authored and jmaupetit committed Nov 26, 2019
1 parent ba72734 commit 224bf44
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 113 deletions.
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
markers =
datafiles: pytest-datafiles plugin marker. This avoids warning message.
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Provide fixtures for pytest-based unit tests."""

from click.testing import CliRunner
import pytest

from watson import Watson
Expand All @@ -13,3 +14,8 @@ def config_dir(tmpdir):
@pytest.fixture
def watson(config_dir):
return Watson(config_dir=config_dir)


@pytest.fixture
def runner():
return CliRunner()
174 changes: 174 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import re
import arrow
from itertools import combinations
from datetime import datetime, timedelta
from dateutil.tz import tzlocal

import pytest

from watson import cli


# Not all ISO-8601 compliant strings are recognized by arrow.get(str)
VALID_DATES_DATA = [
('2018', '2018-01-01 00:00:00'), # years
('2018-04', '2018-04-01 00:00:00'), # calendar dates
('2018-04-10', '2018-04-10 00:00:00'),
('2018/04/10', '2018-04-10 00:00:00'),
('2018.04.10', '2018-04-10 00:00:00'),
('2018-4-10', '2018-04-10 00:00:00'),
('2018/4/10', '2018-04-10 00:00:00'),
('2018.4.10', '2018-04-10 00:00:00'),
('20180410', '2018-04-10 00:00:00'),
('2018-123', '2018-05-03 00:00:00'), # ordinal dates
('2018-04-10 12:30:43', '2018-04-10 12:30:43'),
('2018-04-10T12:30:43', '2018-04-10 12:30:43'),
('2018-04-10 12:30:43Z', '2018-04-10 12:30:43'),
('2018-04-10 12:30:43.1233', '2018-04-10 12:30:43'),
('2018-04-10 12:30:43+03:00', '2018-04-10 12:30:43'),
('2018-04-10 12:30:43-07:00', '2018-04-10 12:30:43'),
('2018-04-10T12:30:43-07:00', '2018-04-10 12:30:43'),
('2018-04-10 12:30', '2018-04-10 12:30:00'),
('2018-04-10T12:30', '2018-04-10 12:30:00'),
('2018-04-10 12', '2018-04-10 12:00:00'),
('2018-04-10T12', '2018-04-10 12:00:00'),
]

INVALID_DATES_DATA = [
(' 2018'),
('2018 '),
('201804'),
('18-04-10'),
('180410'), # truncated representation not allowed
('2018-W08'), # despite week dates being part of ISO-8601
('2018W08'),
('2018-W08-2'),
('2018W082'),
('hello 2018'),
('yesterday'),
('tomorrow'),
('14:05:12.000'), # Times alone are not allowed
('140512.000'),
('14:05:12'),
('140512'),
('14:05'),
('14.05'),
('2018-04-10T'),
('2018-04-10T12:30:43.'),
]

VALID_TIMES_DATA = [
('14:12'),
('14:12:43'),
('2019-04-10T14:12'),
('2019-04-10T14:12:43'),
]


class OutputParser:
FRAME_ID_PATTERN = re.compile(r'id: (?P<frame_id>[0-9a-f]+)')

@staticmethod
def get_frame_id(output):
return OutputParser.FRAME_ID_PATTERN.search(output).group('frame_id')

@staticmethod
def get_start_date(watson, output):
frame_id = OutputParser.get_frame_id(output)
return watson.frames[frame_id].start.format('YYYY-MM-DD HH:mm:ss')


# watson add

@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
def test_add_valid_date(runner, watson, test_dt, expected):
result = runner.invoke(
cli.add,
['-f', test_dt, '-t', test_dt, 'project-name'],
obj=watson)
assert result.exit_code == 0
assert OutputParser.get_start_date(watson, result.output) == expected


@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
def test_add_invalid_date(runner, watson, test_dt):
result = runner.invoke(cli.add,
['-f', test_dt, '-t', test_dt, 'project-name'],
obj=watson)
assert result.exit_code != 0


# watson aggregate

@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
def test_aggregate_valid_date(runner, watson, test_dt, expected):
# This is super fast, because no internal 'report' invocations are made
result = runner.invoke(cli.aggregate,
['-f', test_dt, '-t', test_dt],
obj=watson)
assert result.exit_code == 0


@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
def test_aggregate_invalid_date(runner, watson, test_dt):
# This is super fast, because no internal 'report' invocations are made
result = runner.invoke(cli.aggregate,
['-f', test_dt, '-t', test_dt],
obj=watson)
assert result.exit_code != 0


# watson log

@pytest.mark.parametrize('cmd', [cli.aggregate, cli.log, cli.report])
def test_incompatible_options(runner, watson, cmd):
name_interval_options = ['--' + s for s in cli._SHORTCUT_OPTIONS]
for opt1, opt2 in combinations(name_interval_options, 2):
result = runner.invoke(cmd, [opt1, opt2], obj=watson)
assert result.exit_code != 0


@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
def test_log_valid_date(runner, watson, test_dt, expected):
result = runner.invoke(cli.log, ['-f', test_dt, '-t', test_dt], obj=watson)
assert result.exit_code == 0


@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
def test_log_invalid_date(runner, watson, test_dt):
result = runner.invoke(cli.log, ['-f', test_dt, '-t', test_dt], obj=watson)
assert result.exit_code != 0


# watson report

@pytest.mark.parametrize('test_dt,expected', VALID_DATES_DATA)
def test_report_valid_date(runner, watson, test_dt, expected):
result = runner.invoke(cli.report,
['-f', test_dt, '-t', test_dt],
obj=watson)
assert result.exit_code == 0


@pytest.mark.parametrize('test_dt', INVALID_DATES_DATA)
def test_report_invalid_date(runner, watson, test_dt):
result = runner.invoke(cli.report,
['-f', test_dt, '-t', test_dt],
obj=watson)
assert result.exit_code != 0


# watson stop

@pytest.mark.parametrize('at_dt', VALID_TIMES_DATA)
def test_stop_valid_time(runner, watson, mocker, at_dt):
mocker.patch('arrow.arrow.datetime', wraps=datetime)
start_dt = datetime(2019, 4, 10, 14, 0, 0, tzinfo=tzlocal())
arrow.arrow.datetime.now.return_value = start_dt
result = runner.invoke(cli.start, ['a-project'], obj=watson)
assert result.exit_code == 0
# Simulate one hour has elapsed, so that 'at_dt' is older than now()
# but newer than the start date.
arrow.arrow.datetime.now.return_value = (start_dt + timedelta(hours=1))
result = runner.invoke(cli.stop, ['--at', at_dt], obj=watson)
assert result.exit_code == 0
20 changes: 10 additions & 10 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
from . import mock_read


def test_config_get(mock, watson):
def test_config_get(mocker, watson):
content = u"""
[backend]
url = foo
token =
"""
mock.patch.object(ConfigParser, 'read', mock_read(content))
mocker.patch.object(ConfigParser, 'read', mock_read(content))
config = watson.config
assert config.get('backend', 'url') == 'foo'
assert config.get('backend', 'token') == ''
Expand All @@ -23,7 +23,7 @@ def test_config_get(mock, watson):
assert config.get('option', 'spamm', 'eggs') == 'eggs'


def test_config_getboolean(mock, watson):
def test_config_getboolean(mocker, watson):
content = u"""
[options]
flag1 = 1
Expand All @@ -33,7 +33,7 @@ def test_config_getboolean(mock, watson):
flag5 = false
flag6 =
"""
mock.patch.object(ConfigParser, 'read', mock_read(content))
mocker.patch.object(ConfigParser, 'read', mock_read(content))
config = watson.config
assert config.getboolean('options', 'flag1') is True
assert config.getboolean('options', 'flag1', False) is True
Expand All @@ -47,14 +47,14 @@ def test_config_getboolean(mock, watson):
assert config.getboolean('options', 'missing', True) is True


def test_config_getint(mock, watson):
def test_config_getint(mocker, watson):
content = u"""
[options]
value1 = 42
value2 = spamm
value3 =
"""
mock.patch.object(ConfigParser, 'read', mock_read(content))
mocker.patch.object(ConfigParser, 'read', mock_read(content))
config = watson.config
assert config.getint('options', 'value1') == 42
assert config.getint('options', 'value1', 666) == 42
Expand All @@ -71,7 +71,7 @@ def test_config_getint(mock, watson):
config.getint('options', 'value3')


def test_config_getfloat(mock, watson):
def test_config_getfloat(mocker, watson):
content = u"""
[options]
value1 = 3.14
Expand All @@ -80,7 +80,7 @@ def test_config_getfloat(mock, watson):
value4 =
"""

mock.patch.object(ConfigParser, 'read', mock_read(content))
mocker.patch.object(ConfigParser, 'read', mock_read(content))
config = watson.config
assert config.getfloat('options', 'value1') == 3.14
assert config.getfloat('options', 'value1', 6.66) == 3.14
Expand All @@ -98,7 +98,7 @@ def test_config_getfloat(mock, watson):
config.getfloat('options', 'value4')


def test_config_getlist(mock, watson):
def test_config_getlist(mocker, watson):
content = u"""
# empty lines in option values (including the first one) are discarded
[options]
Expand All @@ -121,7 +121,7 @@ def test_config_getlist(mock, watson):
two #three
four # five
"""
mock.patch.object(ConfigParser, 'read', mock_read(content))
mocker.patch.object(ConfigParser, 'read', mock_read(content))
gl = watson.config.getlist
assert gl('options', 'value1') == ['one', 'two three', 'four',
'five six']
Expand Down
4 changes: 2 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def test_safe_save(config_dir):
assert os.path.getmtime(save_file) >= os.path.getmtime(backup_file)


def test_safe_save_tmpfile_on_other_filesystem(config_dir, mock):
def test_safe_save_tmpfile_on_other_filesystem(config_dir, mocker):
save_file = os.path.join(config_dir, 'test')
backup_file = os.path.join(config_dir, 'test' + '.bak')

Expand All @@ -148,7 +148,7 @@ def test_safe_save_tmpfile_on_other_filesystem(config_dir, mock):

# simulate tmpfile being on another file-system
# OSError is caught and handled by shutil.move() used by save_safe()
mock.patch('os.rename', side_effect=OSError)
mocker.patch('os.rename', side_effect=OSError)
safe_save(save_file, "Again")
assert os.path.exists(backup_file)

Expand Down
Loading

0 comments on commit 224bf44

Please sign in to comment.