Skip to content

Commit

Permalink
Unit tests for ParsecValidator.coerce_* methods
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewrmshin committed Sep 25, 2018
1 parent c3986ba commit 3d37445
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 19 deletions.
22 changes: 9 additions & 13 deletions lib/cylc/cfgvalidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ class CylcConfigValidator(ParsecValidator):
Map value type keys with coerce methods.
"""
# Paramterized names containing at least one comma.
_REC_PARAM_INT_RANGE = re.compile(
r'\A([\+\-]?\d+)\.\.([\+\-]?\d+)(?:\.\.(\d+))?\Z')
_REC_NAME_SUFFIX = re.compile(r'\A[\w\-+%@]+\Z')
_REC_TRIG_FUNC = re.compile(r'(\w+)\((.*)\)(?:\:(\w+))?')

Expand Down Expand Up @@ -193,18 +191,17 @@ def coerce_parameter_list(cls, value, keys):
items = []
can_only_be = None # A flag to prevent mixing str and int range
for item in cls.strip_and_unquote_list(keys, value):
match = cls._REC_PARAM_INT_RANGE.match(item)
if match:
values = cls.parse_int_range(item)
if values is not None:
if can_only_be == str:
raise IllegalValueError(
'parameter', keys, value, 'mixing int range and str')
can_only_be = int
lower, upper, step = match.groups()
if not step:
step = 1
items.extend(range(int(lower), int(upper) + 1, int(step)))
items.extend(values)
elif cls._REC_NAME_SUFFIX.match(item):
if not item.isdigit():
try:
int(item)
except ValueError:
if can_only_be == int:
raise IllegalValueError(
'parameter', keys, value,
Expand All @@ -214,11 +211,10 @@ def coerce_parameter_list(cls, value, keys):
else:
raise IllegalValueError(
'parameter', keys, value, '%s: bad value' % item)
if not items or can_only_be == str or any(
not str(item).isdigit() for item in items):
return items
else:
try:
return [int(item) for item in items]
except ValueError:
return items

def coerce_xtrigger(self, value, keys):
"""Coerce a string into an xtrigger function context object.
Expand Down
24 changes: 24 additions & 0 deletions lib/parsec/tests/unit/00-validate.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
# Copyright (C) 2008-2018 NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------------
# Run unittest for parsec.validate
. "$(dirname "$0")/test_header"

set_test_number 1

run_ok "${TEST_NAME_BASE}" python "${TEST_SOURCE_DIR}/test_validate.py"
exit
1 change: 1 addition & 0 deletions lib/parsec/tests/unit/test_header
195 changes: 195 additions & 0 deletions lib/parsec/tests/unit/test_validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env python2

# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
# Copyright (C) 2008-2018 NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Unit Tests for parsec.validate.ParsecValidator.coerce* methods."""

import unittest

from parsec.validate import IllegalValueError, ParsecValidator


class TestParsecValidator(unittest.TestCase):
"""Unit Tests for parsec.validate.ParsecValidator.coerce* methods."""

def test_coerce_boolean(self):
"""Test coerce_boolean."""
validator = ParsecValidator()
# The good
for value, result in [
('True', True),
(' True ', True),
('"True"', True),
("'True'", True),
('true', True),
(' true ', True),
('"true"', True),
("'true'", True),
('False', False),
(' False ', False),
('"False"', False),
("'False'", False),
('false', False),
(' false ', False),
('"false"', False),
("'false'", False),
('', None),
(' ', None)]:
self.assertEqual(
validator.coerce_boolean(value, ['whatever']), result)
# The bad
for value in [
'None', ' Who cares? ', '3.14', '[]', '[True]', 'True, False']:
self.assertRaises(
IllegalValueError,
validator.coerce_boolean, value, ['whatever'])

def test_coerce_float(self):
"""Test coerce_float."""
validator = ParsecValidator()
# The good
for value, result in [
('', None),
('3', 3.0),
('9.80', 9.80),
('3.141592654', 3.141592654),
('"3.141592654"', 3.141592654),
("'3.141592654'", 3.141592654),
('-3', -3.0),
('-3.1', -3.1),
('0', 0.0),
('-0', -0.0),
('0.0', 0.0),
('1e20', 1.0e20),
('6.02e23', 6.02e23),
('-1.6021765e-19', -1.6021765e-19),
('6.62607004e-34', 6.62607004e-34)]:
self.assertAlmostEqual(
validator.coerce_float(value, ['whatever']), result)
# The bad
for value in [
'None', ' Who cares? ', 'True', '[]', '[3.14]', '3.14, 2.72']:
self.assertRaises(
IllegalValueError,
validator.coerce_float, value, ['whatever'])

def test_coerce_float_list(self):
"""Test coerce_float_list."""
validator = ParsecValidator()
# The good
for value, results in [
('', []),
('3', [3.0]),
('2*3.141592654', [3.141592654, 3.141592654]),
('12*8, 8*12.0', [8.0] * 12 + [12.0] * 8),
('-3, -2, -1, -0.0, 1.0', [-3.0, -2.0, -1.0, -0.0, 1.0]),
('6.02e23, -1.6021765e-19, 6.62607004e-34',
[6.02e23, -1.6021765e-19, 6.62607004e-34])]:
items = validator.coerce_float_list(value, ['whatever'])
for item, result in zip(items, results):
self.assertAlmostEqual(item, result)
# The bad
for value in [
'None', 'e, i, e, i, o', '[]', '[3.14]', 'pi, 2.72']:
self.assertRaises(
IllegalValueError,
validator.coerce_float_list, value, ['whatever'])

def test_coerce_int(self):
"""Test coerce_int."""
validator = ParsecValidator()
# The good
for value, result in [
('', None),
('0', 0),
('3', 3),
('-3', -3),
('-0', -0),
('653456', 653456),
('-8362583645365', -8362583645365)]:
self.assertAlmostEqual(
validator.coerce_int(value, ['whatever']), result)
# The bad
for value in [
'None', ' Who cares? ', 'True', '4.8', '[]', '[3]', '60*60']:
self.assertRaises(
IllegalValueError,
validator.coerce_int, value, ['whatever'])

def test_coerce_int_list(self):
"""Test coerce_int_list."""
validator = ParsecValidator()
# The good
for value, results in [
('', []),
('3', [3]),
('1..10, 11..20..2',
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19]),
('-10..10..3', [-10, -7, -4, -1, 2, 5, 8]),
('10*3, 4*-6', [3] * 10 + [-6] * 4),
('10*128, -78..-72, 2048',
[128] * 10 + [-78, -77, -76, -75, -74, -73, -72, 2048])]:
self.assertEqual(
validator.coerce_int_list(value, ['whatever']), results)
# The bad
for value in [
'None', 'e, i, e, i, o', '[]', '1..3, x', 'one..ten']:
self.assertRaises(
IllegalValueError,
validator.coerce_int_list, value, ['whatever'])

def test_coerce_str(self):
"""Test coerce_str."""
validator = ParsecValidator()
# The good
for value, result in [
('', ''),
('Hello World!', 'Hello World!'),
('"Hello World!"', 'Hello World!'),
('"Hello Cylc\'s World!"', 'Hello Cylc\'s World!'),
("'Hello World!'", 'Hello World!'),
('0', '0'),
('My list is:\nfoo, bar, baz\n', 'My list is:\nfoo, bar, baz'),
(' Hello:\n foo\n bar\n baz\n',
'Hello:\nfoo\nbar\nbaz'),
(' Hello:\n foo\n Greet\n baz\n',
'Hello:\n foo\nGreet\n baz'),
('False', 'False'),
('None', 'None')]:
self.assertAlmostEqual(
validator.coerce_str(value, ['whatever']), result)

def test_coerce_str_list(self):
"""Test coerce_str_list."""
validator = ParsecValidator()
# The good
for value, results in [
('', []),
('Hello', ['Hello']),
('"Hello"', ['Hello']),
('1', ['1']),
('Mercury, Venus, Earth, Mars',
['Mercury', 'Venus', 'Earth', 'Mars']),
('Mercury, Venus, Earth, Mars,\n"Jupiter",\n"Saturn"\n',
['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn']),
('New Zealand, United Kingdom',
['New Zealand', 'United Kingdom'])]:
self.assertEqual(
validator.coerce_str_list(value, ['whatever']), results)


if __name__ == '__main__':
unittest.main()
34 changes: 28 additions & 6 deletions lib/parsec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,11 @@ class ParsecValidator(object):
r"\A'''(.*?)'''\s*(?:#.*)?\Z", re.MULTILINE | re.DOTALL)
_REC_MULTI_LINE_DOUBLE = re.compile(
r'\A"""(.*?)"""\s*(?:#.*)?\Z', re.MULTILINE | re.DOTALL)
# integer range syntax START..END[..STEP]
_REC_INT_RANGE = re.compile(
r'\A([\+\-]?\d+)\.\.([\+\-]?\d+)(?:\.\.(\d+))?\Z')
# Paramterized names containing at least one comma.
_REC_MULTI_PARAM = re.compile(r'<[\w]+,.*?>')
_REC_PARAM_INT_RANGE = re.compile(
r'\A([\+\-]?\d+)\.\.([\+\-]?\d+)(?:\.\.(\d+))?\Z')
_REC_NAME_SUFFIX = re.compile(r'\A[\w\-+%@]+\Z')
_REC_TRIG_FUNC = re.compile(r'(\w+)\((.*)\)(?:\:(\w+))?')

# Value type constants
V_BOOLEAN = 'V_BOOLEAN'
Expand Down Expand Up @@ -224,8 +223,14 @@ def coerce_int(cls, value, keys):
@classmethod
def coerce_int_list(cls, value, keys):
"Coerce list values with optional multipliers to integer."
values = cls.strip_and_unquote_list(keys, value)
return cls.expand_list(values, keys, int)
items = []
for item in cls.strip_and_unquote_list(keys, value):
values = cls.parse_int_range(item)
if values is None:
items.extend(cls.expand_list([item], keys, int))
else:
items.extend(values)
return items

@classmethod
def coerce_str(cls, value, keys):
Expand Down Expand Up @@ -266,6 +271,23 @@ def expand_list(cls, values, keys, type_):
raise IllegalValueError('list', keys, item, exc)
return lvalues

@classmethod
def parse_int_range(cls, value):
"""Parse a value containing an integer range START..END[..STEP].
Return (list):
A list containing the integer values in range,
or None if value does not contain an integer range.
"""
match = cls._REC_INT_RANGE.match(value)
if match:
lower, upper, step = match.groups()
if not step:
step = 1
return range(int(lower), int(upper) + 1, int(step))
else:
return None

@classmethod
def strip_and_unquote(cls, keys, value):
"""Remove leading and trailing spaces and unquote value.
Expand Down
25 changes: 25 additions & 0 deletions tests/cylc.cfgvalidate/00-unit.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash
# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
# Copyright (C) 2008-2018 NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------------
# Run unittest for cylc.cfgvalidate
. "$(dirname "$0")/test_header"

set_test_number 1

run_ok "${TEST_NAME_BASE}" python "${TEST_SOURCE_DIR}/test_cfgvalidate.py"
cat "${TEST_NAME_BASE}.stderr" >&2
exit
Loading

0 comments on commit 3d37445

Please sign in to comment.