diff --git a/bin/cylc-broadcast b/bin/cylc-broadcast index 31d3728434e..ed6d6f02bee 100755 --- a/bin/cylc-broadcast +++ b/bin/cylc-broadcast @@ -86,7 +86,7 @@ from cylc.terminal import cli_function from cylc.broadcast_report import ( get_broadcast_change_report, get_broadcast_bad_options_report) from cylc.cfgspec.suite import SPEC, upg -from cylc.cfgvalidate import cylc_config_validate +from cylc.parsec.validate import cylc_config_validate from cylc.network.client import SuiteRuntimeClient from cylc.option_parsers import CylcOptionParser as COP from cylc.print_tree import print_tree diff --git a/lib/cylc/cfgspec/globalcfg.py b/lib/cylc/cfgspec/globalcfg.py index 37489120fe4..0df8f96fdbe 100644 --- a/lib/cylc/cfgspec/globalcfg.py +++ b/lib/cylc/cfgspec/globalcfg.py @@ -23,17 +23,16 @@ import shutil from tempfile import mkdtemp -from cylc.parsec.config import ParsecConfig -from cylc.parsec.exceptions import ParsecError -from cylc.parsec.upgrade import upgrader - from cylc import LOG -from cylc.cfgvalidate import ( - cylc_config_validate, CylcConfigValidator as VDR, DurationFloat) +from cylc import __version__ as CYLC_VERSION from cylc.exceptions import GlobalConfigError from cylc.hostuserutil import get_user_home, is_remote_user from cylc.network import Priv -from cylc import __version__ as CYLC_VERSION +from cylc.parsec.config import ParsecConfig +from cylc.parsec.exceptions import ParsecError +from cylc.parsec.upgrade import upgrader +from cylc.parsec.validate import ( + DurationFloat, CylcConfigValidator as VDR, cylc_config_validate) # Nested dict of spec items. # Spec value is [value_type, default, allowed_2, allowed_3, ...] diff --git a/lib/cylc/cfgspec/suite.py b/lib/cylc/cfgspec/suite.py index ea84951ed9e..0715c346981 100644 --- a/lib/cylc/cfgspec/suite.py +++ b/lib/cylc/cfgspec/suite.py @@ -19,13 +19,11 @@ from isodatetime.data import Calendar -from cylc.parsec.upgrade import upgrader -from cylc.parsec.config import ParsecConfig - -from cylc.cfgvalidate import ( - cylc_config_validate, CylcConfigValidator as VDR, DurationFloat) from cylc.network import Priv - +from cylc.parsec.config import ParsecConfig +from cylc.parsec.upgrade import upgrader +from cylc.parsec.validate import ( + DurationFloat, CylcConfigValidator as VDR, cylc_config_validate) # Nested dict of spec items. # Spec value is [value_type, default, allowed_2, allowed_3, ...] diff --git a/lib/cylc/cfgvalidate.py b/lib/cylc/cfgvalidate.py deleted file mode 100644 index f60f383a561..00000000000 --- a/lib/cylc/cfgvalidate.py +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/env python3 - -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2019 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 . -"""Extend parsec.validate for Cylc configuration. - -Coerce more value type from string (to time point, duration, xtriggers, etc.). -""" - -import re - -from isodatetime.data import Calendar, Duration, TimePoint -from isodatetime.dumpers import TimePointDumper -from isodatetime.parsers import DurationParser, TimePointParser -from cylc.parsec.validate import ParsecValidator, IllegalValueError - -from cylc.subprocctx import SubFuncContext - - -class DurationFloat(float): - """Duration in floating point seconds, but stringify as ISO8601 format.""" - - def __str__(self): - return str(Duration(seconds=self, standardize=True)) - - -class CylcConfigValidator(ParsecValidator): - """Type validator and coercer for Cylc configurations. - - Attributes: - .coercers (dict): - Map value type keys with coerce methods. - """ - # Parameterized names containing at least one comma. - _REC_NAME_SUFFIX = re.compile(r'\A[\w\-+%@]+\Z') - _REC_TRIG_FUNC = re.compile(r'(\w+)\((.*)\)(?::(\w+))?') - - # Value type constants - V_CYCLE_POINT = 'V_CYCLE_POINT' - V_CYCLE_POINT_FORMAT = 'V_CYCLE_POINT_FORMAT' - V_CYCLE_POINT_TIME_ZONE = 'V_CYCLE_POINT_TIME_ZONE' - V_INTERVAL = 'V_INTERVAL' - V_INTERVAL_LIST = 'V_INTERVAL_LIST' - V_PARAMETER_LIST = 'V_PARAMETER_LIST' - V_XTRIGGER = 'V_XTRIGGER' - - def __init__(self): - ParsecValidator.__init__(self) - self.coercers.update({ - self.V_CYCLE_POINT: self.coerce_cycle_point, - self.V_CYCLE_POINT_FORMAT: self.coerce_cycle_point_format, - self.V_CYCLE_POINT_TIME_ZONE: self.coerce_cycle_point_time_zone, - self.V_INTERVAL: self.coerce_interval, - self.V_INTERVAL_LIST: self.coerce_interval_list, - self.V_PARAMETER_LIST: self.coerce_parameter_list, - self.V_XTRIGGER: self.coerce_xtrigger, - }) - - @classmethod - def coerce_cycle_point(cls, value, keys): - """Coerce value to a cycle point.""" - if not value: - return None - value = cls.strip_and_unquote(keys, value) - if value == 'now': - # Handle this later in config.py when the suite UTC mode is known. - return value - if "next" in value or "previous" in value: - # Handle this later, as for "now". - return value - if value.isdigit(): - # Could be an old date-time cycle point format, or integer format. - return value - if "P" not in value and ( - value.startswith('-') or value.startswith('+')): - # We don't know the value given for num expanded year digits... - for i in range(1, 101): - try: - TimePointParser(num_expanded_year_digits=i).parse(value) - except ValueError: - continue - return value - raise IllegalValueError('cycle point', keys, value) - if "P" in value: - # ICP is an offset - parser = DurationParser() - try: - if value.startswith("-"): - # parser doesn't allow negative duration with this setup? - parser.parse(value[1:]) - else: - parser.parse(value) - return value - except ValueError: - raise IllegalValueError("cycle point", keys, value) - try: - TimePointParser().parse(value) - except ValueError: - raise IllegalValueError('cycle point', keys, value) - return value - - @classmethod - def coerce_cycle_point_format(cls, value, keys): - """Coerce to a cycle point format (either CCYYMM... or %Y%m...).""" - value = cls.strip_and_unquote(keys, value) - if not value: - return None - test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1, - hour_of_day=4, minute_of_hour=30, - second_of_minute=54) - if '/' in value: - raise IllegalValueError('cycle point format', keys, value) - if '%' in value: - try: - TimePointDumper().strftime(test_timepoint, value) - except ValueError: - raise IllegalValueError('cycle point format', keys, value) - return value - if 'X' in value: - for i in range(1, 101): - dumper = TimePointDumper(num_expanded_year_digits=i) - try: - dumper.dump(test_timepoint, value) - except ValueError: - continue - return value - raise IllegalValueError('cycle point format', keys, value) - dumper = TimePointDumper() - try: - dumper.dump(test_timepoint, value) - except ValueError: - raise IllegalValueError('cycle point format', keys, value) - return value - - @classmethod - def coerce_cycle_point_time_zone(cls, value, keys): - """Coerce value to a cycle point time zone format - Z, +13, -0800...""" - value = cls.strip_and_unquote(keys, value) - if not value: - return None - test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1, - hour_of_day=4, minute_of_hour=30, - second_of_minute=54) - dumper = TimePointDumper() - test_timepoint_string = dumper.dump(test_timepoint, 'CCYYMMDDThhmmss') - test_timepoint_string += value - parser = TimePointParser(allow_only_basic=True) - try: - parser.parse(test_timepoint_string) - except ValueError: - raise IllegalValueError( - 'cycle point time zone format', keys, value) - return value - - def coerce_interval(self, value, keys): - """Coerce an ISO 8601 interval (or number: back-comp) into seconds.""" - value = self.strip_and_unquote(keys, value) - if not value: - # Allow explicit empty values. - return None - try: - interval = DurationParser().parse(value) - except ValueError: - raise IllegalValueError("ISO 8601 interval", keys, value) - days, seconds = interval.get_days_and_seconds() - return DurationFloat( - days * Calendar.default().SECONDS_IN_DAY + seconds) - - def coerce_interval_list(self, value, keys): - """Coerce a list of intervals (or numbers: back-comp) into seconds.""" - return self.expand_list( - self.strip_and_unquote_list(keys, value), - keys, - lambda v: self.coerce_interval(v, keys)) - - @classmethod - def coerce_parameter_list(cls, value, keys): - """Coerce parameter list. - - Args: - value (str): - This can be a list of str values. Each str value must conform - to the same restriction as a task name. - Otherwise, this can be a mixture of int ranges and int values. - keys (list): - Keys in nested dict that represents the raw configuration. - - Return (list): - A list of strings or a list of sorted integers. - - Raise: - IllegalValueError: - If value has both str and int range or if a str value breaks - the task name restriction. - """ - items = [] - can_only_be = None # A flag to prevent mixing str and int range - for item in cls.strip_and_unquote_list(keys, value): - 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 - items.extend(values) - elif cls._REC_NAME_SUFFIX.match(item): - try: - int(item) - except ValueError: - if can_only_be == int: - raise IllegalValueError( - 'parameter', keys, value, - 'mixing int range and str') - can_only_be = str - items.append(item) - else: - raise IllegalValueError( - 'parameter', keys, value, '%s: bad value' % item) - 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. - - func_name(*func_args, **func_kwargs) - Checks for legal string templates in arg values too. - - """ - - label = keys[-1] - value = self.strip_and_unquote(keys, value) - if not value: - raise IllegalValueError("xtrigger", keys, value) - fname = None - args = [] - kwargs = {} - match = self._REC_TRIG_FUNC.match(value) - if match is None: - raise IllegalValueError("xtrigger", keys, value) - fname, fargs, intvl = match.groups() - if intvl: - intvl = self.coerce_interval(intvl, keys) - - if fargs: - # Extract function args and kwargs. - for farg in fargs.split(r','): - try: - key, val = farg.strip().split(r'=', 1) - except ValueError: - args.append(self._coerce_type(farg.strip())) - else: - kwargs[key.strip()] = self._coerce_type(val.strip()) - - return SubFuncContext(label, fname, args, kwargs, intvl) - - @classmethod - def _coerce_type(cls, value): - """Convert value to int, float, or bool, if possible.""" - try: - val = int(value) - except ValueError: - try: - val = float(value) - except ValueError: - if value == 'False': - val = False - elif value == 'True': - val = True - else: - # Leave as string. - val = cls.strip_and_unquote([], value) - return val - - -def cylc_config_validate(cfg_root, spec_root): - """Short for "CylcConfigValidator().validate(...)".""" - return CylcConfigValidator().validate(cfg_root, spec_root) diff --git a/lib/cylc/parsec/validate.py b/lib/cylc/parsec/validate.py index 1796679fd97..31023d32e2f 100644 --- a/lib/cylc/parsec/validate.py +++ b/lib/cylc/parsec/validate.py @@ -20,15 +20,21 @@ Check all items are legal. Check all values are legal (type; min, max, allowed options). Coerce value type from string (to int, float, list, etc.). +Coerce more value type from string (to time point, duration, xtriggers, etc.). Also provides default values from the spec as a nested dict. """ -from collections import deque import re +from collections import deque from textwrap import dedent +from isodatetime.data import Duration, TimePoint, Calendar +from isodatetime.dumpers import TimePointDumper +from isodatetime.parsers import TimePointParser, DurationParser + from cylc.parsec.exceptions import ( ListValueError, IllegalValueError, IllegalItemError) +from cylc.subprocctx import SubFuncContext class ParsecValidator(object): @@ -404,3 +410,265 @@ def _unquoted_list_parse(cls, keys, value): def parsec_validate(cfg_root, spec_root): """Short for "ParsecValidator().validate(...)".""" return ParsecValidator().validate(cfg_root, spec_root) + + +class DurationFloat(float): + """Duration in floating point seconds, but stringify as ISO8601 format.""" + + def __str__(self): + return str(Duration(seconds=self, standardize=True)) + + +class CylcConfigValidator(ParsecValidator): + """Type validator and coercer for Cylc configurations. + + Attributes: + .coercers (dict): + Map value type keys with coerce methods. + """ + # Parameterized names containing at least one comma. + _REC_NAME_SUFFIX = re.compile(r'\A[\w\-+%@]+\Z') + _REC_TRIG_FUNC = re.compile(r'(\w+)\((.*)\)(?::(\w+))?') + + # Value type constants + V_CYCLE_POINT = 'V_CYCLE_POINT' + V_CYCLE_POINT_FORMAT = 'V_CYCLE_POINT_FORMAT' + V_CYCLE_POINT_TIME_ZONE = 'V_CYCLE_POINT_TIME_ZONE' + V_INTERVAL = 'V_INTERVAL' + V_INTERVAL_LIST = 'V_INTERVAL_LIST' + V_PARAMETER_LIST = 'V_PARAMETER_LIST' + V_XTRIGGER = 'V_XTRIGGER' + + def __init__(self): + ParsecValidator.__init__(self) + self.coercers.update({ + self.V_CYCLE_POINT: self.coerce_cycle_point, + self.V_CYCLE_POINT_FORMAT: self.coerce_cycle_point_format, + self.V_CYCLE_POINT_TIME_ZONE: self.coerce_cycle_point_time_zone, + self.V_INTERVAL: self.coerce_interval, + self.V_INTERVAL_LIST: self.coerce_interval_list, + self.V_PARAMETER_LIST: self.coerce_parameter_list, + self.V_XTRIGGER: self.coerce_xtrigger, + }) + + @classmethod + def coerce_cycle_point(cls, value, keys): + """Coerce value to a cycle point.""" + if not value: + return None + value = cls.strip_and_unquote(keys, value) + if value == 'now': + # Handle this later in config.py when the suite UTC mode is known. + return value + if "next" in value or "previous" in value: + # Handle this later, as for "now". + return value + if value.isdigit(): + # Could be an old date-time cycle point format, or integer format. + return value + if "P" not in value and ( + value.startswith('-') or value.startswith('+')): + # We don't know the value given for num expanded year digits... + for i in range(1, 101): + try: + TimePointParser(num_expanded_year_digits=i).parse(value) + except ValueError: + continue + return value + raise IllegalValueError('cycle point', keys, value) + if "P" in value: + # ICP is an offset + parser = DurationParser() + try: + if value.startswith("-"): + # parser doesn't allow negative duration with this setup? + parser.parse(value[1:]) + else: + parser.parse(value) + return value + except ValueError: + raise IllegalValueError("cycle point", keys, value) + try: + TimePointParser().parse(value) + except ValueError: + raise IllegalValueError('cycle point', keys, value) + return value + + @classmethod + def coerce_cycle_point_format(cls, value, keys): + """Coerce to a cycle point format (either CCYYMM... or %Y%m...).""" + value = cls.strip_and_unquote(keys, value) + if not value: + return None + test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1, + hour_of_day=4, minute_of_hour=30, + second_of_minute=54) + if '/' in value: + raise IllegalValueError('cycle point format', keys, value) + if '%' in value: + try: + TimePointDumper().strftime(test_timepoint, value) + except ValueError: + raise IllegalValueError('cycle point format', keys, value) + return value + if 'X' in value: + for i in range(1, 101): + dumper = TimePointDumper(num_expanded_year_digits=i) + try: + dumper.dump(test_timepoint, value) + except ValueError: + continue + return value + raise IllegalValueError('cycle point format', keys, value) + dumper = TimePointDumper() + try: + dumper.dump(test_timepoint, value) + except ValueError: + raise IllegalValueError('cycle point format', keys, value) + return value + + @classmethod + def coerce_cycle_point_time_zone(cls, value, keys): + """Coerce value to a cycle point time zone format - Z, +13, -0800...""" + value = cls.strip_and_unquote(keys, value) + if not value: + return None + test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1, + hour_of_day=4, minute_of_hour=30, + second_of_minute=54) + dumper = TimePointDumper() + test_timepoint_string = dumper.dump(test_timepoint, 'CCYYMMDDThhmmss') + test_timepoint_string += value + parser = TimePointParser(allow_only_basic=True) + try: + parser.parse(test_timepoint_string) + except ValueError: + raise IllegalValueError( + 'cycle point time zone format', keys, value) + return value + + def coerce_interval(self, value, keys): + """Coerce an ISO 8601 interval (or number: back-comp) into seconds.""" + value = self.strip_and_unquote(keys, value) + if not value: + # Allow explicit empty values. + return None + try: + interval = DurationParser().parse(value) + except ValueError: + raise IllegalValueError("ISO 8601 interval", keys, value) + days, seconds = interval.get_days_and_seconds() + return DurationFloat( + days * Calendar.default().SECONDS_IN_DAY + seconds) + + def coerce_interval_list(self, value, keys): + """Coerce a list of intervals (or numbers: back-comp) into seconds.""" + return self.expand_list( + self.strip_and_unquote_list(keys, value), + keys, + lambda v: self.coerce_interval(v, keys)) + + @classmethod + def coerce_parameter_list(cls, value, keys): + """Coerce parameter list. + + Args: + value (str): + This can be a list of str values. Each str value must conform + to the same restriction as a task name. + Otherwise, this can be a mixture of int ranges and int values. + keys (list): + Keys in nested dict that represents the raw configuration. + + Return (list): + A list of strings or a list of sorted integers. + + Raise: + IllegalValueError: + If value has both str and int range or if a str value breaks + the task name restriction. + """ + items = [] + can_only_be = None # A flag to prevent mixing str and int range + for item in cls.strip_and_unquote_list(keys, value): + 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 + items.extend(values) + elif cls._REC_NAME_SUFFIX.match(item): + try: + int(item) + except ValueError: + if can_only_be == int: + raise IllegalValueError( + 'parameter', keys, value, + 'mixing int range and str') + can_only_be = str + items.append(item) + else: + raise IllegalValueError( + 'parameter', keys, value, '%s: bad value' % item) + 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. + + func_name(*func_args, **func_kwargs) + Checks for legal string templates in arg values too. + + """ + + label = keys[-1] + value = self.strip_and_unquote(keys, value) + if not value: + raise IllegalValueError("xtrigger", keys, value) + fname = None + args = [] + kwargs = {} + match = self._REC_TRIG_FUNC.match(value) + if match is None: + raise IllegalValueError("xtrigger", keys, value) + fname, fargs, intvl = match.groups() + if intvl: + intvl = self.coerce_interval(intvl, keys) + + if fargs: + # Extract function args and kwargs. + for farg in fargs.split(r','): + try: + key, val = farg.strip().split(r'=', 1) + except ValueError: + args.append(self._coerce_type(farg.strip())) + else: + kwargs[key.strip()] = self._coerce_type(val.strip()) + + return SubFuncContext(label, fname, args, kwargs, intvl) + + @classmethod + def _coerce_type(cls, value): + """Convert value to int, float, or bool, if possible.""" + try: + val = int(value) + except ValueError: + try: + val = float(value) + except ValueError: + if value == 'False': + val = False + elif value == 'True': + val = True + else: + # Leave as string. + val = cls.strip_and_unquote([], value) + return val + + +def cylc_config_validate(cfg_root, spec_root): + """Short for "CylcConfigValidator().validate(...)".""" + return CylcConfigValidator().validate(cfg_root, spec_root) diff --git a/lib/cylc/scheduler.py b/lib/cylc/scheduler.py index 5d48b2a3230..d44c0d25672 100644 --- a/lib/cylc/scheduler.py +++ b/lib/cylc/scheduler.py @@ -1318,7 +1318,7 @@ def set_auto_restart(self, restart_delay=None, Restart handled by `suite_auto_restart`. Args: - restart_delay (cylc.cfgvalidate.DurationFloat): + restart_delay (cylc.parsec.DurationFloat): Suite will wait a random period between 0 and `restart_delay` seconds before attempting to stop/restart in order to avoid multiple suites restarting simultaneously. diff --git a/lib/cylc/tests/parsec/test_config.py b/lib/cylc/tests/parsec/test_config.py index b9512c71b1b..099f0c36f87 100644 --- a/lib/cylc/tests/parsec/test_config.py +++ b/lib/cylc/tests/parsec/test_config.py @@ -19,12 +19,12 @@ import tempfile import unittest -from cylc.parsec import config, validate -from cylc.parsec.exceptions import ParsecError -from cylc.cfgvalidate import CylcConfigValidator as VDR -from cylc.cfgvalidate import cylc_config_validate +from cylc.parsec import config from cylc.parsec.OrderedDict import OrderedDictWithDefaults +from cylc.parsec.exceptions import ParsecError from cylc.parsec.upgrade import upgrader +from cylc.parsec.validate import ( + cylc_config_validate, IllegalItemError, CylcConfigValidator as VDR) SAMPLE_SPEC_1 = { 'section1': { @@ -176,7 +176,7 @@ def test_validate(self): sparse = OrderedDictWithDefaults() parsec_config.validate(sparse) # empty dict is OK - with self.assertRaises(validate.IllegalItemError): + with self.assertRaises(IllegalItemError): sparse = OrderedDictWithDefaults() sparse['name'] = 'True' parsec_config.validate(sparse) # name is not valid diff --git a/lib/cylc/tests/parsec/test_validate.py b/lib/cylc/tests/parsec/test_validate.py index cb4a42bfa74..778cabee429 100644 --- a/lib/cylc/tests/parsec/test_validate.py +++ b/lib/cylc/tests/parsec/test_validate.py @@ -18,9 +18,12 @@ import unittest -from cylc.cfgvalidate import CylcConfigValidator as VDR from cylc.parsec.OrderedDict import OrderedDictWithDefaults -from cylc.parsec.validate import * +from cylc.parsec.exceptions import IllegalValueError +from cylc.parsec.validate import ( + CylcConfigValidator as VDR, + CylcConfigValidator, DurationFloat, ListValueError, + IllegalItemError, ParsecValidator, parsec_validate) SAMPLE_SPEC_1 = { 'section1': { @@ -467,6 +470,133 @@ def test_strip_and_unquote_list_multiparam(self): ['a'], 'a, b, c' ) + def test_coerce_cycle_point(self): + """Test coerce_cycle_point.""" + validator = CylcConfigValidator() + # The good + for value, result in [ + ('', None), + ('3', '3'), + ('2018', '2018'), + ('20181225T12Z', '20181225T12Z'), + ('2018-12-25T12:00+11:00', '2018-12-25T12:00+11:00')]: + self.assertEqual( + validator.coerce_cycle_point(value, ['whatever']), result) + # The bad + for value in [ + 'None', ' Who cares? ', 'True', '1, 2', '20781340E10']: + self.assertRaises( + IllegalValueError, + validator.coerce_cycle_point, value, ['whatever']) + + def test_coerce_cycle_point_format(self): + """Test coerce_cycle_point_format.""" + validator = CylcConfigValidator() + # The good + for value, result in [ + ('', None), + ('%Y%m%dT%H%M%z', '%Y%m%dT%H%M%z'), + ('CCYYMMDDThhmmZ', 'CCYYMMDDThhmmZ'), + ('XCCYYMMDDThhmmZ', 'XCCYYMMDDThhmmZ')]: + self.assertEqual( + validator.coerce_cycle_point_format(value, ['whatever']), + result) + # The bad + for value in ['%i%j', 'Y/M/D']: + self.assertRaises( + IllegalValueError, + validator.coerce_cycle_point_format, value, ['whatever']) + + def test_coerce_cycle_point_time_zone(self): + """Test coerce_cycle_point_time_zone.""" + validator = CylcConfigValidator() + # The good + for value, result in [ + ('', None), + ('Z', 'Z'), + ('+0000', '+0000'), + ('+0100', '+0100'), + ('+1300', '+1300'), + ('-0630', '-0630')]: + self.assertEqual( + validator.coerce_cycle_point_time_zone(value, ['whatever']), + result) + # The bad + for value in ['None', 'Big Bang Time', 'Standard Galaxy Time']: + self.assertRaises( + IllegalValueError, + validator.coerce_cycle_point_time_zone, value, ['whatever']) + + def test_coerce_interval(self): + """Test coerce_interval.""" + validator = CylcConfigValidator() + # The good + for value, result in [ + ('', None), + ('P3D', DurationFloat(259200)), + ('PT10M10S', DurationFloat(610))]: + self.assertEqual( + validator.coerce_interval(value, ['whatever']), result) + # The bad + for value in ['None', '5 days', '20', '-12']: + self.assertRaises( + IllegalValueError, + validator.coerce_interval, value, ['whatever']) + + def test_coerce_interval_list(self): + """Test coerce_interval_list.""" + validator = CylcConfigValidator() + # The good + for value, results in [ + ('', []), + ('P3D', [DurationFloat(259200)]), + ('P3D, PT10M10S', [DurationFloat(259200), DurationFloat(610)]), + ('25*PT30M,10*PT1H', + [DurationFloat(1800)] * 25 + [DurationFloat(3600)] * 10)]: + items = validator.coerce_interval_list(value, ['whatever']) + for item, result in zip(items, results): + self.assertAlmostEqual(item, result) + # The bad + for value in ['None', '5 days', '20', 'PT10S, -12']: + self.assertRaises( + IllegalValueError, + validator.coerce_interval_list, value, ['whatever']) + + def test_coerce_parameter_list(self): + """Test coerce_parameter_list.""" + validator = CylcConfigValidator() + # The good + for value, result in [ + ('', []), + ('planet', ['planet']), + ('planet, star, galaxy', ['planet', 'star', 'galaxy']), + ('1..5, 21..25', [1, 2, 3, 4, 5, 21, 22, 23, 24, 25]), + ('-15, -10, -5, -1..1', [-15, -10, -5, -1, 0, 1])]: + self.assertEqual( + validator.coerce_parameter_list(value, ['whatever']), result) + # The bad + for value in ['foo/bar', 'p1, 1..10', '2..3, 4, p']: + self.assertRaises( + IllegalValueError, + validator.coerce_parameter_list, value, ['whatever']) + + def test_coerce_xtrigger(self): + """Test coerce_xtrigger.""" + validator = CylcConfigValidator() + # The good + for value, result in [ + ('foo(x="bar")', 'foo(x=bar)'), + ('foo(x, y, z="zebra")', 'foo(x, y, z=zebra)')]: + self.assertEqual( + validator.coerce_xtrigger(value, ['whatever']).get_signature(), + result) + # The bad + for value in [ + '', 'foo(', 'foo)', 'foo,bar']: + self.assertRaises( + IllegalValueError, + validator.coerce_xtrigger, value, ['whatever']) + if __name__ == '__main__': unittest.main() diff --git a/lib/cylc/tests/test_cfgvalidate.py b/lib/cylc/tests/test_cfgvalidate.py deleted file mode 100644 index ebf477d453b..00000000000 --- a/lib/cylc/tests/test_cfgvalidate.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python3 - -# THIS FILE IS PART OF THE CYLC SUITE ENGINE. -# Copyright (C) 2008-2019 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 . -"""Unit Tests for cylc.cfgvalidate.CylcConfigValidator.coerce* methods.""" - -import unittest - -from cylc.cfgvalidate import ( - CylcConfigValidator, DurationFloat, IllegalValueError) - - -class TestCylcConfigValidator(unittest.TestCase): - """Unit Tests for cylc.cfgvalidate.CylcConfigValidator.coerce* methods.""" - - def test_coerce_cycle_point(self): - """Test coerce_cycle_point.""" - validator = CylcConfigValidator() - # The good - for value, result in [ - ('', None), - ('3', '3'), - ('2018', '2018'), - ('20181225T12Z', '20181225T12Z'), - ('2018-12-25T12:00+11:00', '2018-12-25T12:00+11:00')]: - self.assertEqual( - validator.coerce_cycle_point(value, ['whatever']), result) - # The bad - for value in [ - 'None', ' Who cares? ', 'True', '1, 2', '20781340E10']: - self.assertRaises( - IllegalValueError, - validator.coerce_cycle_point, value, ['whatever']) - - def test_coerce_cycle_point_format(self): - """Test coerce_cycle_point_format.""" - validator = CylcConfigValidator() - # The good - for value, result in [ - ('', None), - ('%Y%m%dT%H%M%z', '%Y%m%dT%H%M%z'), - ('CCYYMMDDThhmmZ', 'CCYYMMDDThhmmZ'), - ('XCCYYMMDDThhmmZ', 'XCCYYMMDDThhmmZ')]: - self.assertEqual( - validator.coerce_cycle_point_format(value, ['whatever']), - result) - # The bad - for value in ['%i%j', 'Y/M/D']: - self.assertRaises( - IllegalValueError, - validator.coerce_cycle_point_format, value, ['whatever']) - - def test_coerce_cycle_point_time_zone(self): - """Test coerce_cycle_point_time_zone.""" - validator = CylcConfigValidator() - # The good - for value, result in [ - ('', None), - ('Z', 'Z'), - ('+0000', '+0000'), - ('+0100', '+0100'), - ('+1300', '+1300'), - ('-0630', '-0630')]: - self.assertEqual( - validator.coerce_cycle_point_time_zone(value, ['whatever']), - result) - # The bad - for value in ['None', 'Big Bang Time', 'Standard Galaxy Time']: - self.assertRaises( - IllegalValueError, - validator.coerce_cycle_point_time_zone, value, ['whatever']) - - def test_coerce_interval(self): - """Test coerce_interval.""" - validator = CylcConfigValidator() - # The good - for value, result in [ - ('', None), - ('P3D', DurationFloat(259200)), - ('PT10M10S', DurationFloat(610))]: - self.assertEqual( - validator.coerce_interval(value, ['whatever']), result) - # The bad - for value in ['None', '5 days', '20', '-12']: - self.assertRaises( - IllegalValueError, - validator.coerce_interval, value, ['whatever']) - - def test_coerce_interval_list(self): - """Test coerce_interval_list.""" - validator = CylcConfigValidator() - # The good - for value, results in [ - ('', []), - ('P3D', [DurationFloat(259200)]), - ('P3D, PT10M10S', [DurationFloat(259200), DurationFloat(610)]), - ('25*PT30M,10*PT1H', - [DurationFloat(1800)] * 25 + [DurationFloat(3600)] * 10)]: - items = validator.coerce_interval_list(value, ['whatever']) - for item, result in zip(items, results): - self.assertAlmostEqual(item, result) - # The bad - for value in ['None', '5 days', '20', 'PT10S, -12']: - self.assertRaises( - IllegalValueError, - validator.coerce_interval_list, value, ['whatever']) - - def test_coerce_parameter_list(self): - """Test coerce_parameter_list.""" - validator = CylcConfigValidator() - # The good - for value, result in [ - ('', []), - ('planet', ['planet']), - ('planet, star, galaxy', ['planet', 'star', 'galaxy']), - ('1..5, 21..25', [1, 2, 3, 4, 5, 21, 22, 23, 24, 25]), - ('-15, -10, -5, -1..1', [-15, -10, -5, -1, 0, 1])]: - self.assertEqual( - validator.coerce_parameter_list(value, ['whatever']), result) - # The bad - for value in ['foo/bar', 'p1, 1..10', '2..3, 4, p']: - self.assertRaises( - IllegalValueError, - validator.coerce_parameter_list, value, ['whatever']) - - def test_coerce_xtrigger(self): - """Test coerce_xtrigger.""" - validator = CylcConfigValidator() - # The good - for value, result in [ - ('foo(x="bar")', 'foo(x=bar)'), - ('foo(x, y, z="zebra")', 'foo(x, y, z=zebra)')]: - self.assertEqual( - validator.coerce_xtrigger(value, ['whatever']).get_signature(), - result) - # The bad - for value in [ - '', 'foo(', 'foo)', 'foo,bar']: - self.assertRaises( - IllegalValueError, - validator.coerce_xtrigger, value, ['whatever']) - - -if __name__ == '__main__': - unittest.main()