Skip to content

Commit

Permalink
Add integration tests for xtrigger validation, which provides simple …
Browse files Browse the repository at this point in the history
…examples for each of the built in xtriggers.

- Xrandom validate function
- Init test xrandom validate function
- Add unit tests for validation of built in xtriggers
  • Loading branch information
wxtim committed Jan 31, 2024
1 parent da37214 commit 6fea50f
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 2 deletions.
2 changes: 1 addition & 1 deletion cylc/flow/xtriggers/echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def validate(f_args, f_kwargs, f_signature):
This is separate from the xtrigger to allow parse-time validation.
"""
if "succeed" not in f_kwargs or not type(f_kwargs["succeed"]) is bool:
if "succeed" not in f_kwargs or not isinstance(f_kwargs["succeed"], bool):
raise WorkflowConfigError(
f"Requires 'succeed=True/False' arg: {f_signature}"
)
2 changes: 1 addition & 1 deletion cylc/flow/xtriggers/wall_clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,5 @@ def validate(f_args, f_kwargs, f_signature):
# must be a valid interval
try:
interval_parse(f_kwargs["offset"])
except ValueError:
except (ValueError, AttributeError):
raise WorkflowConfigError(f"Invalid offset: {f_signature}")
47 changes: 47 additions & 0 deletions cylc/flow/xtriggers/xrandom.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from random import random, randint
from time import sleep

from cylc.flow.exceptions import WorkflowConfigError


COLORS = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]
SIZES = ["tiny", "small", "medium", "large", "huge", "humongous"]
Expand Down Expand Up @@ -89,3 +91,48 @@ def xrandom(percent, secs=0, _=None):
'SIZE': SIZES[randint(0, len(SIZES) - 1)] # nosec
}
return satisfied, results


def validate(f_args, f_kwargs, f_signature):
"""Validate and manipulate args parsed from the workflow config.
percent: - 0 ≤ x ≤ 100
secs: An int.
If f_args used, convert to f_kwargs for clarity.
"""
n_args = len(f_args)
n_kwargs = len(f_kwargs)

if n_args + n_kwargs > 3:
raise WorkflowConfigError(f"Too many args: {f_signature}")

if n_args != 1:
raise WorkflowConfigError(f"Wrong number of args: {f_signature}")

if n_kwargs:
# kwargs must be "secs" and "_"
kw = next(iter(f_kwargs))
if kw not in ("secs", "_"):
raise WorkflowConfigError(f"Illegal arg '{kw}': {f_signature}")

# convert to kwarg
f_kwargs["percent"] = f_args[0]
del f_args[0]

try:
percent = f_kwargs['percent']
assert isinstance(percent, (float, int))
assert percent >= 0
assert percent <= 100
except AssertionError:
raise WorkflowConfigError(
f"'percent' should be a float between 0 and 100: {f_signature}")

try:
secs = f_kwargs['secs']
assert isinstance(secs, int)
except AssertionError:
raise WorkflowConfigError(
f"'secs' should be an integer: {f_signature}")
122 changes: 122 additions & 0 deletions tests/integration/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@

from pathlib import Path
import sqlite3
from typing import TYPE_CHECKING
import pytest

from cylc.flow.exceptions import ServiceFileError, WorkflowConfigError
from cylc.flow.parsec.exceptions import ListValueError
from cylc.flow.pathutil import get_workflow_run_pub_db_path

if TYPE_CHECKING:
from types import Any

Fixture = Any


@pytest.mark.parametrize(
'task_name,valid', [
Expand Down Expand Up @@ -341,3 +347,119 @@ def test_validate_incompatible_db(one_conf, flow, validate):
finally:
conn.close()
assert tables == ['suite_params']


def test_xtrig_validation_wall_clock(
flow: 'Fixture',
validate: 'Fixture',
):
"""If an xtrigger module has a `validate_config` it is called.
https://github.com/cylc/cylc-flow/issues/5448
"""
id_ = flow({
'scheduler': {'allow implicit tasks': True},
'scheduling': {
'initial cycle point': '1012',
'xtriggers': {'myxt': 'wall_clock(offset=PT755MH)'},
'graph': {'R1': '@myxt => foo'},
}
})
with pytest.raises(
WorkflowConfigError,
match=r'Invalid offset: wall_clock\(offset=PT755MH\)'
):
validate(id_)


def test_xtrig_validation_echo(
flow: 'Fixture',
validate: 'Fixture',
):
"""If an xtrigger module has a `validate_config` it is called.
https://github.com/cylc/cylc-flow/issues/5448
"""
id_ = flow({
'scheduler': {'allow implicit tasks': True},
'scheduling': {
'initial cycle point': '1012',
'xtriggers': {'myxt': 'echo()'},
'graph': {'R1': '@myxt => foo'},
}
})
with pytest.raises(
WorkflowConfigError,
match=r'Requires \'succeed=True/False\' arg: echo()'
):
validate(id_)


def test_xtrig_validation_xrandom(
flow: 'Fixture',
validate: 'Fixture',
):
"""If an xtrigger module has a `validate_config` it is called.
https://github.com/cylc/cylc-flow/issues/5448
"""
id_ = flow({
'scheduler': {'allow implicit tasks': True},
'scheduling': {
'initial cycle point': '1012',
'xtriggers': {'myxt': 'xrandom()'},
'graph': {'R1': '@myxt => foo'},
}
})
with pytest.raises(
WorkflowConfigError,
match=r'Wrong number of args: xrandom\(\)'
):
validate(id_)


def test_xtrig_validation_custom(
flow: 'Fixture',
validate: 'Fixture',
monkeypatch: 'Fixture',
):
"""If an xtrigger module has a `validate_config`
an exception is raised if that validate function fails.
https://github.com/cylc/cylc-flow/issues/5448
Rather than create our own xtrigger module on disk
and attempt to trigger a validation failure we
mock our own exception, xtrigger and xtrigger
validation functions and inject these into the
appropriate locations:
"""
GreenExc = type('Green', (Exception,), {})

def kustom_mock(suite):
return True, {}

def kustom_validate(args, kwargs, sig):
raise GreenExc('This is only a test.')

monkeypatch.setattr(
'cylc.flow.xtrigger_mgr.get_xtrig_func',
lambda *args: kustom_mock,
)
monkeypatch.setattr(
'cylc.flow.config.get_xtrig_func',
lambda *args: kustom_validate if "validate" in args else ''
)

id_ = flow({
'scheduler': {'allow implicit tasks': True},
'scheduling': {
'initial cycle point': '1012',
'xtriggers': {'myxt': 'kustom_xt(feature=42)'},
'graph': {'R1': '@myxt => foo'},
}
})

Path(id_)
with pytest.raises(GreenExc, match=r'This is only a test.'):
validate(id_)
37 changes: 37 additions & 0 deletions tests/unit/xtriggers/test_echo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) 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/>.

from cylc.flow.exceptions import WorkflowConfigError
from cylc.flow.xtriggers.echo import validate
import pytest
from pytest import param


def test_validate_good_path():
assert validate(
[], {'succeed': False, 'egg': 'fried', 'potato': 'baked'}, 'succeed'
) is None


@pytest.mark.parametrize(
'args, kwargs', (
param([False], {}, id='no-kwarg'),
param([], {'spud': 'mashed'}, id='no-succeed-kwarg'),
)
)
def test_validate_exceptions(args, kwargs):
with pytest.raises(WorkflowConfigError, match='^Requires'):
validate(args, kwargs, 'blah')
51 changes: 51 additions & 0 deletions tests/unit/xtriggers/test_wall_clock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) 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/>.

from cylc.flow.exceptions import WorkflowConfigError
from cylc.flow.xtriggers.wall_clock import validate
from metomi.isodatetime.parsers import DurationParser
import pytest
from pytest import param


@pytest.fixture
def monkeypatch_interval_parser(monkeypatch):
"""Interval parse only works normally if a WorkflowSpecifics
object identify the parser to be used.
"""
monkeypatch.setattr(
'cylc.flow.xtriggers.wall_clock.interval_parse',
DurationParser().parse
)


def test_validate_good_path(monkeypatch_interval_parser):
assert validate([], {}, 'Alles Gut') is None


@pytest.mark.parametrize(
'args, kwargs, err', (
param([1, 2], {}, "^Too", id='too-many-args'),
param([], {'egg': 12}, "^Illegal", id='illegal-arg'),
param([1], {}, "^Invalid", id='invalid-offset-int'),
param([], {'offset': 'Zaphod'}, "^Invalid", id='invalid-offset-str'),
)
)
def test_validate_exceptions(
monkeypatch_interval_parser, args, kwargs, err
):
with pytest.raises(WorkflowConfigError, match=err):
validate(args, kwargs, 'Alles Gut')
42 changes: 42 additions & 0 deletions tests/unit/xtriggers/test_xrandom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) 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/>.

import pytest
from pytest import param

from cylc.flow.xtriggers.xrandom import validate
from cylc.flow.exceptions import WorkflowConfigError


def test_validate_good_path():
assert validate([1], {'secs': 0, '_': 'HelloWorld'}, 'good_path') is None


@pytest.mark.parametrize(
'args, kwargs, err', (
param([100], {'f': 1.1, 'b': 1, 'x': 2}, 'Too', id='too-many-args'),
param([], {}, 'Wrong number', id='too-few-args'),
param(['foo'], {}, '\'percent', id='percent-not-numeric'),
param([101], {}, '\'percent', id='percent>100'),
param([-1], {}, '\'percent', id='percent<0'),
param([100], {'egg': 1}, 'Illegal', id='invalid-kwarg'),
param([100], {'secs': 1.1}, "'secs'", id='secs-not-int'),
)
)
def test_validate_exceptions(args, kwargs, err):
"""Illegal args and kwargs cause a WorkflowConfigError raised."""
with pytest.raises(WorkflowConfigError, match=f'^{err}'):
validate(args, kwargs, 'blah')

0 comments on commit 6fea50f

Please sign in to comment.