Skip to content
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
23 changes: 23 additions & 0 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,29 @@ def pipeline_hooks(cls):
#: :default: :class:`True`
build_locally = variable(typ.Bool, value=True, loggable=True)

#: .. versionadded:: 4.2
#:
#: Extra options to be passed to the child CI pipeline generated for this
#: test using the :option:`--ci-generate` option.
#:
#: This variable is a dictionary whose keys refer the CI generate backend
#: and the values can be in any CI backend-specific format.
#:
#: Currently, the only key supported is ``'gitlab'`` and the values is a
#: Gitlab configuration in JSON format. For example, if we want a pipeline
#: to run only when files in ``backend`` or ``src/main.c`` have changed,
#: this variable should be set as follows:
#:
#: .. code-block:: python
#:
#: ci_extras = {
#: 'only': {'changes': ['backend/*', 'src/main.c']}
#: }
#:
#: :type: :class:`dict`
#: :default: ``{}``
ci_extras = variable(typ.Dict[typ.Str['gitlab'], object], value={})

# Special variables

#: Dry-run mode
Expand Down
27 changes: 23 additions & 4 deletions reframe/frontend/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import sys
import yaml

import reframe.core.exceptions as errors
import reframe.core.runtime as runtime
from reframe.core.exceptions import ReframeError


def _emit_gitlab_pipeline(testcases, child_pipeline_opts):
Expand Down Expand Up @@ -46,6 +46,19 @@ def rfm_command(testcase):
*child_pipeline_opts
])

def _valid_ci_extras(extras):
'''Validate Gitlab CI pipeline extras'''

for kwd in ('stage', 'script', 'needs'):
if kwd in extras:
errmsg = f"invalid keyword found: {kwd!r}"
if kwd == 'script':
errmsg += " (use 'before_script' or 'after_script')"

raise ReframeError(f"could not validate 'ci_extras': {errmsg}")

return extras

max_level = 0 # We need the maximum level to generate the stages section
json = {
'cache': {
Expand All @@ -62,13 +75,19 @@ def rfm_command(testcase):
json['image'] = image_name

for tc in testcases:
ci_extras = _valid_ci_extras(tc.check.ci_extras.get('gitlab', {}))
extra_artifacts = ci_extras.pop('artifacts', {})
extra_artifact_paths = extra_artifacts.pop('paths', [])
json[f'{tc.check.unique_name}'] = {
'stage': f'rfm-stage-{tc.level}',
'script': [rfm_command(tc)],
'artifacts': {
'paths': [f'{tc.check.unique_name}-report.json']
'paths': [f'{tc.check.unique_name}-report.json',
*extra_artifact_paths],
**extra_artifacts
},
'needs': [t.check.unique_name for t in tc.deps]
'needs': [t.check.unique_name for t in tc.deps],
**ci_extras
}
max_level = max(max_level, tc.level)

Expand All @@ -78,7 +97,7 @@ def rfm_command(testcase):

def emit_pipeline(fp, testcases, child_pipeline_opts=None, backend='gitlab'):
if backend != 'gitlab':
raise errors.ReframeError(f'unknown CI backend {backend!r}')
raise ReframeError(f'unknown CI backend {backend!r}')

child_pipeline_opts = child_pipeline_opts or []
yaml.dump(_emit_gitlab_pipeline(testcases, child_pipeline_opts), stream=fp,
Expand Down
71 changes: 66 additions & 5 deletions unittests/test_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,27 @@
import reframe.frontend.ci as ci
import reframe.frontend.dependencies as dependencies
import reframe.frontend.executors as executors
from reframe.core.exceptions import ReframeError
from reframe.frontend.loader import RegressionCheckLoader


def _generate_test_cases(checks):
return dependencies.toposort(
dependencies.build_deps(executors.generate_testcases(checks))[0]
)


@pytest.fixture
def hello_test():
from unittests.resources.checks.hellocheck import HelloTest
return HelloTest()


def test_ci_gitlab_pipeline():
loader = RegressionCheckLoader([
'unittests/resources/checks_unlisted/deps_complex.py'
])
cases = dependencies.toposort(
dependencies.build_deps(
executors.generate_testcases(loader.load_all())
)[0]
)
cases = _generate_test_cases(loader.load_all())
with io.StringIO() as fp:
ci.emit_pipeline(fp, cases)
pipeline = fp.getvalue()
Expand All @@ -41,3 +50,55 @@ def test_ci_gitlab_pipeline():

schema = response.json()
jsonschema.validate(yaml.safe_load(pipeline), schema)


def test_ci_gitlab_ci_extras(hello_test):
hello_test.ci_extras = {
'gitlab': {
'before_script': ['touch foo.txt'],
'after_script': ['echo done'],
'artifacts': {
'paths': ['foo.txt']
},
'only': {
'changes': ['src/foo.c']
}
}
}
cases = _generate_test_cases([hello_test])
with io.StringIO() as fp:
ci.emit_pipeline(fp, cases)
pipeline = fp.getvalue()

pipeline_json = yaml.safe_load(pipeline)['HelloTest']
assert pipeline_json['before_script'] == ['touch foo.txt']
assert pipeline_json['after_script'] == ['echo done']
assert pipeline_json['artifacts'] == {
'paths': ['HelloTest-report.json', 'foo.txt']
}
assert pipeline_json['only'] == {'changes': ['src/foo.c']}


def test_ci_gitlab_ci_extras_invalid(hello_test):
with pytest.raises(TypeError):
hello_test.ci_extras = {
'before_script': ['touch foo.txt']
}

with pytest.raises(TypeError):
hello_test.ci_extras = {
'foolab': {
'before_script': ['touch foo.txt']
}
}

# Check invalid keywords
for kwd in ('stage', 'script', 'needs'):
hello_test.ci_extras = {
'gitlab': {kwd: 'something'}
}
cases = _generate_test_cases([hello_test])
with io.StringIO() as fp:
with pytest.raises(ReframeError,
match=rf'invalid keyword found: {kwd!r}'):
ci.emit_pipeline(fp, cases)