Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically validate xtrigger function signature #59

Merged
merged 3 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
5 changes: 1 addition & 4 deletions cylc/flow/subprocpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,7 @@ def get_xtrig_func(mod_name, func_name, src_dir):

mod = get_xtrig_mod(mod_name, src_dir)

try:
_XTRIG_FUNC_CACHE[(mod_name, func_name)] = getattr(mod, func_name)
except AttributeError:
raise
_XTRIG_FUNC_CACHE[(mod_name, func_name)] = getattr(mod, func_name)

return _XTRIG_FUNC_CACHE[(mod_name, func_name)]

Expand Down
11 changes: 11 additions & 0 deletions cylc/flow/xtrigger_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from contextlib import suppress
from enum import Enum
from inspect import signature
import json
import re
from copy import deepcopy
Expand Down Expand Up @@ -279,12 +280,22 @@ def check_xtrigger(
fname,
f"'{fname}' not found in xtrigger module '{fname}'",
)

if not callable(func):
raise XtriggerConfigError(
label,
fname,
f"'{fname}' not callable in xtrigger module '{fname}'",
)
if func is not wall_clock:
# Validate args and kwargs against the function signature
# (but not for wall_clock because it's a special case).
wxtim marked this conversation as resolved.
Show resolved Hide resolved
try:
signature(func).bind(*fctx.func_args, **fctx.func_kwargs)
except TypeError as exc:
raise XtriggerConfigError(
label, fname, f"{fctx.get_signature()}: {exc}"
)

# Check any string templates in the function arg values (note this
# won't catch bad task-specific values - which are added dynamically).
Expand Down
15 changes: 0 additions & 15 deletions cylc/flow/xtriggers/xrandom.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,6 @@ def validate(f_args, f_kwargs, f_signature):
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 + n_kwargs < 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]
Expand Down
63 changes: 41 additions & 22 deletions tests/integration/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@

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

from cylc.flow.exceptions import ServiceFileError, WorkflowConfigError
from cylc.flow.exceptions import (
ServiceFileError,
WorkflowConfigError,
XtriggerConfigError,
)
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
Fixture = Any


@pytest.mark.parametrize(
Expand Down Expand Up @@ -353,7 +354,7 @@ def test_xtrig_validation_wall_clock(
flow: 'Fixture',
validate: 'Fixture',
):
"""If an xtrigger module has a `validate_config` it is called.
"""If an xtrigger module has a `validate()` function is called.

https://github.com/cylc/cylc-flow/issues/5448
"""
Expand All @@ -376,14 +377,13 @@ def test_xtrig_validation_echo(
flow: 'Fixture',
validate: 'Fixture',
):
"""If an xtrigger module has a `validate_config` it is called.
"""If an xtrigger module has a `validate()` function 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'},
}
Expand All @@ -399,21 +399,20 @@ def test_xtrig_validation_xrandom(
flow: 'Fixture',
validate: 'Fixture',
):
"""If an xtrigger module has a `validate_config` it is called.
"""If an xtrigger module has a `validate()` function 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()'},
'xtriggers': {'myxt': 'xrandom(200)'},
'graph': {'R1': '@myxt => foo'},
}
})
with pytest.raises(
WorkflowConfigError,
match=r'Wrong number of args: xrandom\(\)'
match=r"'percent' should be a float between 0 and 100:"
):
validate(id_)

Expand All @@ -423,29 +422,30 @@ def test_xtrig_validation_custom(
validate: 'Fixture',
monkeypatch: 'Fixture',
):
"""If an xtrigger module has a `validate_config`
"""If an xtrigger module has a `validate()` function
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:
"""
# 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):
def kustom_xt(feature):
return True, {}

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

# Patch xtrigger func
monkeypatch.setattr(
'cylc.flow.xtrigger_mgr.get_xtrig_func',
lambda *args: kustom_mock,
lambda *args: kustom_xt,
)
# Patch xtrigger's validate func
monkeypatch.setattr(
'cylc.flow.config.get_xtrig_func',
lambda *args: kustom_validate if "validate" in args else ''
Expand All @@ -463,3 +463,22 @@ def kustom_validate(args, kwargs, sig):
Path(id_)
with pytest.raises(GreenExc, match=r'This is only a test.'):
validate(id_)


def test_xtrig_signature_validation(
flow: 'Fixture',
validate: 'Fixture',
):
"""Test automatic xtrigger function signature validation."""
id_ = flow({
'scheduler': {'allow implicit tasks': True},
'scheduling': {
'xtriggers': {'myxt': 'xrandom()'},
'graph': {'R1': '@myxt => foo'},
}
})
with pytest.raises(
XtriggerConfigError,
match=r"xrandom\(\): missing a required argument: 'percent'"
):
validate(id_)
11 changes: 4 additions & 7 deletions tests/unit/xtriggers/test_xrandom.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,10 @@ def test_validate_good_path():

@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'),
param(['foo'], {}, r"'percent", id='percent-not-numeric'),
param([101], {}, r"'percent", id='percent>100'),
param([-1], {}, r"'percent", id='percent<0'),
param([100], {'secs': 1.1}, r"'secs'", id='secs-not-int'),
)
)
def test_validate_exceptions(args, kwargs, err):
Expand Down
Loading